source: tailor/vcpx/darcs.py @ 577

Revision 577, 17.1 KB checked in by lele@…, 8 years ago (diff)

Dont fail badly when darcs changes does not return any output

RevLine 
[12]1# -*- mode: python; coding: utf-8 -*-
2# :Progetto: vcpx -- Darcs details
3# :Creato:   ven 18 giu 2004 14:45:28 CEST
4# :Autore:   Lele Gaifax <lele@nautilus.homeip.net>
[305]5# :Licenza:  GNU General Public License
[448]6#
[12]7
[18]8"""
[311]9This module contains supporting classes for the ``darcs`` versioning system.
[18]10"""
11
[12]12__docformat__ = 'reStructuredText'
13
[425]14from shwrap import ExternalCommand, PIPE, STDOUT
[207]15from source import UpdatableSourceWorkingDir, ChangesetApplicationFailure, \
16     GetUpstreamChangesetsFailure
[51]17from target import SyncronizableTargetWorkingDir, TargetInitializationFailure
[353]18from xml.sax import SAXException
[393]19
[192]20MOTD = """\
21This is the Darcs equivalent of
[534]22%s
[192]23"""
[12]24
[355]25def changesets_from_darcschanges(changes, unidiff=False, repodir=None):
[159]26    """
27    Parse XML output of ``darcs changes``.
28
[311]29    Return a list of ``Changeset`` instances.
[159]30    """
[448]31
[393]32    from xml.sax import parse
[103]33    from xml.sax.handler import ContentHandler
34    from changes import ChangesetEntry, Changeset
35    from datetime import datetime
36
37    class DarcsXMLChangesHandler(ContentHandler):
38        def __init__(self):
39            self.changesets = []
40            self.current = None
41            self.current_field = []
[355]42            if unidiff and repodir:
[393]43                cmd = ["darcs", "diff", "--unified", "--repodir", repodir,
44                       "--patch", "%(patchname)s"]
45                self.darcsdiff = ExternalCommand(command=cmd)
[355]46            else:
47                self.darcsdiff = None
[448]48
[103]49        def startElement(self, name, attributes):
50            if name == 'patch':
51                self.current = {}
52                self.current['author'] = attributes['author']
53                date = attributes['date']
54                # 20040619130027
55                y = int(date[:4])
56                m = int(date[4:6])
57                d = int(date[6:8])
58                hh = int(date[8:10])
59                mm = int(date[10:12])
60                ss = int(date[12:14])
61                timestamp = datetime(y, m, d, hh, mm, ss)
62                self.current['date'] = timestamp
63                self.current['comment'] = ''
64                self.current['entries'] = []
[551]65            elif name in ['name', 'comment', 'add_file', 'add_directory',
66                          'modify_file', 'remove_file', 'remove_directory']:
[103]67                self.current_field = []
68            elif name == 'move':
69                self.old_name = attributes['from']
70                self.new_name = attributes['to']
71
72        def endElement(self, name):
73            if name == 'patch':
74                # Sort the paths to make tests easier
[141]75                self.current['entries'].sort(lambda x,y: cmp(x.name, y.name))
[526]76                name = self.current['name']
77                log = self.current['comment']
78                if log:
79                    changelog = name + '\n' + log
80                else:
81                    changelog = name
82                cset = Changeset(name,
[355]83                                 self.current['date'],
84                                 self.current['author'],
[526]85                                 changelog,
[355]86                                 self.current['entries'])
[448]87
[355]88                if self.darcsdiff:
[393]89                    cset.unidiff = self.darcsdiff.execute(
90                        stdout=PIPE, patchname=cset.revision).read()
[448]91
[355]92                self.changesets.append(cset)
[103]93                self.current = None
94            elif name in ['name', 'comment']:
95                self.current[name] = ''.join(self.current_field)
96            elif name == 'move':
97                entry = ChangesetEntry(self.new_name)
98                entry.action_kind = entry.RENAMED
99                entry.old_name = self.old_name
100                self.current['entries'].append(entry)
[551]101            elif name in ['add_file', 'add_directory', 'modify_file',
102                          'remove_file', 'remove_directory']:
[208]103                entry = ChangesetEntry(''.join(self.current_field).strip())
[103]104                entry.action_kind = { 'add_file': entry.ADDED,
105                                      'add_directory': entry.ADDED,
106                                      'modify_file': entry.UPDATED,
107                                      'remove_file': entry.DELETED,
[551]108                                      'remove_directory': entry.DELETED
[103]109                                    }[name]
110
111                self.current['entries'].append(entry)
112
113        def characters(self, data):
114            self.current_field.append(data)
115
116
[393]117    handler = DarcsXMLChangesHandler()
118    parse(changes, handler)
[103]119    changesets = handler.changesets
[448]120
[103]121    # sort changeset by date
122    changesets.sort(lambda x, y: cmp(x.date, y.date))
123
124    return changesets
125
[448]126
[12]127class DarcsWorkingDir(UpdatableSourceWorkingDir,SyncronizableTargetWorkingDir):
[14]128    """
129    A working directory under ``darcs``.
130    """
[59]131
[12]132    ## UpdatableSourceWorkingDir
[448]133
[527]134    def _getUpstreamChangesets(self, sincerev):
[12]135        """
136        Do the actual work of fetching the upstream changeset.
[110]137        """
138
139        from datetime import datetime
140        from time import strptime
141        from changes import Changeset
[448]142
[515]143        cmd = [self.repository.DARCS_CMD, "pull", "--dry-run"]
[527]144        pull = ExternalCommand(cwd=self.basedir, command=cmd)
145        output = pull.execute(self.repository.repository,
146                              stdout=PIPE, stderr=STDOUT, TZ='UTC')
[448]147
[393]148        if pull.exit_status:
149            raise GetUpstreamChangesetsFailure(
150                "%s returned status %d saying \"%s\"" %
[425]151                (str(pull), pull.exit_status, output.read()))
[393]152
[110]153        l = output.readline()
[210]154        while l and not (l.startswith('Would pull the following changes:') or
155                         l == 'No remote changes to pull in!\n'):
[110]156            l = output.readline()
157
158        changesets = []
[12]159
[210]160        if l <> 'No remote changes to pull in!\n':
161            ## Sat Jul 17 01:22:08 CEST 2004  lele@nautilus
[467]162            ##   * Refix _getUpstreamChangesets for darcs
[210]163
[110]164            l = output.readline()
[210]165            while not l.startswith('Making no changes:  this is a dry run.'):
166                # Assume it's a line like
167                #    Sun Jan  2 00:24:04 UTC 2005  lele@nautilus.homeip.net
168                # we used to split on the double space before the email,
169                # but in this case this is wrong. Waiting for xml output,
170                # is it really sane asserting date's length to 28 chars?
171                date = l[:28]
172                author = l[30:-1]
173                y,m,d,hh,mm,ss,d1,d2,d3 = strptime(date, "%a %b %d %H:%M:%S %Z %Y")
174                date = datetime(y,m,d,hh,mm,ss)
[110]175                l = output.readline()
[279]176                assert (l.startswith('  * ') or
177                        l.startswith('  UNDO:') or
178                        l.startswith('  tagged'))
179
180                if l.startswith('  *'):
181                    name = l[4:-1]
182                else:
183                    name = l[2:-1]
[21]184
[210]185                changelog = []
[110]186                l = output.readline()
[210]187                while l.startswith(' '):
188                    changelog.append(l.strip())
189                    l = output.readline()
190
[234]191                changesets.append(Changeset(name, date, author, '\n'.join(changelog)))
[210]192
193                while not l.strip():
194                    l = output.readline()
[21]195
196        return changesets
[448]197
[527]198    def _applyChangeset(self, changeset):
[12]199        """
200        Do the actual work of applying the changeset to the working copy.
201        """
202
[431]203        from re import escape
[448]204
[394]205        if changeset.revision.startswith('tagged '):
206            selector = '--tags'
207            revtag = changeset.revision[7:]
208        else:
[523]209            selector = '--match'
210            revtag = 'date "%s" && author "%s" && exact "%s"' % (
211                changeset.date.strftime("%a %b %d %H:%M:%S UTC %Y"),
212                changeset.author,
213                changeset.revision)
[448]214
[515]215        cmd = [self.repository.DARCS_CMD, "pull", "--all", selector, revtag]
[527]216        pull = ExternalCommand(cwd=self.basedir, command=cmd)
[425]217        output = pull.execute(stdout=PIPE, stderr=STDOUT)
[448]218
[393]219        if pull.exit_status:
220            raise ChangesetApplicationFailure(
221                "%s returned status %d saying \"%s\"" %
[425]222                (str(pull), pull.exit_status, output.read()))
[100]223
[515]224        cmd = [self.repository.DARCS_CMD, "changes", selector, revtag,
[393]225               "--xml-output", "--summ"]
[527]226        changes = ExternalCommand(cwd=self.basedir, command=cmd)
[393]227        last = changesets_from_darcschanges(changes.execute(stdout=PIPE))
[284]228        if last:
229            changeset.entries.extend(last[0].entries)
[110]230
[527]231    def _checkoutUpstreamRevision(self, revision):
[100]232        """
[426]233        Concretely do the checkout of the upstream revision and return
234        the last applied changeset.
[100]235        """
236
[201]237        from os.path import join, exists
[203]238        from os import mkdir
[431]239        from re import escape
[448]240
[431]241        if revision == 'INITIAL':
242            initial = True
[515]243            cmd = [self.repository.DARCS_CMD, "changes", "--xml-output",
[527]244                   "--repo", self.repository.repository]
[431]245            changes = ExternalCommand(command=cmd)
246            output = changes.execute(stdout=PIPE, stderr=STDOUT)
247
248            if changes.exit_status:
249                raise ChangesetApplicationFailure(
250                    "%s returned status %d saying \"%s\"" %
[577]251                    (str(changes), changes.exit_status,
252                     output and output.read() or ''))
[203]253
[431]254            csets = changesets_from_darcschanges(output)
[523]255            changeset = csets[0]
256            revision = 'date "%s" && author "%s" && exact "%s"' % (
257                changeset.date.strftime("%a %b %d %H:%M:%S UTC %Y"),
258                changeset.author,
259                changeset.revision)
[431]260        else:
261            initial = False
262
[527]263        if self.repository.subdir == '.':
[300]264            # This is currently *very* slow, compared to the darcs get
265            # below!
[527]266            if not exists(join(self.basedir, '_darcs')):
267                if not exists(self.basedir):
268                    mkdir(self.basedir)
[203]269
[527]270                init = ExternalCommand(cwd=self.basedir,
[515]271                                       command=[self.repository.DARCS_CMD,
272                                                "initialize"])
[571]273                init.execute()
[300]274
[393]275                if init.exit_status:
[300]276                    raise TargetInitializationFailure(
[393]277                        "%s returned status %s" % (str(init),
278                                                   init.exit_status))
[300]279
[515]280                cmd = [self.repository.DARCS_CMD, "pull", "--all", "--verbose"]
[393]281                if revision and revision<>'HEAD':
[527]282                    cmd.extend([initial and "--match" or "--tags", revision])
283                dpull = ExternalCommand(cwd=self.basedir, command=cmd)
284                output = dpull.execute(self.repository.repository,
285                                       stdout=PIPE, stderr=STDOUT)
[448]286
[300]287                if dpull.exit_status:
288                    raise TargetInitializationFailure(
[393]289                        "%s returned status %d saying \"%s\"" %
[425]290                        (str(dpull), dpull.exit_status, output.read()))
[300]291        else:
292            # Use much faster 'darcs get'
[515]293            cmd = [self.repository.DARCS_CMD, "get", "--partial", "--verbose"]
[393]294            if revision and revision<>'HEAD':
[431]295                cmd.extend([initial and "--to-patch" or "--tag", revision])
[527]296            dget = ExternalCommand(command=cmd)
297            output = dget.execute(self.repository.repository, self.basedir,
[425]298                                  stdout=PIPE, stderr=STDOUT)
[448]299
[300]300            if dget.exit_status:
[100]301                raise TargetInitializationFailure(
[393]302                    "%s returned status %d saying \"%s\"" %
[425]303                    (str(dget), dget.exit_status, output.read()))
[100]304
[515]305        cmd = [self.repository.DARCS_CMD, "changes", "--last", "1",
306               "--xml-output"]
[527]307        changes = ExternalCommand(cwd=self.basedir, command=cmd)
[425]308        output = changes.execute(stdout=PIPE, stderr=STDOUT)
[448]309
[393]310        if changes.exit_status:
311            raise ChangesetApplicationFailure(
312                "%s returned status %d saying \"%s\"" %
[425]313                (str(changes), changes.exit_status, output.read()))
[448]314
[195]315        last = changesets_from_darcschanges(output)
[448]316
[426]317        return last[0]
[100]318
[448]319
[12]320    ## SyncronizableTargetWorkingDir
[448]321
[527]322    def _addPathnames(self, names):
[12]323        """
[291]324        Add some new filesystems objects.
[12]325        """
[55]326
[515]327        cmd = [self.repository.DARCS_CMD, "add", "--case-ok",
328               "--not-recursive", "--quiet"]
[527]329        ExternalCommand(cwd=self.basedir, command=cmd).execute(names)
[448]330
[527]331    def _addSubtree(self, subdir):
[294]332        """
[311]333        Use the --recursive variant of ``darcs add`` to add a subtree.
[294]334        """
[393]335
[515]336        cmd = [self.repository.DARCS_CMD, "add", "--case-ok", "--recursive",
337               "--quiet"]
[527]338        ExternalCommand(cwd=self.basedir, command=cmd).execute(subdir)
[448]339
[527]340    def _commit(self, date, author, patchname, changelog=None, entries=None):
[12]341        """
342        Commit the changeset.
343        """
[46]344
[433]345        from sys import getdefaultencoding
[448]346
[433]347        encoding = ExternalCommand.FORCE_ENCODING or getdefaultencoding()
348
349        logmessage = []
350
351        logmessage.append(date.strftime('%Y/%m/%d %H:%M:%S UTC'))
352        logmessage.append(author.encode(encoding))
[513]353        logmessage.append(patchname and patchname.encode(encoding) or 'Unnamed patch')
[433]354        logmessage.append(changelog and changelog.encode(encoding) or '')
355        logmessage.append('')
356
[515]357        cmd = [self.repository.DARCS_CMD, "record", "--all", "--pipe"]
[393]358        if not entries:
359            entries = ['.']
[448]360
[527]361        record = ExternalCommand(cwd=self.basedir, command=cmd)
[571]362        record.execute(entries, input='\n'.join(logmessage))
[448]363
[393]364        if record.exit_status:
365            raise ChangesetApplicationFailure(
366                "%s returned status %d" % (str(record), record.exit_status))
[448]367
[527]368    def _removePathnames(self, names):
[12]369        """
[291]370        Remove some filesystem object.
[12]371        """
372
[224]373        # Since the source VCS already deleted the entry, and given that
374        # darcs will do the right thing with it, do nothing here, instead
[448]375        # of
[527]376        #         c = ExternalCommand(cwd=self.basedir,
[515]377        #                             command=[self.repository.DARCS_CMD,
378        #                                      "remove"])
[393]379        #         c.execute(entries)
[224]380        # that raises status 512 on darcs not finding the entry.
[12]381
[224]382        pass
[448]383
[527]384    def _renamePathname(self, oldname, newname):
[12]385        """
[291]386        Rename a filesystem object.
[12]387        """
388
[239]389        from os.path import join, exists
390        from os import rename
[448]391
[239]392        # Check to see if the oldentry is still there. If it does,
393        # that probably means one thing: it's been moved and then
394        # replaced, see svn 'R' event. In this case, rename the
395        # existing old entry to something else to trick "darcs mv"
396        # (that will assume the move was already done manually) and
397        # finally restore its name.
398
[527]399        renamed = exists(join(self.basedir, oldname))
[239]400        if renamed:
[291]401            rename(oldname, oldname + '-TAILOR-HACKED-TEMP-NAME')
[239]402
403        try:
[515]404            cmd = [self.repository.DARCS_CMD, "mv"]
[527]405            ExternalCommand(cwd=self.basedir, command=cmd).execute(oldname,
406                                                                   newname)
[239]407        finally:
408            if renamed:
[291]409                rename(oldname + '-TAILOR-HACKED-TEMP-NAME', oldname)
[12]410
[534]411    def _prepareTargetRepository(self, source_repo):
[19]412        """
[534]413        Execute ``darcs initialize``.
[19]414        """
[192]415
[534]416        from os import makedirs
[559]417        from os.path import join, exists
[534]418
419        if not exists(self.basedir):
420            makedirs(self.basedir)
[559]421        elif exists(join(self.basedir, self.repository.METADIR)):
422            return
[428]423
[527]424        init = ExternalCommand(cwd=self.basedir,
425                               command=[self.repository.DARCS_CMD,
426                                        "initialize"])
[571]427        init.execute()
[19]428
[393]429        if init.exit_status:
[51]430            raise TargetInitializationFailure(
[393]431                "%s returned status %s" % (str(init), init.exit_status))
[192]432
[534]433    def _prepareWorkingDirectory(self, source_repo):
434        """
435        Tweak the default settings of the repository.
436        """
437
438        from os.path import join
439        from re import escape
440        from dualwd import IGNORED_METADIRS
441
[527]442        motd = open(join(self.basedir, '_darcs/prefs/motd'), 'w')
[534]443        motd.write(MOTD % str(source_repo))
[192]444        motd.close()
445
[386]446        # Remove .cvsignore from default boring file
[527]447        boring = open(join(self.basedir, '_darcs/prefs/boring'), 'r')
[386]448        ignored = [line for line in boring if line <> '\.cvsignore$\n']
449        boring.close()
[448]450
[384]451        # Augment the boring file, that contains a regexp per line
452        # with all known VCs metadirs to be skipped.
[527]453        boring = open(join(self.basedir, '_darcs/prefs/boring'), 'w')
[386]454        boring.write(''.join(ignored))
[384]455        boring.write('\n'.join(['(^|/)%s($|/)' % escape(md)
456                                for md in IGNORED_METADIRS]))
[543]457        boring.write('\n')
458        if self.logfile.startswith(self.basedir):
459            boring.write('^')
460            boring.write(self.logfile[len(self.basedir)+1:])
461            boring.write('$\n')
462        if self.state_file.filename.startswith(self.basedir):
463            boring.write('^')
464            boring.write(self.state_file.filename[len(self.basedir)+1:])
465            boring.write('$\n')
[192]466        boring.close()
Note: See TracBrowser for help on using the repository browser.