source: tailor/vcpx/cvs.py @ 95

Revision 95, 13.0 KB checked in by lele@…, 9 years ago (diff)

Corrected the date format passed to strptime

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