source: tailor/vcpx/cvsps.py @ 305

Revision 305, 18.0 KB checked in by lele@…, 8 years ago (diff)

Explicitly license everything under GPL

Line 
1#! /usr/bin/python
2# -*- mode: python; coding: utf-8 -*-
3# :Progetto: vcpx -- CVS details
4# :Creato:   mer 16 giu 2004 00:46:12 CEST
5# :Autore:   Lele Gaifax <lele@nautilus.homeip.net>
6# :Licenza:  GNU General Public License
7#
8
9"""
10This module contains supporting classes for CVS. To get a
11cross-repository revision number ala Subversion, the implementation
12uses `cvsps` to fetch the changes from the upstream repository.
13"""
14
15__docformat__ = 'reStructuredText'
16
17from shwrap import SystemCommand, shrepr, ReopenableNamedTemporaryFile
18from source import UpdatableSourceWorkingDir, ChangesetApplicationFailure, \
19     InvocationError
20from target import SyncronizableTargetWorkingDir, TargetInitializationFailure
21
22
23class CvsPsLog(SystemCommand):
24    COMMAND = "cvsps %(update)s-b %(branch)s 2>/dev/null"
25
26    def __call__(self, output=None, dry_run=False, **kwargs):
27        update = kwargs.get('update', '')
28        if update:
29            update = '-u '
30        kwargs['update'] = update
31       
32        return SystemCommand.__call__(self, output=output,
33                                      dry_run=dry_run, **kwargs)
34
35   
36class CvsUpdate(SystemCommand):
37    COMMAND = 'cvs -q %(dry)supdate -d %(revision)s %(entry)s 2>&1'
38   
39    def __call__(self, output=None, dry_run=False, **kwargs):
40        if dry_run:
41            kwargs['dry'] = '-n '
42        else:
43            kwargs['dry'] = ''
44
45        if kwargs['revision'] is None:
46            kwargs['revision'] = ''
47        else:
48            kwargs['revision'] = '-r%s' % kwargs['revision']
49           
50        return SystemCommand.__call__(self, output=output,
51                                      dry_run=False, **kwargs)
52
53
54class CvsCommit(SystemCommand):
55    COMMAND = "cvs -q ci -F %(logfile)s %(entries)s"
56   
57
58class CvsRemove(SystemCommand):
59    COMMAND = "cvs -q remove %(entry)s"
60
61
62class CvsCheckout(SystemCommand):
63    COMMAND = "cvs -q -d%(repository)s checkout %(revision)s -d%(workingdir)s %(module)s"
64
65    def __call__(self, output=None, dry_run=False, **kwargs):
66        revision = kwargs['revision']
67        if revision is None:
68            revision = ''
69        else:
70            # If the revision contains a space, assume it really
71            # specify a branch and a timestamp. If it starts with
72            # a digit, assume it's a timestamp. Otherwise, it must
73            # be a branch name
74            if revision[0] in '0123456789':
75                revision = "-D'%s'" % revision
76            elif ' ' in revision:
77                branch, timestamp =revision.split(' ', 1)
78                revision = "-r%s -D'%s'" % (branch, timestamp)
79            else:
80                revision = '-r%s' % revision
81        kwargs['revision'] = revision
82       
83        return SystemCommand.__call__(self, output=output,
84                                      dry_run=False, **kwargs)
85
86
87def changesets_from_cvsps(log, sincerev=None):
88    """
89    Parse CVSps log.
90    """
91
92    from changes import Changeset, ChangesetEntry
93    from datetime import datetime
94    from cvs import compare_cvs_revs
95   
96    # cvsps output sample:
97    ## ---------------------
98    ## PatchSet 1500
99    ## Date: 2004/05/09 17:54:22
100    ## Author: grubert
101    ## Branch: HEAD
102    ## Tag: (none)
103    ## Log:
104    ## Tell the reason for using mbox (not wrapping long lines).
105    ##
106    ## Members:
107    ##         docutils/writers/latex2e.py:1.78->1.79
108
109    log.seek(0)
110
111    l = None
112    while 1:
113        l = log.readline()
114        if l <> '---------------------\n':
115            break
116
117        l = log.readline()
118        assert l.startswith('PatchSet '), "Parse error: %s"%l
119
120        pset = {}
121        pset['revision'] = l[9:-1].strip()
122        l = log.readline()
123        while not l.startswith('Log:'):
124            field,value = l.split(':',1)
125            pset[field.lower()] = value.strip()
126            l = log.readline()
127
128        msg = []
129        l = log.readline()
130        msg.append(l)
131        l = log.readline()
132        while l <> 'Members: \n':
133            msg.append(l)
134            l = log.readline()
135
136        pset['log'] = ''.join(msg)
137
138        assert l.startswith('Members:'), "Parse error: %s" % l
139
140        pset['entries'] = entries = []
141        l = log.readline()
142        seen = {}
143        while l.startswith('\t'):
144            if not sincerev or (sincerev<int(pset['revision'])):
145                file,revs = l[1:-1].split(':')
146                fromrev,torev = revs.split('->')
147
148                # Due to the fuzzy mechanism, cvsps may group
149                # together two commits on a single entry, thus
150                # giving something like:
151                #
152                #   Normalizer.py:1.12->1.13
153                #   Registry.py:1.22->1.23
154                #   Registry.py:1.21->1.22
155                #   Stopwords.py:1.9->1.10
156                #
157                # Collapse those into a single one.
158
159                e = seen.get(file)
160                if not e:
161                    e = ChangesetEntry(file)
162                    e.old_revision = fromrev
163                    e.new_revision = torev
164                    seen[file] = e
165                    entries.append(e)
166                else:
167                    if compare_cvs_revs(e.old_revision, fromrev)>0:
168                        e.old_revision = fromrev
169
170                    if compare_cvs_revs(e.new_revision, torev)<0:
171                        e.new_revision = torev
172
173                if fromrev=='INITIAL':
174                    e.action_kind = e.ADDED
175                elif "(DEAD)" in torev:
176                    e.action_kind = e.DELETED
177                    e.new_revision = torev[:torev.index('(DEAD)')]
178                else:
179                    e.action_kind = e.UPDATED
180
181            l = log.readline()
182
183        if not sincerev or (sincerev<int(pset['revision'])):
184            cvsdate = pset['date']
185            y,m,d = map(int, cvsdate[:10].split('/'))
186            hh,mm,ss = map(int, cvsdate[11:19].split(':'))
187            timestamp = datetime(y, m, d, hh, mm, ss)
188            pset['date'] = timestamp
189
190            yield Changeset(**pset)
191
192
193class CvspsWorkingDir(UpdatableSourceWorkingDir,
194                      SyncronizableTargetWorkingDir):
195
196    """
197    An instance of this class represent a read/write CVS working directory,
198    so that it can be used both as source of patches, or as a target
199    repository.
200
201    It uses `cvsps` to do the actual fetch of the changesets metadata
202    from the server, so that we can reasonably group together related
203    changes that would otherwise be sparsed, as CVS is file-centric.
204    """
205   
206    ## UpdatableSourceWorkingDir
207   
208    def getUpstreamChangesets(self, root, repository, module, sincerev=None):
209        from os.path import join, exists
210         
211        cvsps = CvsPsLog(working_dir=root)
212       
213        branch="HEAD"
214        fname = join(root, 'CVS', 'Tag')
215        if exists(fname):
216            tag = open(fname).read()
217            if tag.startswith('T'):
218                branch=tag[1:-1]
219
220        if sincerev:
221            sincerev = int(sincerev)
222           
223        changesets = []
224        log = cvsps(output=True, update=True, branch=branch)
225        for cs in changesets_from_cvsps(log, sincerev):
226            changesets.append(cs)
227
228        return changesets
229
230    def __maybeDeleteDirectory(self, root, entrydir, changeset):
231        from os.path import join, exists
232        from os import listdir
233       
234        if not entrydir:
235            return
236
237        try:
238            cache = self.__deletedDirsCache
239        except AttributeError:
240            cache = self.__deletedDirsCache = {}
241
242        if entrydir in cache:
243            return
244       
245        absentrydir = join(root, entrydir)
246        if not exists(absentrydir) or listdir(absentrydir) == ['CVS']:
247            deldir = changeset.addEntry(entrydir, None)
248            deldir.action_kind = deldir.DELETED
249
250        cache[entrydir] = True
251       
252    def _applyChangeset(self, root, changeset, logger=None):
253        from os.path import join, exists, dirname, split
254        from os import makedirs, listdir
255        from cvs import CvsEntries
256        from time import sleep
257       
258        entries = CvsEntries(root)
259       
260        cvsup = CvsUpdate(working_dir=root)
261        for e in changeset.entries:
262            if e.action_kind == e.UPDATED:
263                info = entries.getFileInfo(e.name)
264                if info and info.cvs_version == e.new_revision:
265                    if logger: logger.debug("skipping '%s' since it's already "
266                                            "at revision %s", e.name,
267                                            e.new_revision)
268                    continue
269            elif e.action_kind == e.DELETED:
270                if not exists(join(root, e.name)):
271                    if logger: logger.debug("skipping '%s' since it's already "
272                                            "deleted", e.name)
273                    self.__maybeDeleteDirectory(root, split(e.name)[0],
274                                                changeset)
275                    continue
276            elif e.action_kind == e.ADDED and e.new_revision is None:
277                # This is a new directory entry, there is no need to update it
278                continue
279           
280            cvsup(output=True, entry=shrepr(e.name), revision=e.new_revision)
281           
282            if cvsup.exit_status:
283                if logger: logger.warning("'cvs update' on %s exited "
284                                          "with status %d, retrying once..." %
285                                          (e.name, cvsup.exit_status))
286                sleep(2)
287                cvsup(output=True, entry=e.name, revision=e.new_revision)
288                if cvsup.exit_status:
289                    if logger: logger.warning("'cvs update' on %s exited "
290                                              "with status %d, retrying "
291                                              "one last time..." %
292                                              (e.name, cvsup.exit_status))
293                    sleep(8)
294                    cvsup(output=True, entry=e.name, revision=e.new_revision)
295                   
296            if cvsup.exit_status:
297                raise ChangesetApplicationFailure(
298                    "'cvs update' returned status %s" % cvsup.exit_status)
299           
300            if logger: logger.info("%s updated to %s" % (e.name,
301                                                         e.new_revision))
302           
303            if e.action_kind == e.DELETED:
304                self.__maybeDeleteDirectory(root, split(e.name)[0],
305                                            changeset)
306               
307    def _checkoutUpstreamRevision(self, basedir, repository, module, revision,
308                                  subdir=None, logger=None):
309        """
310        Concretely do the checkout of the upstream sources. Use `revision` as
311        the name of the tag to get, or as a date if it starts with a number.
312
313        Return the effective cvsps revision.
314        """
315
316        from os.path import join, exists
317        from cvs import CvsEntries, compare_cvs_revs
318
319        if not module:
320            raise InvocationError("Must specify a module name")
321
322        wdir = join(basedir, subdir)
323        if not exists(join(wdir, 'CVS')):
324            c = CvsCheckout(working_dir=basedir)
325            c(output=True,
326              repository=repository,
327              module=module,
328              revision=revision,
329              workingdir=shrepr(subdir))
330            if c.exit_status:
331                raise TargetInitializationFailure(
332                    "'cvs checkout' returned status %s" % c.exit_status)
333        else:
334            if logger: logger.info("Using existing %s", wdir)
335           
336        self.__forceTagOnEachEntry(wdir)
337       
338        entries = CvsEntries(wdir)
339       
340        # update cvsps cache, then loop over the changesets and find the
341        # last applied, to find out the actual cvsps revision
342
343        csets = self.getUpstreamChangesets(wdir, repository, module)
344        csets.reverse()
345        found = False
346        for cset in csets:
347            for m in cset.entries:
348                info = entries.getFileInfo(m.name)
349                if info:
350                    actualversion = info.cvs_version
351                    found = compare_cvs_revs(actualversion,m.new_revision)>=0
352                    if not found:
353                        break
354               
355            if found:
356                last = cset
357                break
358
359        if not found:
360            raise TargetInitializationFailure(
361                "Something went wrong: unable to determine the exact upstream "
362                "revision of the checked out tree in '%s'" % wdir)
363        else:
364            if logger: logger.info("working copy up to cvsps revision %s",
365                                   last.revision)
366           
367        return last.revision
368   
369    def _willApplyChangeset(self, root, changeset, applyable):
370        """
371        This gets called just before applying each changeset.
372       
373        Since CVS has no "createdir" event, we have to take care
374        of new directories, creating empty-but-reasonable CVS dirs.
375        """
376
377        if UpdatableSourceWorkingDir._willApplyChangeset(self, root, changeset,
378                                                         applyable):
379            for m in changeset.entries:
380                if m.action_kind == m.ADDED:
381                    self.__createParentCVSDirectories(changeset, root, m.name)
382           
383            return True
384        else:
385            return False
386       
387    def __createParentCVSDirectories(self, changeset, root, entry):
388        """
389        Verify that the hierarchy down to the entry is under CVS.
390
391        If the directory containing the entry does not exists,
392        create it and make it appear as under CVS so that succeding
393        'cvs update' will work.
394        """
395       
396        from os.path import split, join, exists
397        from os import mkdir
398
399        path = split(entry)[0]
400        if path:
401            basedir = join(root, path)
402        else:
403            basedir = root           
404        cvsarea = join(basedir, 'CVS')
405       
406        if path and not exists(cvsarea):
407            parentcvs = self.__createParentCVSDirectories(changeset,
408                                                          root, path)
409
410            assert exists(parentcvs), "Uhm, strange things happen"
411           
412            if not exists(basedir):
413                mkdir(basedir)
414
415            # Create fake CVS area
416            mkdir(cvsarea)
417
418            # Create an empty "Entries" file
419            entries = open(join(cvsarea, 'Entries'), 'w')
420            entries.close()
421
422            reposf = open(join(parentcvs, 'Repository'))
423            rep = reposf.readline()[:-1]
424            reposf.close()
425
426            reposf = open(join(cvsarea, 'Repository'), 'w')
427            reposf.write("%s/%s\n" % (rep, split(basedir)[1]))
428            reposf.close()
429
430            rootf = open(join(parentcvs, 'Root'))
431            root = rootf.readline()
432            rootf.close()
433
434            rootf = open(join(cvsarea, 'Root'), 'w')
435            rootf.write(root)
436            rootf.close()
437
438            # Add the "new" directory to the changeset, so that the
439            # replayer get its name
440
441            entry = changeset.addEntry(path, None)
442            entry.action_kind = entry.ADDED
443           
444        return cvsarea
445   
446    ## SyncronizableTargetWorkingDir
447
448    def _addPathnames(self, root, names):
449        """
450        Add some new filesystem objects.
451        """
452
453        c = SystemCommand(working_dir=root, command="cvs -q add %(names)s")
454        c(names=' '.join([shrepr(n) for n in names]))
455
456    def __forceTagOnEachEntry(self, root):
457        """
458        Massage each CVS/Entries file, locking (ie, tagging) each
459        entry to its current CVS version.
460
461        This is to prevent silly errors such those that could arise
462        after a manual `cvs update` in the working directory.
463        """
464       
465        from os import walk, rename
466        from os.path import join
467
468        for dir, subdirs, files in walk(root):
469            if dir[-3:] == 'CVS':
470                efn = join(dir, 'Entries')
471                f = open(efn)
472                entries = f.readlines()
473                f.close()
474                rename(efn, efn+'.old')
475               
476                newentries = []
477                for e in entries:
478                    if e.startswith('/'):
479                        fields = e.split('/')
480                        fields[-1] = "T%s\n" % fields[2]
481                        newe = '/'.join(fields)
482                        newentries.append(newe)
483                    else:
484                        newentries.append(e)
485
486                f = open(efn, 'w')
487                f.writelines(newentries)
488                f.close()
489   
490    def _getCommitEntries(self, changeset):
491        """
492        Extract the names of the entries for the commit phase.  Since CVS
493        does not have a "rename" operation, this is simulated by a
494        remove+add, and both entries must be committed.
495        """
496
497        entries = SyncronizableTargetWorkingDir._getCommitEntries(self,
498                                                                  changeset)
499        entries.extend([e.old_name for e in changeset.renamedEntries()])
500
501        return entries
502       
503    def _commit(self,root, date, author, remark, changelog=None, entries=None):
504        """
505        Commit the changeset.
506        """
507       
508        rontf = ReopenableNamedTemporaryFile('cvs', 'tailor')
509        log = open(rontf.name, "w")
510        log.write(remark)
511        log.write('\n')
512        if changelog:
513            log.write(changelog)
514            log.write('\n')
515        log.close()
516       
517        c = CvsCommit(working_dir=root)
518
519        if entries:
520            entries = ' '.join([shrepr(e) for e in entries])
521        else:
522            entries = '.'
523         
524        c(entries=entries, logfile=rontf.name)
525       
526    def _removePathnames(self, root, names):
527        """
528        Remove some filesystem objects.
529        """
530
531        c = CvsRemove(working_dir=root)
532        c(entry=' '.join([shrepr(n) for n in names]))
533
534    def _renamePathname(self, root, oldname, newname):
535        """
536        Rename a filesystem object.
537        """
538
539        self._removePathnames(root, [oldname])
540        self._addPathnames(root, [newname])
Note: See TracBrowser for help on using the repository browser.