source: tailor/vcpx/darcs.py @ 526

Revision 526, 16.7 KB checked in by lele@…, 8 years ago (diff)

Put the patch name in the changelog too

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, root, repository, module, sincerev=None):
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=root, command=cmd)
146        output = pull.execute(repository, 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, root, changeset, logger=None):
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=root, 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=root, 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, basedir, repository, module, revision,
232                                  subdir=None, logger=None, **kwargs):
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", 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        wdir = join(basedir, subdir)
264        if subdir == '.':
265            # This is currently *very* slow, compared to the darcs get
266            # below!
267            if not exists(join(wdir, '_darcs')):
268                if not exists(wdir):
269                    mkdir(wdir)
270
271                init = ExternalCommand(cwd=wdir,
272                                       command=[self.repository.DARCS_CMD,
273                                                "initialize"])
274                init.execute(stdout=PIPE)
275
276                if init.exit_status:
277                    raise TargetInitializationFailure(
278                        "%s returned status %s" % (str(init),
279                                                   init.exit_status))
280
281                cmd = [self.repository.DARCS_CMD, "pull", "--all", "--verbose"]
282                if revision and revision<>'HEAD':
283                    cmd.extend([initial and "--patches" or "--tags", revision])
284                dpull = ExternalCommand(cwd=wdir, command=cmd)
285                output = dpull.execute(repository, 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(cwd=basedir, command=cmd)
297            output = dget.execute(repository, subdir,
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=wdir, 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, root, 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=root, command=cmd).execute(names)
330
331    def _addSubtree(self, root, 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=root, command=cmd).execute(subdir)
339
340    def _commit(self, root, 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=root, 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, root, 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=root,
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, root, 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(root, 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=root, command=cmd).execute(oldname, newname)
406        finally:
407            if renamed:
408                rename(oldname + '-TAILOR-HACKED-TEMP-NAME', oldname)
409
410    def _initializeWorkingDir(self, root, source_repository, source_module,
411                              subdir):
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=root, command=[self.repository.DARCS_CMD,
422                                                  "initialize"])
423        init.execute(stdout=PIPE)
424
425        if init.exit_status:
426            raise TargetInitializationFailure(
427                "%s returned status %s" % (str(init), init.exit_status))
428
429        motd = open(join(root, '_darcs/prefs/motd'), 'w')
430        motd.write(MOTD % (source_repository, source_module))
431        motd.close()
432
433        # Remove .cvsignore from default boring file
434        boring = open(join(root, '_darcs/prefs/boring'), 'r')
435        ignored = [line for line in boring if line <> '\.cvsignore$\n']
436        boring.close()
437
438        # Augment the boring file, that contains a regexp per line
439        # with all known VCs metadirs to be skipped.
440        boring = open(join(root, '_darcs/prefs/boring'), 'w')
441        boring.write(''.join(ignored))
442        boring.write('\n'.join(['(^|/)%s($|/)' % escape(md)
443                                for md in IGNORED_METADIRS]))
444        boring.write('\n^tailor.log$\n^tailor.info$\n')
445        boring.close()
446
447        SyncronizableTargetWorkingDir._initializeWorkingDir(self, root,
448                                                            source_repository,
449                                                            source_module,
450                                                            subdir)
Note: See TracBrowser for help on using the repository browser.