source: tailor/vcpx/cvs.py @ 260

Revision 260, 14.5 KB checked in by lele@…, 8 years ago (diff)

Resolved a conflict

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