source: tailor/vcpx/cvs.py @ 51

Revision 51, 15.3 KB checked in by lele@…, 9 years ago (diff)

Use the logger instead of print statements

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, ChangesetApplicationFailure
18from target import SyncronizableTargetWorkingDir, TargetInitializationFailure
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, logger=None):
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 cvsup.exit_status:
209                raise ChangesetApplicationFailure(
210                    "'cvs update' returned status %s" % cvsup.exit_status)
211           
212            if e.action_kind == e.DELETED:
213                # XXX: should drop edir if empty
214                pass
215               
216    def _checkoutUpstreamRevision(self, basedir, repository, module, revision,
217                                  logger=None):
218        """
219        Concretely do the checkout of the upstream sources. Use `revision` as
220        the name of the tag to get.
221
222        Return the effective cvsps revision.
223        """
224
225        from os.path import join, exists
226       
227        wdir = join(basedir, module)
228        if not exists(wdir):
229            c = CvsCheckout(working_dir=basedir)
230            c(output=True,
231              repository=repository,
232              module=module,
233              revision=revision)
234            if c.exit_status:
235                raise TargetInitializationFailure(
236                    "'cvs checkout' returned status %s" % c.exit_status)
237        else:
238            if logger: logger.info("Using existing %s", wdir)
239           
240        self.__forceTagOnEachEntry(wdir)
241       
242        entries = CvsEntries(wdir)
243       
244        # update cvsps cache, then loop over the changesets and find the
245        # last applied, to find out the actual cvsps revision
246
247        csets = self._getUpstreamChangesets(wdir)
248        csets.reverse()
249        found = False
250        for cset in csets:
251            for m in cset.entries:
252                info = entries.getFileInfo(m.name)
253                if info:
254                    actualversion = info.cvs_version
255                    found = compare_cvs_revs(actualversion,m.new_revision)>=0
256                    if not found:
257                        break
258               
259            if found:
260                last = cset
261                break
262
263        if not found:
264            raise TargetInitializationFailure(
265                "Something went wrong, did not find the right cvsps "
266                "revision in '%s'" % wdir)
267        else:
268            if logger: logger.info("working copy up to cvsps revision %s",
269                                   last.revision)
270           
271        return last.revision
272   
273
274    ## SyncronizableTargetWorkingDir
275
276    def _addEntry(self, root, entry):
277        """
278        Add a new entry, maybe registering the directory as well.
279        """
280
281        from os.path import split, join, exists
282
283        basedir = split(entry)[0]
284        if basedir and not exists(join(root, basedir, 'CVS')):
285            self._addEntry(root, basedir)
286       
287        c = CvsAdd(working_dir=root)
288        c(entry=entry)
289
290    def __forceTagOnEachEntry(self, root):
291        """
292        Massage each CVS/Entries file, locking (ie, tagging) each
293        entry to its current CVS version.
294
295        This is to prevent silly errors such those that could arise
296        after a manual `cvs update` in the working directory.
297        """
298       
299        from os import walk, rename
300        from os.path import join
301
302        for dir, subdirs, files in walk(root):
303            if dir[-3:] == 'CVS':
304                efn = join(dir, 'Entries')
305                f = open(efn)
306                entries = f.readlines()
307                f.close()
308                rename(efn, efn+'.old')
309               
310                newentries = []
311                for e in entries:
312                    if e.startswith('/'):
313                        fields = e.split('/')
314                        fields[-1] = "T%s\n" % fields[2]
315                        newe = '/'.join(fields)
316                        newentries.append(newe)
317                    else:
318                        newentries.append(e)
319
320                f = open(efn, 'w')
321                f.writelines(newentries)
322                f.close()
323   
324    def _commit(self,root, date, author, remark, changelog=None, entries=None):
325        """
326        Commit the changeset.
327        """
328       
329        from tempfile import NamedTemporaryFile
330       
331        log = NamedTemporaryFile(bufsize=0)
332        log.write(remark)
333        log.write('\n')
334        if changelog:
335            log.write(changelog)
336            log.write('\n')
337       
338        c = CvsCommit(working_dir=root)
339
340        if entries:
341            entries = ' '.join(entries)
342        else:
343            entries = '.'
344           
345        c(entries=entries, logfile=log.name)
346        log.close()
347       
348    def _removeEntry(self, root, entry):
349        """
350        Remove an entry.
351        """
352
353        c = CvsRemove(working_dir=root)
354        c(entry=entry)
355
356    def _renameEntry(self, root, oldentry, newentry):
357        """
358        Rename an entry.
359        """
360
361        self._removeEntry(root, oldentry)
362        self._addEntry(root, newentry)
363
364    def _initializeWorkingDir(self, root, addentry=None):
365        """
366        Add the given directory to an already existing CVS working tree.
367        """
368
369        SyncronizableTargetWorkingDir._initializeWorkingDir(self, root, CvsAdd)
370
371
372class CvsEntry(object):
373    """Collect the info about a file in a CVS working dir."""
374   
375    __slots__ = ('filename', 'cvs_version', 'cvs_tag')
376
377    def __init__(self, entry):
378        """Initialize a CvsEntry."""
379       
380        dummy, fn, rev, date, dummy, tag = entry.split('/')
381        self.filename = fn
382        self.cvs_version = rev
383        self.cvs_tag = tag
384
385    def __str__(self):
386        return "CvsEntry('%s', '%s', '%s')" % (self.filename,
387                                               self.cvs_version,
388                                               self.cvs_tag)
389
390
391class CvsEntries(object):
392    """Collection of CvsEntry."""
393
394    __slots__ = ('files', 'directories', 'deleted')
395   
396    def __init__(self, root):
397        """Parse CVS/Entries file.
398
399           Walk down the working directory, collecting info from each
400           CVS/Entries found."""
401
402        from os.path import join, exists, isdir
403        from os import listdir
404       
405        self.files = {}
406        """Dict of `CvsEntry`, keyed on each file under revision control."""
407       
408        self.directories = {}
409        """Dict of `CvsEntries`, keyed on subdirectories under revision
410           control."""
411
412        self.deleted = False
413        """Flag to denote that this directory was removed."""
414       
415        entries = join(root, 'CVS/Entries')
416        if exists(entries):
417            for entry in open(entries).readlines():
418                entry = entry[:-1]
419
420                if entry.startswith('/'):
421                    e = CvsEntry(entry)
422                    if file and e.filename==file:
423                        return e
424                    else:
425                        self.files[e.filename] = e
426                elif entry.startswith('D/'):
427                    d = entry.split('/')[1]
428                    subdir = CvsEntries(join(root, d))
429                    self.directories[d] = subdir
430                elif entry == 'D':
431                    self.deleted = True 
432
433            # Sometimes the Entries file does not contain the directories:
434            # crawl the current directory looking for missing ones.
435
436            for entry in listdir(root):
437                if entry == '.svn':
438                    continue               
439                dir = join(root, entry)
440                if (isdir(dir) and exists(join(dir, 'CVS/Entries'))
441                    and not self.directories.has_key(entry)):
442                    self.directories[entry] = CvsEntries(dir)
443                   
444            if self.deleted:
445                self.deleted = not self.files and not self.directories
446           
447    def __str__(self):
448        return "CvsEntries(%d files, %d subdirectories)" % (
449            len(self.files), len(self.directories))
450
451    def getFileInfo(self, fpath):
452        """Fetch the info about a path, if known.  Otherwise return None."""
453
454        try:
455            if '/' in fpath:
456                dir,rest = fpath.split('/', 1)
457                return self.directories[dir].getFileInfo(rest)
458            else:
459                return self.files[fpath]
460        except KeyError:
461            return None
462
463    def removedDirectories(self, other, prefix=''):
464        from os.path import join
465       
466        result = []
467        for d in self.directories.keys():
468            a = self.directories.get(d)
469            b = other.directories.get(d)
470            dirpath = join(prefix, d)
471            if not b or b.deleted:
472                result.append(dirpath)
473            else:
474                result.extend(a.removedDirectories(b, prefix=dirpath))
475        return result
476
477    def addedDirectories(self, other, prefix=''):
478        from os.path import join
479       
480        result = []
481        for d in other.directories.keys():
482            a = self.directories.get(d)
483            b = other.directories.get(d)
484            dirpath = join(prefix, d)
485            if not a:
486                result.append(dirpath)
487            else:
488                result.extend(a.addedDirectories(b, prefix=dirpath))
489        return result
490   
491    def compareDirectories(self, other):
492        """Compare the directories with those of another instance and return
493           a tuple (added, removed)."""
494
495        added = self.addedDirectories(other)
496        removed = self.removedDirectories(other)
497
498        return (added, removed)
499
500
Note: See TracBrowser for help on using the repository browser.