source: tailor/vcpx/darcs.py @ 523

Revision 523, 16.5 KB checked in by lele@…, 8 years ago (diff)

Be much more exact in pulling a single darcs changeset
Instead of relaying on "--patches", use a "--match" expression that
includes date, author and patch name.

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