source: tailor/vcpx/cvs.py @ 54

Revision 54, 17.4 KB checked in by lele@…, 9 years ago (diff)

Do not execute 'cvs update' against the same revision as the current one

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