source: tailor/vcpx/cvs.py @ 135

Revision 135, 12.6 KB checked in by lele@…, 9 years ago (diff)

Strip the CVS module name, not just the first component

Line 
1#! /usr/bin/python
2# -*- mode: python; coding: utf-8 -*-
3# :Progetto: vcpx -- Pure CVS solution
4# :Creato:   dom 11 lug 2004 01:59:36 CEST
5# :Autore:   Lele Gaifax <lele@nautilus.homeip.net>
6#
7
8"""
9Given `cvsps` shortcomings, this backend uses CVS only.
10"""
11
12__docformat__ = 'reStructuredText'
13
14from shwrap import SystemCommand
15from cvsps import CvspsWorkingDir
16
17
18def compare_cvs_revs(rev1, rev2):
19    """Compare two CVS revision numerically, not alphabetically."""
20
21    if not rev1: rev1 = '0'
22    if not rev2: rev2 = '0'
23
24    r1 = [int(n) for n in rev1.split('.')]
25    r2 = [int(n) for n in rev2.split('.')]
26   
27    return cmp(r1, r2)
28
29
30class CvsLog(SystemCommand):
31    COMMAND = "TZ=UTC cvs -d%(repository)s rlog -N -S %(branch)s %(since)s %(module)s 2>&1"
32       
33    def __call__(self, output=None, dry_run=False, **kwargs):
34        since = kwargs.get('since')
35        if since:
36            kwargs['since'] = "-d'%s UTC<'" % since
37        else:
38            kwargs['since'] = ''
39
40        branch = kwargs.get('branch')
41        if branch:
42            kwargs['branch'] = "-r%s" % branch
43        else:
44            kwargs['branch'] = ''
45           
46        return SystemCommand.__call__(self, output=output,
47                                      dry_run=dry_run, **kwargs)
48
49
50def changesets_from_cvslog(log, module):
51    """
52    Parse CVS log.
53    """
54
55    from datetime import timedelta
56
57    collected = ChangeSetCollector(log, module)
58    collapsed = []
59
60    threshold = timedelta(seconds=180)
61    last = None
62   
63    for cs in collected:
64        if not last:
65            last = cs
66            collapsed.append(cs)
67        else:
68            if last.author == cs.author and \
69               last.log == cs.log and \
70               abs(last.date - cs.date) < threshold:
71                last.entries.extend(cs.entries)
72            else:
73                last = cs
74                collapsed.append(cs)
75
76    return collapsed
77
78       
79class ChangeSetCollector(object):
80    """Collector of the applied change sets."""
81   
82    def __init__(self, log, module):
83        """
84        Initialize a ChangeSetCollector instance.
85
86        Loop over the modified entries and collect their logs.
87        """
88
89        self.changesets = {}
90        """The dictionary mapping (date, author, log) to each entry."""
91       
92        self.__parseCvsLog(log, module)
93       
94    def __iter__(self):
95        keys = self.changesets.keys()
96        keys.sort()
97        return iter([self.changesets[k] for k in keys])
98   
99    def __getGlobalRevision(self, timestamp, author, changelog):
100        """
101        CVS does not have the notion of a repository-wide revision number,
102        since it tracks just single files.
103
104        Here we could "count" the grouped changesets ala `cvsps`,
105        but that's tricky because of branches.  Since right now there
106        is nothing that depends on this being a number, not to mention
107        a *serial* number, simply emit a (hopefully) unique signature...
108        """
109
110        # NB: the getUpstreamChangesets() below depends on this format
111
112        return "%s by %s" % (timestamp, author)
113
114    def __collect(self, timestamp, author, changelog, entry, revision):
115        """Register a change set about an entry."""
116
117        from changes import Changeset
118       
119        key = (timestamp, author, changelog)
120        if self.changesets.has_key(key):
121            return self.changesets[key].addEntry(entry, revision)
122        else:
123            cs = Changeset(self.__getGlobalRevision(timestamp,
124                                                    author,
125                                                    changelog),
126                           timestamp, author, changelog)
127            self.changesets[key] = cs
128            return cs.addEntry(entry, revision)
129
130    def __parseRevision(self, entry, log):
131        """Parse a single revision log, extracting the needed information
132           and register it.
133
134           Return None when there are no more logs to be parsed,
135           otherwise the revision number."""
136
137        from datetime import datetime
138       
139        revision = log.readline()
140        if not revision or not revision.startswith('revision '):
141            return None
142        rev = revision[9:-1]
143
144        infoline = log.readline()
145
146        info = infoline.split(';')
147
148        assert info[0][:6] == 'date: '
149        # 2004-04-19 14:45:42 +0000, the timezone may be missing
150        dateparts = info[0][6:].split(' ') 
151        day = dateparts[0]
152        time = dateparts[1]
153        y,m,d = map(int, day.split(day[4]))
154        hh,mm,ss = map(int, time.split(':'))
155        date = datetime(y,m,d,hh,mm,ss)
156
157        assert info[1].strip()[:8] == 'author: '
158
159        author = info[1].strip()[8:]
160
161        assert info[2].strip()[:7] == 'state: '
162
163        state = info[2].strip()[7:]
164
165        # Fourth element, if present and like "lines +x -y", indicates
166        # this is a change to an existing file. Otherwise its a new
167        # one.
168
169        newentry = not info[3].strip().startswith('lines: ')
170       
171        # The next line may be either the first of the changelog or a
172        # continuation (?) of the preceeding info line with the
173        # "branches"
174
175        l = log.readline()
176        if l.startswith('branches: ') and l.endswith(';\n'):
177            infoline = infoline[:-1] + ';' + l
178            # read the effective first line of log
179            l = log.readline()
180           
181        mesg = []
182        while (l <> '----------------------------\n' and
183               l <> '=============================================================================\n'):
184            mesg.append(l[:-1])
185            l = log.readline()
186
187        if len(mesg)==1 and mesg[0] == '*** empty log message ***':
188            changelog = ''
189        else:
190            changelog = '\n'.join(mesg)
191           
192        return (date, author, changelog, entry, rev, state, newentry)
193   
194    def __parseCvsLog(self, log, module):
195        """Parse a complete CVS log."""
196
197        from os.path import split, join
198
199        currentdir = None
200       
201        while 1:
202            l = log.readline()
203           
204            while l and not l.startswith('RCS file: '):
205                if l.startswith('cvs rlog: Logging '):
206                    currentdir = l[18:-1]
207                    # strip away first component, the name of the product
208                    if '/' in currentdir:
209                        assert currentdir.startswith(module)
210                        currentdir = currentdir[len(module)+1:]
211                    else:
212                        currentdir = ''
213
214                l = log.readline()
215           
216            if not l.startswith('RCS file: '):
217                break
218
219            assert currentdir is not None, "Missed 'cvs rlog: Logging XX' line"
220           
221            entry = join(currentdir, split(l[10:-1])[1][:-2])
222           
223            l = log.readline()
224            while l and l <> '----------------------------\n':
225                l = log.readline()
226               
227            cs = self.__parseRevision(entry, log)
228            while cs:
229                date,author,changelog,e,rev,state,newentry = cs
230
231                last = self.__collect(date, author, changelog, e, rev)
232                if state == 'dead':
233                    last.action_kind = last.DELETED
234                elif newentry:
235                    last.action_kind = last.ADDED
236                else:
237                    last.action_kind = last.UPDATED
238               
239                cs = self.__parseRevision(entry, log)
240       
241
242class CvsWorkingDir(CvspsWorkingDir):
243    """
244    Reimplement the mechanism used to get a *changeset* view of the
245    CVS commits.
246    """
247   
248    def getUpstreamChangesets(self, root, sincerev=None):
249        from os.path import join, exists
250
251        entries = CvsEntries(root)
252       
253        if not sincerev:
254            # We are bootstrapping, trying to collimate the
255            # actual revision on disk with the changesets.
256            # Start from the ancient entry timestamp.
257            ancient = entries.getAncientEntry()
258            since = ancient.timestamp.isoformat(sep=' ')
259        else:
260            # Assume this is from __getGlobalRevision()
261            since,author = sincerev.split(' by ')
262
263        branch = ''
264        fname = join(root, 'CVS', 'Tag')
265        if exists(fname):
266            tag = open(fname).read()
267            if tag.startswith('T'):
268                branch=tag[1:-1]
269
270        repository = open(join(root, 'CVS', 'Root')).read()[:-1]
271        module = open(join(root, 'CVS', 'Repository')).read()[:-1]
272       
273        cvslog = CvsLog(working_dir=root)
274       
275        changesets = []
276        log = cvslog(output=True, since=since, branch=branch,
277                     repository=repository, module=module)
278        for cs in changesets_from_cvslog(log, module):
279            changesets.append(cs)
280
281        return changesets
282   
283
284class CvsEntry(object):
285    """Collect the info about a file in a CVS working dir."""
286   
287    __slots__ = ('filename', 'cvs_version', 'timestamp', 'cvs_tag')
288
289    def __init__(self, entry):
290        """Initialize a CvsEntry."""
291
292        from datetime import datetime
293        from time import strptime
294       
295        dummy, fn, rev, ts, dummy, tag = entry.split('/')
296
297        if ts.startswith('Result of merge+'):
298            ts = ts[16:]
299           
300        self.filename = fn
301        self.cvs_version = rev
302        y,m,d,hh,mm,ss,d1,d2,d3 = strptime(ts, "%a %b %d %H:%M:%S %Y")
303        self.timestamp = datetime(y,m,d,hh,mm,ss)
304        self.cvs_tag = tag
305
306    def __str__(self):
307        return "CvsEntry('%s', '%s', '%s')" % (self.filename,
308                                               self.cvs_version,
309                                               self.cvs_tag)
310
311
312class CvsEntries(object):
313    """Collection of CvsEntry."""
314
315    __slots__ = ('files', 'directories', 'deleted')
316   
317    def __init__(self, root):
318        """Parse CVS/Entries file.
319
320           Walk down the working directory, collecting info from each
321           CVS/Entries found."""
322
323        from os.path import join, exists, isdir
324        from os import listdir
325       
326        self.files = {}
327        """Dict of `CvsEntry`, keyed on each file under revision control."""
328       
329        self.directories = {}
330        """Dict of `CvsEntries`, keyed on subdirectories under revision
331           control."""
332
333        self.deleted = False
334        """Flag to denote that this directory was removed."""
335       
336        entries = join(root, 'CVS', 'Entries')
337        if exists(entries):
338            for entry in open(entries).readlines():
339                entry = entry[:-1]
340
341                if entry.startswith('/'):
342                    e = CvsEntry(entry)
343                    if file and e.filename==file:
344                        return e
345                    else:
346                        self.files[e.filename] = e
347                elif entry.startswith('D/'):
348                    d = entry.split('/')[1]
349                    subdir = CvsEntries(join(root, d))
350                    self.directories[d] = subdir
351                elif entry == 'D':
352                    self.deleted = True 
353
354            # Sometimes the Entries file does not contain the directories:
355            # crawl the current directory looking for missing ones.
356
357            for entry in listdir(root):
358                if entry == '.svn':
359                    continue               
360                dir = join(root, entry)
361                if (isdir(dir) and exists(join(dir, 'CVS', 'Entries'))
362                    and not self.directories.has_key(entry)):
363                    self.directories[entry] = CvsEntries(dir)
364                   
365            if self.deleted:
366                self.deleted = not self.files and not self.directories
367           
368    def __str__(self):
369        return "CvsEntries(%d files, %d subdirectories)" % (
370            len(self.files), len(self.directories))
371
372    def getFileInfo(self, fpath):
373        """Fetch the info about a path, if known.  Otherwise return None."""
374
375        try:
376            if '/' in fpath:
377                dir,rest = fpath.split('/', 1)
378                return self.directories[dir].getFileInfo(rest)
379            else:
380                return self.files[fpath]
381        except KeyError:
382            return None
383
384    def getAncientEntry(self):
385        """Find and return the older entry."""
386       
387        latest = None
388        for e in self.files.values():
389            if not latest:
390                latest = e
391
392            if e.timestamp < latest.timestamp:
393                latest = e
394
395        for d in self.directories.values():
396            e = d.getAncientEntry()
397
398            # skip if there are no entries in the directory
399            if not e:
400                continue
401           
402            if not latest:
403                latest = e
404
405            if e.timestamp < latest.timestamp:
406                latest = e
407
408        return latest
Note: See TracBrowser for help on using the repository browser.