source: tailor/vcpx/cvs.py @ 85

Revision 85, 12.3 KB checked in by lele@…, 9 years ago (diff)

Some re-wording, tag the CVS changesets using only the timestamp

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