source: tailor/vcpx/cvsps.py @ 560

Revision 560, 17.9 KB checked in by lele@…, 8 years ago (diff)

Moved the check against empty repository at the checkout step

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