source: tailor/vcpx/darcs.py @ 527

Revision 527, 16.6 KB checked in by lele@…, 8 years ago (diff)

Big API change, reducing arguments in favour of instance attributes
This is a big and subtle change that brings nothing in term of
functionality but make it a lot easier maintaining and extending
tailor as a whole.

Basically, 'root' and 'subdir' arguments are gone replaced by a
self.basedir, computed from the configuration; instead of 'logger',
the code uses two new methods, log_info() and log_error() on most
objects. Other arguments are derived from the configuration objects
that hang around.

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                name = self.current['name']
78                log = self.current['comment']
79                if log:
80                    changelog = name + '\n' + log
81                else:
82                    changelog = name
83                cset = Changeset(name,
84                                 self.current['date'],
85                                 self.current['author'],
86                                 changelog,
87                                 self.current['entries'])
88
89                if self.darcsdiff:
90                    cset.unidiff = self.darcsdiff.execute(
91                        stdout=PIPE, patchname=cset.revision).read()
92
93                self.changesets.append(cset)
94                self.current = None
95            elif name in ['name', 'comment']:
96                self.current[name] = ''.join(self.current_field)
97            elif name == 'move':
98                entry = ChangesetEntry(self.new_name)
99                entry.action_kind = entry.RENAMED
100                entry.old_name = self.old_name
101                self.current['entries'].append(entry)
102            elif name in ['add_file', 'add_directory',
103                          'modify_file', 'remove_file']:
104                entry = ChangesetEntry(''.join(self.current_field).strip())
105                entry.action_kind = { 'add_file': entry.ADDED,
106                                      'add_directory': entry.ADDED,
107                                      'modify_file': entry.UPDATED,
108                                      'remove_file': entry.DELETED,
109                                      'rename_file': entry.RENAMED
110                                    }[name]
111
112                self.current['entries'].append(entry)
113
114        def characters(self, data):
115            self.current_field.append(data)
116
117
118    handler = DarcsXMLChangesHandler()
119    parse(changes, handler)
120    changesets = handler.changesets
121
122    # sort changeset by date
123    changesets.sort(lambda x, y: cmp(x.date, y.date))
124
125    return changesets
126
127
128class DarcsWorkingDir(UpdatableSourceWorkingDir,SyncronizableTargetWorkingDir):
129    """
130    A working directory under ``darcs``.
131    """
132
133    ## UpdatableSourceWorkingDir
134
135    def _getUpstreamChangesets(self, sincerev):
136        """
137        Do the actual work of fetching the upstream changeset.
138        """
139
140        from datetime import datetime
141        from time import strptime
142        from changes import Changeset
143
144        cmd = [self.repository.DARCS_CMD, "pull", "--dry-run"]
145        pull = ExternalCommand(cwd=self.basedir, command=cmd)
146        output = pull.execute(self.repository.repository,
147                              stdout=PIPE, stderr=STDOUT, TZ='UTC')
148
149        if pull.exit_status:
150            raise GetUpstreamChangesetsFailure(
151                "%s returned status %d saying \"%s\"" %
152                (str(pull), pull.exit_status, output.read()))
153
154        l = output.readline()
155        while l and not (l.startswith('Would pull the following changes:') or
156                         l == 'No remote changes to pull in!\n'):
157            l = output.readline()
158
159        changesets = []
160
161        if l <> 'No remote changes to pull in!\n':
162            ## Sat Jul 17 01:22:08 CEST 2004  lele@nautilus
163            ##   * Refix _getUpstreamChangesets for darcs
164
165            l = output.readline()
166            while not l.startswith('Making no changes:  this is a dry run.'):
167                # Assume it's a line like
168                #    Sun Jan  2 00:24:04 UTC 2005  lele@nautilus.homeip.net
169                # we used to split on the double space before the email,
170                # but in this case this is wrong. Waiting for xml output,
171                # is it really sane asserting date's length to 28 chars?
172                date = l[:28]
173                author = l[30:-1]
174                y,m,d,hh,mm,ss,d1,d2,d3 = strptime(date, "%a %b %d %H:%M:%S %Z %Y")
175                date = datetime(y,m,d,hh,mm,ss)
176                l = output.readline()
177                assert (l.startswith('  * ') or
178                        l.startswith('  UNDO:') or
179                        l.startswith('  tagged'))
180
181                if l.startswith('  *'):
182                    name = l[4:-1]
183                else:
184                    name = l[2:-1]
185
186                changelog = []
187                l = output.readline()
188                while l.startswith(' '):
189                    changelog.append(l.strip())
190                    l = output.readline()
191
192                changesets.append(Changeset(name, date, author, '\n'.join(changelog)))
193
194                while not l.strip():
195                    l = output.readline()
196
197        return changesets
198
199    def _applyChangeset(self, changeset):
200        """
201        Do the actual work of applying the changeset to the working copy.
202        """
203
204        from re import escape
205
206        if changeset.revision.startswith('tagged '):
207            selector = '--tags'
208            revtag = changeset.revision[7:]
209        else:
210            selector = '--match'
211            revtag = 'date "%s" && author "%s" && exact "%s"' % (
212                changeset.date.strftime("%a %b %d %H:%M:%S UTC %Y"),
213                changeset.author,
214                changeset.revision)
215
216        cmd = [self.repository.DARCS_CMD, "pull", "--all", selector, revtag]
217        pull = ExternalCommand(cwd=self.basedir, command=cmd)
218        output = pull.execute(stdout=PIPE, stderr=STDOUT)
219
220        if pull.exit_status:
221            raise ChangesetApplicationFailure(
222                "%s returned status %d saying \"%s\"" %
223                (str(pull), pull.exit_status, output.read()))
224
225        cmd = [self.repository.DARCS_CMD, "changes", selector, revtag,
226               "--xml-output", "--summ"]
227        changes = ExternalCommand(cwd=self.basedir, command=cmd)
228        last = changesets_from_darcschanges(changes.execute(stdout=PIPE))
229        if last:
230            changeset.entries.extend(last[0].entries)
231
232    def _checkoutUpstreamRevision(self, revision):
233        """
234        Concretely do the checkout of the upstream revision and return
235        the last applied changeset.
236        """
237
238        from os.path import join, exists
239        from os import mkdir
240        from re import escape
241
242        if revision == 'INITIAL':
243            initial = True
244            cmd = [self.repository.DARCS_CMD, "changes", "--xml-output",
245                   "--repo", self.repository.repository]
246            changes = ExternalCommand(command=cmd)
247            output = changes.execute(stdout=PIPE, stderr=STDOUT)
248
249            if changes.exit_status:
250                raise ChangesetApplicationFailure(
251                    "%s returned status %d saying \"%s\"" %
252                    (str(changes), changes.exit_status, output.read()))
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(stdout=PIPE)
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), stdout=PIPE)
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 _initializeWorkingDir(self):
412        """
413        Execute ``darcs initialize`` and tweak the default settings of
414        the repository, then add the whole subtree.
415        """
416
417        from os.path import join
418        from re import escape
419        from dualwd import IGNORED_METADIRS
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        motd = open(join(self.basedir, '_darcs/prefs/motd'), 'w')
431        motd.write(MOTD % (source_repository, source_module))
432        motd.close()
433
434        # Remove .cvsignore from default boring file
435        boring = open(join(self.basedir, '_darcs/prefs/boring'), 'r')
436        ignored = [line for line in boring if line <> '\.cvsignore$\n']
437        boring.close()
438
439        # Augment the boring file, that contains a regexp per line
440        # with all known VCs metadirs to be skipped.
441        boring = open(join(self.basedir, '_darcs/prefs/boring'), 'w')
442        boring.write(''.join(ignored))
443        boring.write('\n'.join(['(^|/)%s($|/)' % escape(md)
444                                for md in IGNORED_METADIRS]))
445        boring.write('\n^tailor.log$\n^tailor.info$\n')
446        boring.close()
447
448        SyncronizableTargetWorkingDir._initializeWorkingDir(self)
Note: See TracBrowser for help on using the repository browser.