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

Line 
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>
5# :Licenza:  GNU General Public License
6#
7
8"""
9This module contains supporting classes for the ``darcs`` versioning system.
10"""
11
12__docformat__ = 'reStructuredText'
13
14from shwrap import ExternalCommand, PIPE, STDOUT
15from source import UpdatableSourceWorkingDir, ChangesetApplicationFailure, \
16     GetUpstreamChangesetsFailure
17from target import SyncronizableTargetWorkingDir, TargetInitializationFailure
18from xml.sax import SAXException
19
20MOTD = """\
21This is the Darcs equivalent of
22%s
23"""
24
25def changesets_from_darcschanges(changes, unidiff=False, repodir=None):
26    """
27    Parse XML output of ``darcs changes``.
28
29    Return a list of ``Changeset`` instances.
30    """
31
32    from xml.sax import parse
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 = []
42            if unidiff and repodir:
43                cmd = ["darcs", "diff", "--unified", "--repodir", repodir,
44                       "--patch", "%(patchname)s"]
45                self.darcsdiff = ExternalCommand(command=cmd)
46            else:
47                self.darcsdiff = None
48
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'] = []
65            elif name in ['name', 'comment', 'add_file', 'add_directory',
66                          'modify_file', 'remove_file', 'remove_directory']:
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
75                self.current['entries'].sort(lambda x,y: cmp(x.name, y.name))
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,
83                                 self.current['date'],
84                                 self.current['author'],
85                                 changelog,
86                                 self.current['entries'])
87
88                if self.darcsdiff:
89                    cset.unidiff = self.darcsdiff.execute(
90                        stdout=PIPE, patchname=cset.revision).read()
91
92                self.changesets.append(cset)
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)
101            elif name in ['add_file', 'add_directory', 'modify_file',
102                          'remove_file', 'remove_directory']:
103                entry = ChangesetEntry(''.join(self.current_field).strip())
104                entry.action_kind = { 'add_file': entry.ADDED,
105                                      'add_directory': entry.ADDED,
106                                      'modify_file': entry.UPDATED,
107                                      'remove_file': entry.DELETED,
108                                      'remove_directory': entry.DELETED
109                                    }[name]
110
111                self.current['entries'].append(entry)
112
113        def characters(self, data):
114            self.current_field.append(data)
115
116
117    handler = DarcsXMLChangesHandler()
118    parse(changes, handler)
119    changesets = handler.changesets
120
121    # sort changeset by date
122    changesets.sort(lambda x, y: cmp(x.date, y.date))
123
124    return changesets
125
126
127class DarcsWorkingDir(UpdatableSourceWorkingDir,SyncronizableTargetWorkingDir):
128    """
129    A working directory under ``darcs``.
130    """
131
132    ## UpdatableSourceWorkingDir
133
134    def _getUpstreamChangesets(self, sincerev):
135        """
136        Do the actual work of fetching the upstream changeset.
137        """
138
139        from datetime import datetime
140        from time import strptime
141        from changes import Changeset
142
143        cmd = [self.repository.DARCS_CMD, "pull", "--dry-run"]
144        pull = ExternalCommand(cwd=self.basedir, command=cmd)
145        output = pull.execute(self.repository.repository,
146                              stdout=PIPE, stderr=STDOUT, TZ='UTC')
147
148        if pull.exit_status:
149            raise GetUpstreamChangesetsFailure(
150                "%s returned status %d saying \"%s\"" %
151                (str(pull), pull.exit_status, output.read()))
152
153        l = output.readline()
154        while l and not (l.startswith('Would pull the following changes:') or
155                         l == 'No remote changes to pull in!\n'):
156            l = output.readline()
157
158        changesets = []
159
160        if l <> 'No remote changes to pull in!\n':
161            ## Sat Jul 17 01:22:08 CEST 2004  lele@nautilus
162            ##   * Refix _getUpstreamChangesets for darcs
163
164            l = output.readline()
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)
175                l = output.readline()
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]
184
185                changelog = []
186                l = output.readline()
187                while l.startswith(' '):
188                    changelog.append(l.strip())
189                    l = output.readline()
190
191                changesets.append(Changeset(name, date, author, '\n'.join(changelog)))
192
193                while not l.strip():
194                    l = output.readline()
195
196        return changesets
197
198    def _applyChangeset(self, changeset):
199        """
200        Do the actual work of applying the changeset to the working copy.
201        """
202
203        from re import escape
204
205        if changeset.revision.startswith('tagged '):
206            selector = '--tags'
207            revtag = changeset.revision[7:]
208        else:
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)
214
215        cmd = [self.repository.DARCS_CMD, "pull", "--all", selector, revtag]
216        pull = ExternalCommand(cwd=self.basedir, command=cmd)
217        output = pull.execute(stdout=PIPE, stderr=STDOUT)
218
219        if pull.exit_status:
220            raise ChangesetApplicationFailure(
221                "%s returned status %d saying \"%s\"" %
222                (str(pull), pull.exit_status, output.read()))
223
224        cmd = [self.repository.DARCS_CMD, "changes", selector, revtag,
225               "--xml-output", "--summ"]
226        changes = ExternalCommand(cwd=self.basedir, command=cmd)
227        last = changesets_from_darcschanges(changes.execute(stdout=PIPE))
228        if last:
229            changeset.entries.extend(last[0].entries)
230
231    def _checkoutUpstreamRevision(self, revision):
232        """
233        Concretely do the checkout of the upstream revision and return
234        the last applied changeset.
235        """
236
237        from os.path import join, exists
238        from os import mkdir
239        from re import escape
240
241        if revision == 'INITIAL':
242            initial = True
243            cmd = [self.repository.DARCS_CMD, "changes", "--xml-output",
244                   "--repo", self.repository.repository]
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\"" %
251                    (str(changes), changes.exit_status,
252                     output and output.read() or ''))
253
254            csets = changesets_from_darcschanges(output)
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)
260        else:
261            initial = False
262
263        if self.repository.subdir == '.':
264            # This is currently *very* slow, compared to the darcs get
265            # below!
266            if not exists(join(self.basedir, '_darcs')):
267                if not exists(self.basedir):
268                    mkdir(self.basedir)
269
270                init = ExternalCommand(cwd=self.basedir,
271                                       command=[self.repository.DARCS_CMD,
272                                                "initialize"])
273                init.execute()
274
275                if init.exit_status:
276                    raise TargetInitializationFailure(
277                        "%s returned status %s" % (str(init),
278                                                   init.exit_status))
279
280                cmd = [self.repository.DARCS_CMD, "pull", "--all", "--verbose"]
281                if revision and revision<>'HEAD':
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)
286
287                if dpull.exit_status:
288                    raise TargetInitializationFailure(
289                        "%s returned status %d saying \"%s\"" %
290                        (str(dpull), dpull.exit_status, output.read()))
291        else:
292            # Use much faster 'darcs get'
293            cmd = [self.repository.DARCS_CMD, "get", "--partial", "--verbose"]
294            if revision and revision<>'HEAD':
295                cmd.extend([initial and "--to-patch" or "--tag", revision])
296            dget = ExternalCommand(command=cmd)
297            output = dget.execute(self.repository.repository, self.basedir,
298                                  stdout=PIPE, stderr=STDOUT)
299
300            if dget.exit_status:
301                raise TargetInitializationFailure(
302                    "%s returned status %d saying \"%s\"" %
303                    (str(dget), dget.exit_status, output.read()))
304
305        cmd = [self.repository.DARCS_CMD, "changes", "--last", "1",
306               "--xml-output"]
307        changes = ExternalCommand(cwd=self.basedir, command=cmd)
308        output = changes.execute(stdout=PIPE, stderr=STDOUT)
309
310        if changes.exit_status:
311            raise ChangesetApplicationFailure(
312                "%s returned status %d saying \"%s\"" %
313                (str(changes), changes.exit_status, output.read()))
314
315        last = changesets_from_darcschanges(output)
316
317        return last[0]
318
319
320    ## SyncronizableTargetWorkingDir
321
322    def _addPathnames(self, names):
323        """
324        Add some new filesystems objects.
325        """
326
327        cmd = [self.repository.DARCS_CMD, "add", "--case-ok",
328               "--not-recursive", "--quiet"]
329        ExternalCommand(cwd=self.basedir, command=cmd).execute(names)
330
331    def _addSubtree(self, subdir):
332        """
333        Use the --recursive variant of ``darcs add`` to add a subtree.
334        """
335
336        cmd = [self.repository.DARCS_CMD, "add", "--case-ok", "--recursive",
337               "--quiet"]
338        ExternalCommand(cwd=self.basedir, command=cmd).execute(subdir)
339
340    def _commit(self, date, author, patchname, changelog=None, entries=None):
341        """
342        Commit the changeset.
343        """
344
345        from sys import getdefaultencoding
346
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))
353        logmessage.append(patchname and patchname.encode(encoding) or 'Unnamed patch')
354        logmessage.append(changelog and changelog.encode(encoding) or '')
355        logmessage.append('')
356
357        cmd = [self.repository.DARCS_CMD, "record", "--all", "--pipe"]
358        if not entries:
359            entries = ['.']
360
361        record = ExternalCommand(cwd=self.basedir, command=cmd)
362        record.execute(entries, input='\n'.join(logmessage))
363
364        if record.exit_status:
365            raise ChangesetApplicationFailure(
366                "%s returned status %d" % (str(record), record.exit_status))
367
368    def _removePathnames(self, names):
369        """
370        Remove some filesystem object.
371        """
372
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
375        # of
376        #         c = ExternalCommand(cwd=self.basedir,
377        #                             command=[self.repository.DARCS_CMD,
378        #                                      "remove"])
379        #         c.execute(entries)
380        # that raises status 512 on darcs not finding the entry.
381
382        pass
383
384    def _renamePathname(self, oldname, newname):
385        """
386        Rename a filesystem object.
387        """
388
389        from os.path import join, exists
390        from os import rename
391
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
399        renamed = exists(join(self.basedir, oldname))
400        if renamed:
401            rename(oldname, oldname + '-TAILOR-HACKED-TEMP-NAME')
402
403        try:
404            cmd = [self.repository.DARCS_CMD, "mv"]
405            ExternalCommand(cwd=self.basedir, command=cmd).execute(oldname,
406                                                                   newname)
407        finally:
408            if renamed:
409                rename(oldname + '-TAILOR-HACKED-TEMP-NAME', oldname)
410
411    def _prepareTargetRepository(self, source_repo):
412        """
413        Execute ``darcs initialize``.
414        """
415
416        from os import makedirs
417        from os.path import join, exists
418
419        if not exists(self.basedir):
420            makedirs(self.basedir)
421        elif exists(join(self.basedir, self.repository.METADIR)):
422            return
423
424        init = ExternalCommand(cwd=self.basedir,
425                               command=[self.repository.DARCS_CMD,
426                                        "initialize"])
427        init.execute()
428
429        if init.exit_status:
430            raise TargetInitializationFailure(
431                "%s returned status %s" % (str(init), init.exit_status))
432
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
442        motd = open(join(self.basedir, '_darcs/prefs/motd'), 'w')
443        motd.write(MOTD % str(source_repo))
444        motd.close()
445
446        # Remove .cvsignore from default boring file
447        boring = open(join(self.basedir, '_darcs/prefs/boring'), 'r')
448        ignored = [line for line in boring if line <> '\.cvsignore$\n']
449        boring.close()
450
451        # Augment the boring file, that contains a regexp per line
452        # with all known VCs metadirs to be skipped.
453        boring = open(join(self.basedir, '_darcs/prefs/boring'), 'w')
454        boring.write(''.join(ignored))
455        boring.write('\n'.join(['(^|/)%s($|/)' % escape(md)
456                                for md in IGNORED_METADIRS]))
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')
466        boring.close()
Note: See TracBrowser for help on using the repository browser.