source: tailor/vcpx/darcs.py @ 467

Revision 467, 15.8 KB checked in by lele@…, 8 years ago (diff)

Renamed method, since it's one of those that subclasses should reimplement

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