source: tailor/vcpx/cvsps.py @ 166

Revision 166, 16.3 KB checked in by lele@…, 9 years ago (diff)

Fix the handling of new directories from upstream

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