source: tailor/vcpx/cvs.py @ 50

Revision 50, 14.5 KB checked in by lele@…, 9 years ago (diff)

Force sticky tags after checkout

Line 
1#! /usr/bin/python
2# -*- mode: python; coding: utf-8 -*-
3# :Progetto: vcpx -- CVS details
4# :Creato:   mer 16 giu 2004 00:46:12 CEST
5# :Autore:   Lele Gaifax <lele@nautilus.homeip.net>
6#
7
8"""
9This module contains supporting classes for CVS. To get a
10cross-repository revision number ala Subversion, the implementation
11uses `cvsps` to fetch the changes from the upstream repository.
12"""
13
14__docformat__ = 'reStructuredText'
15
16from shwrap import SystemCommand
17from source import UpdatableSourceWorkingDir
18from target import SyncronizableTargetWorkingDir
19
20
21class CvsPsLog(SystemCommand):
22    COMMAND = "cvsps %(update)s-b %(branch)s 2>/dev/null"
23
24    def __call__(self, output=None, dry_run=False, **kwargs):
25        update = kwargs.get('update', '')
26        if update:
27            update = '-u '
28        kwargs['update'] = update
29       
30        return SystemCommand.__call__(self, output=output,
31                                      dry_run=dry_run, **kwargs)
32
33   
34class CvsUpdate(SystemCommand):
35    COMMAND = 'cvs -q %(dry)supdate -d -r%(revision)s %(entry)s 2>&1'
36   
37    def __call__(self, output=None, dry_run=False, **kwargs):
38        if dry_run:
39            kwargs['dry'] = '-n '
40        else:
41            kwargs['dry'] = ''
42
43        return SystemCommand.__call__(self, output=output,
44                                      dry_run=False, **kwargs)
45
46
47class CvsAdd(SystemCommand):
48    COMMAND = "cvs -q add %(entry)s"
49
50
51class CvsCommit(SystemCommand):
52    COMMAND = "cvs -q ci -F %(logfile)s %(entries)s"
53   
54
55class CvsRemove(SystemCommand):
56    COMMAND = "cvs -q remove %(entry)s"
57
58
59class CvsCheckout(SystemCommand):
60    COMMAND = "cvs -q -d%(repository)s checkout -r %(revision)s %(module)s"
61
62
63def compare_cvs_revs(rev1, rev2):
64    """Compare two CVS revision numerically, not alphabetically."""
65
66    if not rev1: rev1 = '0'
67    if not rev2: rev2 = '0'
68
69    r1 = [int(n) for n in rev1.split('.')]
70    r2 = [int(n) for n in rev2.split('.')]
71   
72    return cmp(r1, r2)
73
74
75class CvsWorkingDir(UpdatableSourceWorkingDir,
76                    SyncronizableTargetWorkingDir):
77
78    """
79    An instance of this class represent a read/write CVS working directory,
80    so that it can be used both as source of patches, or as a target
81    repository.
82
83    It uses `cvsps` to do the actual fetch of the changesets metadata
84    from the server, so that we can reasonably group together related
85    changes that would otherwise be sparsed, as CVS is file-centric.
86    """
87   
88    ## UpdatableSourceWorkingDir
89   
90    def _getUpstreamChangesets(self, root, sincerev=None):
91        cvsps = CvsPsLog(working_dir=root)
92       
93        from os.path import join, exists
94       
95        fname = join(root, 'CVS', 'Tag')
96        if exists(fname):
97            branch=open(fname).read()[1:-1]
98        else:
99            branch="HEAD"
100
101        if sincerev:
102            sincerev = int(sincerev)
103           
104        changesets = []
105        log = cvsps(output=True, update=True, branch=branch)
106        for cs in self.__enumerateChangesets(log, sincerev):
107            changesets.append(cs)
108
109        return changesets
110   
111    def __enumerateChangesets(self, log, sincerev=None):
112        """
113        Parse CVSps log.
114        """
115
116        from changes import Changeset, ChangesetEntry
117        from datetime import datetime
118       
119        # cvsps output sample:
120        ## ---------------------
121        ## PatchSet 1500
122        ## Date: 2004/05/09 17:54:22
123        ## Author: grubert
124        ## Branch: HEAD
125        ## Tag: (none)
126        ## Log:
127        ## Tell the reason for using mbox (not wrapping long lines).
128        ##
129        ## Members:
130        ##         docutils/writers/latex2e.py:1.78->1.79
131       
132        log.seek(0)
133
134        l = None
135        while 1:
136            l = log.readline()
137            if l <> '---------------------\n':
138                break
139
140            l = log.readline()
141            assert l.startswith('PatchSet '), "Parse error: %s"%l
142
143            pset = {}
144            pset['revision'] = l[9:-1].strip()
145            l = log.readline()
146            while not l.startswith('Log:'):
147                field,value = l.split(':',1)
148                pset[field.lower()] = value.strip()
149                l = log.readline()
150
151            msg = []
152            l = log.readline()
153            msg.append(l)
154            l = log.readline()
155            while l <> 'Members: \n':
156                msg.append(l)
157                l = log.readline()
158
159            pset['log'] = ''.join(msg)
160
161            assert l.startswith('Members:'), "Parse error: %s" % l
162
163            pset['entries'] = entries = []
164            l = log.readline()
165
166            while l.startswith('\t'):
167                if not sincerev or (sincerev<int(pset['revision'])):
168                    file,revs = l[1:-1].split(':')
169                    fromrev,torev = revs.split('->')
170
171                    e = ChangesetEntry(file)
172                    e.old_revision = fromrev
173                    e.new_revision = torev
174
175                    if fromrev=='INITIAL':
176                        e.action_kind = e.ADDED
177                    elif "(DEAD)" in torev:
178                        e.action_kind = e.DELETED
179                        e.new_revision = torev[:torev.index('(DEAD)')]
180                    else:
181                        e.action_kind = e.UPDATED
182
183                    entries.append(e)
184                   
185                l = log.readline()
186
187            if not sincerev or (sincerev<int(pset['revision'])):
188                cvsdate = pset['date']
189                y,m,d = map(int, cvsdate[:10].split('/'))
190                hh,mm,ss = map(int, cvsdate[11:19].split(':'))
191                timestamp = datetime(y, m, d, hh, mm, ss)
192                pset['date'] = timestamp
193           
194                yield Changeset(**pset)
195
196    def _applyChangeset(self, root, changeset):
197        from os.path import join, exists, dirname
198        from os import makedirs
199       
200        cvsup = CvsUpdate(working_dir=root)
201        for e in changeset.entries:
202            edir = dirname(join(root, e.name))
203            if e.action_kind != e.DELETED and not exists(edir):
204                makedirs(edir)
205
206            cvsup(output=True, entry=e.name, revision=e.new_revision)
207
208            if e.action_kind == e.DELETED:
209                # XXX: should drop edir if empty
210                pass
211               
212
213    ## SyncronizableTargetWorkingDir
214
215    def _addEntry(self, root, entry):
216        """
217        Add a new entry, maybe registering the directory as well.
218        """
219
220        from os.path import split, join, exists
221
222        basedir = split(entry)[0]
223        if basedir and not exists(join(root, basedir, 'CVS')):
224            self._addEntry(root, basedir)
225       
226        c = CvsAdd(working_dir=root)
227        c(entry=entry)
228
229    def _checkoutUpstreamRevision(self, basedir, repository, module, revision):
230        """
231        Concretely do the checkout of the upstream sources. Use `revision` as
232        the name of the tag to get.
233
234        Return the effective cvsps revision.
235        """
236
237        from os.path import join, exists
238       
239        wdir = join(basedir, module)
240        if not exists(wdir):
241            c = CvsCheckout(working_dir=basedir)
242            c(output=True,
243              repository=repository,
244              module=module,
245              revision=revision)
246           
247        self.__forceTagOnEachEntry(wdir)
248       
249        entries = CvsEntries(wdir)
250       
251        # update cvsps cache, then loop over the changesets and find the
252        # last applied, to find out the actual cvsps revision
253
254        csets = self._getUpstreamChangesets(wdir)
255        csets.reverse()
256        found = False
257        for cset in csets:
258            for m in cset.entries:
259                info = entries.getFileInfo(m.name)
260                if info:
261                    actualversion = info.cvs_version
262                    found = compare_cvs_revs(actualversion,m.new_revision)>=0
263                    if not found:
264                        break
265               
266            if found:
267                last = cset
268                break
269
270        assert found, "Something went wrong, did not find the right cvsps revision in '%s'" % wdir
271       
272        return last.revision
273
274    def __forceTagOnEachEntry(self, root):
275        """
276        Massage each CVS/Entries file, locking (ie, tagging) each
277        entry to its current CVS version.
278
279        This is to prevent silly errors such those that could arise
280        after a manual `cvs update` in the working directory.
281        """
282       
283        from os import walk, rename
284        from os.path import join
285
286        for dir, subdirs, files in walk(root):
287            if dir[-3:] == 'CVS':
288                efn = join(dir, 'Entries')
289                f = open(efn)
290                entries = f.readlines()
291                f.close()
292                rename(efn, efn+'.old')
293               
294                newentries = []
295                for e in entries:
296                    if e.startswith('/'):
297                        fields = e.split('/')
298                        fields[-1] = "T%s\n" % fields[2]
299                        newe = '/'.join(fields)
300                        newentries.append(newe)
301                    else:
302                        newentries.append(e)
303
304                f = open(efn, 'w')
305                f.writelines(newentries)
306                f.close()
307   
308    def _commit(self,root, date, author, remark, changelog=None, entries=None):
309        """
310        Commit the changeset.
311        """
312       
313        from tempfile import NamedTemporaryFile
314       
315        log = NamedTemporaryFile(bufsize=0)
316        log.write(remark)
317        log.write('\n')
318        if changelog:
319            log.write(changelog)
320            log.write('\n')
321       
322        c = CvsCommit(working_dir=root)
323
324        if entries:
325            entries = ' '.join(entries)
326        else:
327            entries = '.'
328           
329        c(entries=entries, logfile=log.name)
330        log.close()
331       
332    def _removeEntry(self, root, entry):
333        """
334        Remove an entry.
335        """
336
337        c = CvsRemove(working_dir=root)
338        c(entry=entry)
339
340    def _renameEntry(self, root, oldentry, newentry):
341        """
342        Rename an entry.
343        """
344
345        self._removeEntry(root, oldentry)
346        self._addEntry(root, newentry)
347
348    def _initializeWorkingDir(self, root, addentry=None):
349        """
350        Add the given directory to an already existing CVS working tree.
351        """
352
353        SyncronizableTargetWorkingDir._initializeWorkingDir(self, root, CvsAdd)
354
355
356class CvsEntry(object):
357    """Collect the info about a file in a CVS working dir."""
358   
359    __slots__ = ('filename', 'cvs_version', 'cvs_tag')
360
361    def __init__(self, entry):
362        """Initialize a CvsEntry."""
363       
364        dummy, fn, rev, date, dummy, tag = entry.split('/')
365        self.filename = fn
366        self.cvs_version = rev
367        self.cvs_tag = tag
368
369    def __str__(self):
370        return "CvsEntry('%s', '%s', '%s')" % (self.filename,
371                                               self.cvs_version,
372                                               self.cvs_tag)
373
374
375class CvsEntries(object):
376    """Collection of CvsEntry."""
377
378    __slots__ = ('files', 'directories', 'deleted')
379   
380    def __init__(self, root):
381        """Parse CVS/Entries file.
382
383           Walk down the working directory, collecting info from each
384           CVS/Entries found."""
385
386        from os.path import join, exists, isdir
387        from os import listdir
388       
389        self.files = {}
390        """Dict of `CvsEntry`, keyed on each file under revision control."""
391       
392        self.directories = {}
393        """Dict of `CvsEntries`, keyed on subdirectories under revision
394           control."""
395
396        self.deleted = False
397        """Flag to denote that this directory was removed."""
398       
399        entries = join(root, 'CVS/Entries')
400        if exists(entries):
401            for entry in open(entries).readlines():
402                entry = entry[:-1]
403
404                if entry.startswith('/'):
405                    e = CvsEntry(entry)
406                    if file and e.filename==file:
407                        return e
408                    else:
409                        self.files[e.filename] = e
410                elif entry.startswith('D/'):
411                    d = entry.split('/')[1]
412                    subdir = CvsEntries(join(root, d))
413                    self.directories[d] = subdir
414                elif entry == 'D':
415                    self.deleted = True 
416
417            # Sometimes the Entries file does not contain the directories:
418            # crawl the current directory looking for missing ones.
419
420            for entry in listdir(root):
421                if entry == '.svn':
422                    continue               
423                dir = join(root, entry)
424                if (isdir(dir) and exists(join(dir, 'CVS/Entries'))
425                    and not self.directories.has_key(entry)):
426                    self.directories[entry] = CvsEntries(dir)
427                   
428            if self.deleted:
429                self.deleted = not self.files and not self.directories
430           
431    def __str__(self):
432        return "CvsEntries(%d files, %d subdirectories)" % (
433            len(self.files), len(self.directories))
434
435    def getFileInfo(self, fpath):
436        """Fetch the info about a path, if known.  Otherwise return None."""
437
438        try:
439            if '/' in fpath:
440                dir,rest = fpath.split('/', 1)
441                return self.directories[dir].getFileInfo(rest)
442            else:
443                return self.files[fpath]
444        except KeyError:
445            return None
446
447    def removedDirectories(self, other, prefix=''):
448        from os.path import join
449       
450        result = []
451        for d in self.directories.keys():
452            a = self.directories.get(d)
453            b = other.directories.get(d)
454            dirpath = join(prefix, d)
455            if not b or b.deleted:
456                result.append(dirpath)
457            else:
458                result.extend(a.removedDirectories(b, prefix=dirpath))
459        return result
460
461    def addedDirectories(self, other, prefix=''):
462        from os.path import join
463       
464        result = []
465        for d in other.directories.keys():
466            a = self.directories.get(d)
467            b = other.directories.get(d)
468            dirpath = join(prefix, d)
469            if not a:
470                result.append(dirpath)
471            else:
472                result.extend(a.addedDirectories(b, prefix=dirpath))
473        return result
474   
475    def compareDirectories(self, other):
476        """Compare the directories with those of another instance and return
477           a tuple (added, removed)."""
478
479        added = self.addedDirectories(other)
480        removed = self.removedDirectories(other)
481
482        return (added, removed)
483
484
Note: See TracBrowser for help on using the repository browser.