source: tailor/vcpx/cvs.py @ 58

Revision 58, 17.5 KB checked in by lele@…, 9 years ago (diff)

Little fixups

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        branch="HEAD"
96        fname = join(root, 'CVS', 'Tag')
97        if exists(fname):
98            tag = open(fname).read()
99            if tag.startswith('T'):
100                branch=tag[1:-1]
101
102        if sincerev:
103            sincerev = int(sincerev)
104           
105        changesets = []
106        log = cvsps(output=True, update=True, branch=branch)
107        for cs in self.__enumerateChangesets(log, sincerev):
108            changesets.append(cs)
109
110        return changesets
111   
112    def __enumerateChangesets(self, log, sincerev=None):
113        """
114        Parse CVSps log.
115        """
116
117        from changes import Changeset, ChangesetEntry
118        from datetime import datetime
119       
120        # cvsps output sample:
121        ## ---------------------
122        ## PatchSet 1500
123        ## Date: 2004/05/09 17:54:22
124        ## Author: grubert
125        ## Branch: HEAD
126        ## Tag: (none)
127        ## Log:
128        ## Tell the reason for using mbox (not wrapping long lines).
129        ##
130        ## Members:
131        ##         docutils/writers/latex2e.py:1.78->1.79
132       
133        log.seek(0)
134
135        l = None
136        while 1:
137            l = log.readline()
138            if l <> '---------------------\n':
139                break
140
141            l = log.readline()
142            assert l.startswith('PatchSet '), "Parse error: %s"%l
143
144            pset = {}
145            pset['revision'] = l[9:-1].strip()
146            l = log.readline()
147            while not l.startswith('Log:'):
148                field,value = l.split(':',1)
149                pset[field.lower()] = value.strip()
150                l = log.readline()
151
152            msg = []
153            l = log.readline()
154            msg.append(l)
155            l = log.readline()
156            while l <> 'Members: \n':
157                msg.append(l)
158                l = log.readline()
159
160            pset['log'] = ''.join(msg)
161
162            assert l.startswith('Members:'), "Parse error: %s" % l
163
164            pset['entries'] = entries = []
165            l = log.readline()
166
167            while l.startswith('\t'):
168                if not sincerev or (sincerev<int(pset['revision'])):
169                    file,revs = l[1:-1].split(':')
170                    fromrev,torev = revs.split('->')
171
172                    e = ChangesetEntry(file)
173                    e.old_revision = fromrev
174                    e.new_revision = torev
175
176                    if fromrev=='INITIAL':
177                        e.action_kind = e.ADDED
178                    elif "(DEAD)" in torev:
179                        e.action_kind = e.DELETED
180                        e.new_revision = torev[:torev.index('(DEAD)')]
181                    else:
182                        e.action_kind = e.UPDATED
183
184                    entries.append(e)
185                   
186                l = log.readline()
187
188            if not sincerev or (sincerev<int(pset['revision'])):
189                cvsdate = pset['date']
190                y,m,d = map(int, cvsdate[:10].split('/'))
191                hh,mm,ss = map(int, cvsdate[11:19].split(':'))
192                timestamp = datetime(y, m, d, hh, mm, ss)
193                pset['date'] = timestamp
194           
195                yield Changeset(**pset)
196
197    def _applyChangeset(self, root, changeset, logger=None):
198        from os.path import join, exists, dirname
199        from os import makedirs
200       
201        entries = CvsEntries(root)
202       
203        cvsup = CvsUpdate(working_dir=root)
204        for e in changeset.entries:
205            if e.action_kind == e.UPDATED:
206                info = entries.getFileInfo(e.name)
207                if info and info.cvs_version == e.new_revision:
208                    if logger: logger.debug("skipping '%s' since it's already "
209                                            "at revision %s", e.name,
210                                            e.new_revision)
211                    continue
212               
213            cvsup(output=True, entry=e.name, revision=e.new_revision)
214
215            if cvsup.exit_status:
216                raise ChangesetApplicationFailure(
217                    "'cvs update' returned status %s" % cvsup.exit_status)
218           
219            if e.action_kind == e.DELETED:
220                # XXX: should drop edir if empty
221                pass
222               
223    def _checkoutUpstreamRevision(self, basedir, repository, module, revision,
224                                  logger=None):
225        """
226        Concretely do the checkout of the upstream sources. Use `revision` as
227        the name of the tag to get.
228
229        Return the effective cvsps revision.
230        """
231
232        from os.path import join, exists
233       
234        wdir = join(basedir, module)
235        if not exists(wdir):
236            c = CvsCheckout(working_dir=basedir)
237            c(output=True,
238              repository=repository,
239              module=module,
240              revision=revision)
241            if c.exit_status:
242                raise TargetInitializationFailure(
243                    "'cvs checkout' returned status %s" % c.exit_status)
244        else:
245            if logger: logger.info("Using existing %s", wdir)
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        if not found:
271            raise TargetInitializationFailure(
272                "Something went wrong, did not find the right cvsps "
273                "revision in '%s'" % wdir)
274        else:
275            if logger: logger.info("working copy up to cvsps revision %s",
276                                   last.revision)
277           
278        return last.revision
279   
280    def _willApplyChangeset(self, changeset):
281        """
282        This gets called just before applying each changeset.
283       
284        Since CVS has no "createdir" event, we have to take care
285        of new directories, creating empty-but-reasonable CVS dirs.
286        """
287
288        for m in changeset.entries:
289            if m.action_kind == m.ADDED:
290                self.__createParentCVSDirectories(m.name)
291           
292        return True
293
294    def __createParentCVSDirectories(self, path):
295        """
296        Verify that the hierarchy down to the entry is under CVS.
297
298        If the directory containing the entry does not exists,
299        create it and make it appear as under CVS so that succeding
300        'cvs update' will work.
301        """
302       
303        from os.path import split, join, exists
304        from os import mkdir
305
306        basedir = split(path)[0]
307        if not basedir:
308            return
309       
310        cvsarea = join(basedir, 'CVS') 
311        if basedir and not exists(cvsarea):
312            parentcvs = self.__createParentCVSDirectories(basedir)
313
314            assert parentcvs, "Uhm, strange things happen"
315           
316            if not exists(basedir):
317                mkdir(basedir)
318
319            # Create fake CVS area
320            mkdir(cvsarea)
321
322            # Create an empty "Entries" file
323            entries = open(join(cvsarea, 'Entries'), 'w')
324            entries.close()
325
326            reposf = open(join(parentcvs, 'Repository'))
327            rep = reposf.readline()[:-1]
328            reposf.close()
329
330            reposf = open(join(cvsarea, 'Repository'), 'w')
331            reposf.write("%s/%s\n" % (rep, split(basedir)[1]))
332            reposf.close()
333
334            rootf = open(join(parentcvs, 'Root'))
335            root = rootf.readline()
336            rootf.close()
337
338            rootf = open(join(cvsarea, 'Root'), 'w')
339            rootf.write(root)
340            rootf.close()
341
342        return cvsarea
343   
344    ## SyncronizableTargetWorkingDir
345
346    def _addEntry(self, root, entry):
347        """
348        Add a new entry, maybe registering the directory as well.
349        """
350
351        from os.path import split, join, exists
352
353        basedir = split(entry)[0]
354        if basedir and not exists(join(root, basedir, 'CVS')):
355            self._addEntry(root, basedir)
356       
357        c = CvsAdd(working_dir=root)
358        c(entry=entry)
359
360    def __forceTagOnEachEntry(self, root):
361        """
362        Massage each CVS/Entries file, locking (ie, tagging) each
363        entry to its current CVS version.
364
365        This is to prevent silly errors such those that could arise
366        after a manual `cvs update` in the working directory.
367        """
368       
369        from os import walk, rename
370        from os.path import join
371
372        for dir, subdirs, files in walk(root):
373            if dir[-3:] == 'CVS':
374                efn = join(dir, 'Entries')
375                f = open(efn)
376                entries = f.readlines()
377                f.close()
378                rename(efn, efn+'.old')
379               
380                newentries = []
381                for e in entries:
382                    if e.startswith('/'):
383                        fields = e.split('/')
384                        fields[-1] = "T%s\n" % fields[2]
385                        newe = '/'.join(fields)
386                        newentries.append(newe)
387                    else:
388                        newentries.append(e)
389
390                f = open(efn, 'w')
391                f.writelines(newentries)
392                f.close()
393   
394    def _commit(self,root, date, author, remark, changelog=None, entries=None):
395        """
396        Commit the changeset.
397        """
398       
399        from tempfile import NamedTemporaryFile
400       
401        log = NamedTemporaryFile(bufsize=0)
402        log.write(remark)
403        log.write('\n')
404        if changelog:
405            log.write(changelog)
406            log.write('\n')
407       
408        c = CvsCommit(working_dir=root)
409
410        if entries:
411            entries = ' '.join(entries)
412        else:
413            entries = '.'
414           
415        c(entries=entries, logfile=log.name)
416        log.close()
417       
418    def _removeEntry(self, root, entry):
419        """
420        Remove an entry.
421        """
422
423        c = CvsRemove(working_dir=root)
424        c(entry=entry)
425
426    def _renameEntry(self, root, oldentry, newentry):
427        """
428        Rename an entry.
429        """
430
431        self._removeEntry(root, oldentry)
432        self._addEntry(root, newentry)
433
434    def _initializeWorkingDir(self, root, addentry=None):
435        """
436        Add the given directory to an already existing CVS working tree.
437        """
438
439        SyncronizableTargetWorkingDir._initializeWorkingDir(self, root, CvsAdd)
440
441
442class CvsEntry(object):
443    """Collect the info about a file in a CVS working dir."""
444   
445    __slots__ = ('filename', 'cvs_version', 'cvs_tag')
446
447    def __init__(self, entry):
448        """Initialize a CvsEntry."""
449       
450        dummy, fn, rev, date, dummy, tag = entry.split('/')
451        self.filename = fn
452        self.cvs_version = rev
453        self.cvs_tag = tag
454
455    def __str__(self):
456        return "CvsEntry('%s', '%s', '%s')" % (self.filename,
457                                               self.cvs_version,
458                                               self.cvs_tag)
459
460
461class CvsEntries(object):
462    """Collection of CvsEntry."""
463
464    __slots__ = ('files', 'directories', 'deleted')
465   
466    def __init__(self, root):
467        """Parse CVS/Entries file.
468
469           Walk down the working directory, collecting info from each
470           CVS/Entries found."""
471
472        from os.path import join, exists, isdir
473        from os import listdir
474       
475        self.files = {}
476        """Dict of `CvsEntry`, keyed on each file under revision control."""
477       
478        self.directories = {}
479        """Dict of `CvsEntries`, keyed on subdirectories under revision
480           control."""
481
482        self.deleted = False
483        """Flag to denote that this directory was removed."""
484       
485        entries = join(root, 'CVS/Entries')
486        if exists(entries):
487            for entry in open(entries).readlines():
488                entry = entry[:-1]
489
490                if entry.startswith('/'):
491                    e = CvsEntry(entry)
492                    if file and e.filename==file:
493                        return e
494                    else:
495                        self.files[e.filename] = e
496                elif entry.startswith('D/'):
497                    d = entry.split('/')[1]
498                    subdir = CvsEntries(join(root, d))
499                    self.directories[d] = subdir
500                elif entry == 'D':
501                    self.deleted = True 
502
503            # Sometimes the Entries file does not contain the directories:
504            # crawl the current directory looking for missing ones.
505
506            for entry in listdir(root):
507                if entry == '.svn':
508                    continue               
509                dir = join(root, entry)
510                if (isdir(dir) and exists(join(dir, 'CVS/Entries'))
511                    and not self.directories.has_key(entry)):
512                    self.directories[entry] = CvsEntries(dir)
513                   
514            if self.deleted:
515                self.deleted = not self.files and not self.directories
516           
517    def __str__(self):
518        return "CvsEntries(%d files, %d subdirectories)" % (
519            len(self.files), len(self.directories))
520
521    def getFileInfo(self, fpath):
522        """Fetch the info about a path, if known.  Otherwise return None."""
523
524        try:
525            if '/' in fpath:
526                dir,rest = fpath.split('/', 1)
527                return self.directories[dir].getFileInfo(rest)
528            else:
529                return self.files[fpath]
530        except KeyError:
531            return None
532
533    def removedDirectories(self, other, prefix=''):
534        from os.path import join
535       
536        result = []
537        for d in self.directories.keys():
538            a = self.directories.get(d)
539            b = other.directories.get(d)
540            dirpath = join(prefix, d)
541            if not b or b.deleted:
542                result.append(dirpath)
543            else:
544                result.extend(a.removedDirectories(b, prefix=dirpath))
545        return result
546
547    def addedDirectories(self, other, prefix=''):
548        from os.path import join
549       
550        result = []
551        for d in other.directories.keys():
552            a = self.directories.get(d)
553            b = other.directories.get(d)
554            dirpath = join(prefix, d)
555            if not a:
556                result.append(dirpath)
557            else:
558                result.extend(a.addedDirectories(b, prefix=dirpath))
559        return result
560   
561    def compareDirectories(self, other):
562        """Compare the directories with those of another instance and return
563           a tuple (added, removed)."""
564
565        added = self.addedDirectories(other)
566        removed = self.removedDirectories(other)
567
568        return (added, removed)
569
570
Note: See TracBrowser for help on using the repository browser.