source: tailor/vcpx/cvs.py @ 205

Revision 205, 14.4 KB checked in by lele@…, 8 years ago (diff)

Replaced a tab with spaces

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