source: tailor/vcpx/cvs.py @ 179

Revision 179, 14.2 KB checked in by lele@…, 8 years ago (diff)

Better error message when trying to bootstrap empty CVS repository

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    """
19    This is the exception raised when we try to tailor an empty CVS
20    repository. This is more a shortcoming of tailor, rather than a
21    real problem with those repositories.
22    """
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        for cs in changesets_from_cvslog(log, module):
312            for e in cs.entries:
313                # If the entry is not already there, and, for whatever
314                # reason (most probably, manually tweaked CVS
315                # repository), from the log we desumed it's an update,
316                # consider it as a NEW entry instead.
317               
318                if (e.action_kind == e.UPDATED and
319                    entries.getFileInfo(e.name) is None):
320                    e.action_kind = e.ADDED
321                   
322            changesets.append(cs)
323
324        return changesets
325   
326
327class CvsEntry(object):
328    """Collect the info about a file in a CVS working dir."""
329   
330    __slots__ = ('filename', 'cvs_version', 'timestamp', 'cvs_tag')
331
332    def __init__(self, entry):
333        """Initialize a CvsEntry."""
334
335        from datetime import datetime
336        from time import strptime
337       
338        dummy, fn, rev, ts, dummy, tag = entry.split('/')
339
340        if ts.startswith('Result of merge+'):
341            ts = ts[16:]
342           
343        self.filename = fn
344        self.cvs_version = rev
345        y,m,d,hh,mm,ss,d1,d2,d3 = strptime(ts, "%a %b %d %H:%M:%S %Y")
346        self.timestamp = datetime(y,m,d,hh,mm,ss)
347        self.cvs_tag = tag
348
349    def __str__(self):
350        return "CvsEntry('%s', '%s', '%s')" % (self.filename,
351                                               self.cvs_version,
352                                               self.cvs_tag)
353
354
355class CvsEntries(object):
356    """Collection of CvsEntry."""
357
358    __slots__ = ('files', 'directories', 'deleted')
359   
360    def __init__(self, root):
361        """Parse CVS/Entries file.
362
363           Walk down the working directory, collecting info from each
364           CVS/Entries found."""
365
366        from os.path import join, exists, isdir
367        from os import listdir
368       
369        self.files = {}
370        """Dict of `CvsEntry`, keyed on each file under revision control."""
371       
372        self.directories = {}
373        """Dict of `CvsEntries`, keyed on subdirectories under revision
374           control."""
375
376        self.deleted = False
377        """Flag to denote that this directory was removed."""
378       
379        entries = join(root, 'CVS', 'Entries')
380        if exists(entries):
381            for entry in open(entries).readlines():
382                entry = entry[:-1]
383
384                if entry.startswith('/'):
385                    e = CvsEntry(entry)
386                    if file and e.filename==file:
387                        return e
388                    else:
389                        self.files[e.filename] = e
390                elif entry.startswith('D/'):
391                    d = entry.split('/')[1]
392                    subdir = CvsEntries(join(root, d))
393                    self.directories[d] = subdir
394                elif entry == 'D':
395                    self.deleted = True 
396
397            # Sometimes the Entries file does not contain the directories:
398            # crawl the current directory looking for missing ones.
399
400            for entry in listdir(root):
401                if entry == '.svn':
402                    continue               
403                dir = join(root, entry)
404                if (isdir(dir) and exists(join(dir, 'CVS', 'Entries'))
405                    and not self.directories.has_key(entry)):
406                    self.directories[entry] = CvsEntries(dir)
407                   
408            if self.deleted:
409                self.deleted = not self.files and not self.directories
410           
411    def __str__(self):
412        return "CvsEntries(%d files, %d subdirectories)" % (
413            len(self.files), len(self.directories))
414
415    def getFileInfo(self, fpath):
416        """Fetch the info about a path, if known.  Otherwise return None."""
417
418        try:
419            if '/' in fpath:
420                dir,rest = fpath.split('/', 1)
421                return self.directories[dir].getFileInfo(rest)
422            else:
423                return self.files[fpath]
424        except KeyError:
425            return None
426
427    def getYoungestEntry(self):
428        """Find and return the most recently changed entry."""
429       
430        latest = None
431       
432        for e in self.files.values():
433            if not latest or e.timestamp > latest.timestamp:
434                latest = e
435
436        for d in self.directories.values():
437            e = d.getYoungestEntry()
438
439            # skip if there are no entries in the directory
440            if not e:
441                continue
442           
443            if not latest or e.timestamp > latest.timestamp:
444                latest = e
445
446        return latest
Note: See TracBrowser for help on using the repository browser.