source: tailor/vcpx/darcs.py @ 551

Revision 551, 17.0 KB checked in by lele@…, 8 years ago (diff)

Fixed the darcs changes parser to handle remove_directory

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, output.read()))
252
253            csets = changesets_from_darcschanges(output)
254            changeset = csets[0]
255            revision = 'date "%s" && author "%s" && exact "%s"' % (
256                changeset.date.strftime("%a %b %d %H:%M:%S UTC %Y"),
257                changeset.author,
258                changeset.revision)
259        else:
260            initial = False
261
262        if self.repository.subdir == '.':
263            # This is currently *very* slow, compared to the darcs get
264            # below!
265            if not exists(join(self.basedir, '_darcs')):
266                if not exists(self.basedir):
267                    mkdir(self.basedir)
268
269                init = ExternalCommand(cwd=self.basedir,
270                                       command=[self.repository.DARCS_CMD,
271                                                "initialize"])
272                init.execute(stdout=PIPE)
273
274                if init.exit_status:
275                    raise TargetInitializationFailure(
276                        "%s returned status %s" % (str(init),
277                                                   init.exit_status))
278
279                cmd = [self.repository.DARCS_CMD, "pull", "--all", "--verbose"]
280                if revision and revision<>'HEAD':
281                    cmd.extend([initial and "--match" or "--tags", revision])
282                dpull = ExternalCommand(cwd=self.basedir, command=cmd)
283                output = dpull.execute(self.repository.repository,
284                                       stdout=PIPE, stderr=STDOUT)
285
286                if dpull.exit_status:
287                    raise TargetInitializationFailure(
288                        "%s returned status %d saying \"%s\"" %
289                        (str(dpull), dpull.exit_status, output.read()))
290        else:
291            # Use much faster 'darcs get'
292            cmd = [self.repository.DARCS_CMD, "get", "--partial", "--verbose"]
293            if revision and revision<>'HEAD':
294                cmd.extend([initial and "--to-patch" or "--tag", revision])
295            dget = ExternalCommand(command=cmd)
296            output = dget.execute(self.repository.repository, self.basedir,
297                                  stdout=PIPE, stderr=STDOUT)
298
299            if dget.exit_status:
300                raise TargetInitializationFailure(
301                    "%s returned status %d saying \"%s\"" %
302                    (str(dget), dget.exit_status, output.read()))
303
304        cmd = [self.repository.DARCS_CMD, "changes", "--last", "1",
305               "--xml-output"]
306        changes = ExternalCommand(cwd=self.basedir, command=cmd)
307        output = changes.execute(stdout=PIPE, stderr=STDOUT)
308
309        if changes.exit_status:
310            raise ChangesetApplicationFailure(
311                "%s returned status %d saying \"%s\"" %
312                (str(changes), changes.exit_status, output.read()))
313
314        last = changesets_from_darcschanges(output)
315
316        return last[0]
317
318
319    ## SyncronizableTargetWorkingDir
320
321    def _addPathnames(self, names):
322        """
323        Add some new filesystems objects.
324        """
325
326        cmd = [self.repository.DARCS_CMD, "add", "--case-ok",
327               "--not-recursive", "--quiet"]
328        ExternalCommand(cwd=self.basedir, command=cmd).execute(names)
329
330    def _addSubtree(self, subdir):
331        """
332        Use the --recursive variant of ``darcs add`` to add a subtree.
333        """
334
335        cmd = [self.repository.DARCS_CMD, "add", "--case-ok", "--recursive",
336               "--quiet"]
337        ExternalCommand(cwd=self.basedir, command=cmd).execute(subdir)
338
339    def _commit(self, date, author, patchname, changelog=None, entries=None):
340        """
341        Commit the changeset.
342        """
343
344        from sys import getdefaultencoding
345
346        encoding = ExternalCommand.FORCE_ENCODING or getdefaultencoding()
347
348        logmessage = []
349
350        logmessage.append(date.strftime('%Y/%m/%d %H:%M:%S UTC'))
351        logmessage.append(author.encode(encoding))
352        logmessage.append(patchname and patchname.encode(encoding) or 'Unnamed patch')
353        logmessage.append(changelog and changelog.encode(encoding) or '')
354        logmessage.append('')
355
356        cmd = [self.repository.DARCS_CMD, "record", "--all", "--pipe"]
357        if not entries:
358            entries = ['.']
359
360        record = ExternalCommand(cwd=self.basedir, command=cmd)
361        record.execute(entries, input='\n'.join(logmessage), stdout=PIPE)
362
363        if record.exit_status:
364            raise ChangesetApplicationFailure(
365                "%s returned status %d" % (str(record), record.exit_status))
366
367    def _removePathnames(self, names):
368        """
369        Remove some filesystem object.
370        """
371
372        # Since the source VCS already deleted the entry, and given that
373        # darcs will do the right thing with it, do nothing here, instead
374        # of
375        #         c = ExternalCommand(cwd=self.basedir,
376        #                             command=[self.repository.DARCS_CMD,
377        #                                      "remove"])
378        #         c.execute(entries)
379        # that raises status 512 on darcs not finding the entry.
380
381        pass
382
383    def _renamePathname(self, oldname, newname):
384        """
385        Rename a filesystem object.
386        """
387
388        from os.path import join, exists
389        from os import rename
390
391        # Check to see if the oldentry is still there. If it does,
392        # that probably means one thing: it's been moved and then
393        # replaced, see svn 'R' event. In this case, rename the
394        # existing old entry to something else to trick "darcs mv"
395        # (that will assume the move was already done manually) and
396        # finally restore its name.
397
398        renamed = exists(join(self.basedir, oldname))
399        if renamed:
400            rename(oldname, oldname + '-TAILOR-HACKED-TEMP-NAME')
401
402        try:
403            cmd = [self.repository.DARCS_CMD, "mv"]
404            ExternalCommand(cwd=self.basedir, command=cmd).execute(oldname,
405                                                                   newname)
406        finally:
407            if renamed:
408                rename(oldname + '-TAILOR-HACKED-TEMP-NAME', oldname)
409
410    def _prepareTargetRepository(self, source_repo):
411        """
412        Execute ``darcs initialize``.
413        """
414
415        from os import makedirs
416        from os.path import exists
417
418        if not exists(self.basedir):
419            makedirs(self.basedir)
420
421        init = ExternalCommand(cwd=self.basedir,
422                               command=[self.repository.DARCS_CMD,
423                                        "initialize"])
424        init.execute(stdout=PIPE)
425
426        if init.exit_status:
427            raise TargetInitializationFailure(
428                "%s returned status %s" % (str(init), init.exit_status))
429
430    def _prepareWorkingDirectory(self, source_repo):
431        """
432        Tweak the default settings of the repository.
433        """
434
435        from os.path import join
436        from re import escape
437        from dualwd import IGNORED_METADIRS
438
439        motd = open(join(self.basedir, '_darcs/prefs/motd'), 'w')
440        motd.write(MOTD % str(source_repo))
441        motd.close()
442
443        # Remove .cvsignore from default boring file
444        boring = open(join(self.basedir, '_darcs/prefs/boring'), 'r')
445        ignored = [line for line in boring if line <> '\.cvsignore$\n']
446        boring.close()
447
448        # Augment the boring file, that contains a regexp per line
449        # with all known VCs metadirs to be skipped.
450        boring = open(join(self.basedir, '_darcs/prefs/boring'), 'w')
451        boring.write(''.join(ignored))
452        boring.write('\n'.join(['(^|/)%s($|/)' % escape(md)
453                                for md in IGNORED_METADIRS]))
454        boring.write('\n')
455        if self.logfile.startswith(self.basedir):
456            boring.write('^')
457            boring.write(self.logfile[len(self.basedir)+1:])
458            boring.write('$\n')
459        if self.state_file.filename.startswith(self.basedir):
460            boring.write('^')
461            boring.write(self.state_file.filename[len(self.basedir)+1:])
462            boring.write('$\n')
463        boring.close()
Note: See TracBrowser for help on using the repository browser.