source: tailor/vcpx/repository/darcs/source.py @ 1663

Revision 1663, 29.1 KB checked in by lele@…, 4 years ago (diff)

Remove darcs 2.1+ "Ignore-this: ..." noise from the changelog

Line 
1# -*- mode: python; coding: utf-8 -*-
2# :Progetto: Tailor -- Darcs peculiarities when used as a source
3# :Creato:   lun 10 lug 2006 00:04:59 CEST
4# :Autore:   Lele Gaifax <lele@nautilus.homeip.net>
5# :Licenza:  GNU General Public License
6#
7
8"""
9This module contains the source specific bits of the darcs backend.
10"""
11
12__docformat__ = 'reStructuredText'
13
14import re
15
16from vcpx.changes import ChangesetEntry, Changeset
17from vcpx.shwrap import ExternalCommand, PIPE, STDOUT
18from vcpx.source import UpdatableSourceWorkingDir, ChangesetApplicationFailure, \
19                        GetUpstreamChangesetsFailure
20from vcpx.target import TargetInitializationFailure
21from vcpx.tzinfo import UTC
22
23
24class DarcsChangeset(Changeset):
25    """
26    Fixup darcs idiosyncrasies:
27
28    - collapse "add A; rename A B" into "add B"
29    - collapse "rename A B; remove B" into "remove A"
30    """
31
32    def __init__(self, revision, date, author, log, entries=None, **other):
33        """
34        Initialize a new DarcsChangeset.
35        """
36
37        super(DarcsChangeset, self).__init__(revision, date, author, log, entries=None, **other)
38        self.darcs_hash = other.get('darcs_hash')
39        if entries is not None:
40            for e in entries:
41                self.addEntry(e, revision)
42
43    def __eq__(self, other):
44        equal = (self.revision == other.revision and
45                 self.date == other.date and
46                 self.author == other.author)
47        if equal:
48            other_hash = getattr(other, 'darcs_hash', None)
49            if (self.darcs_hash is not None and
50                other_hash is not None and
51                self.darcs_hash != other_hash):
52                equal = False
53        return equal
54
55    def __ne__(self, other):
56        different = (self.revision <> other.revision or
57                     self.date <> other.date or
58                     self.author <> other.author)
59        if not different:
60            other_hash = getattr(other, 'darcs_hash', None)
61            if (self.darcs_hash is not None and
62                other_hash is not None and
63                self.darcs_hash != other_hash):
64                different = True
65        return different
66
67    # Match darcs 2.1+ junk: hopefully this regex is strict enough to
68    # not obliterate useful info...
69    ignore_this = re.compile('^Ignore-this: [a-f\\d]+\\n?')
70
71    def setLog(self, log):
72        """Strip away the "Ignore-this:" noise from the changelog."""
73
74        super(DarcsChangeset, self).setLog(self.ignore_this.sub('', log))
75
76    def addEntry(self, entry, revision):
77        """
78        Fixup darcs idiosyncrasies:
79
80        - collapse "add A; rename A B" into "add B"
81        - collapse "rename A B; add B" into "add B"
82        - annihilate "add A; remove A"
83        - collapse "rename A B; remove B" into "remove A"
84        - collapse "rename A B; rename B C" into "rename A C"
85        - collapse "add A; edit A" into "add A"
86        """
87
88        # This should not happen, since the parser feeds us an already built
89        # list of ChangesetEntries, anyway...
90        if not isinstance(entry, ChangesetEntry):
91            return super(DarcsChangeset, self).addEntry(entry, revision)
92
93        # Ok, before adding this entry, check it against already
94        # known: if this is an add, and there's a rename (such as "add
95        # A; rename A B; ") then...
96
97        if entry.action_kind == entry.ADDED:
98            # ... we have to check existings, because of a bug in
99            # darcs: `changes --xml` (as of 1.0.7) emits the changes
100            # in the wrong order, that is, it prefers to start with
101            # renames, *always*, even when they obviously follows the
102            # add of the same entry (even, it should apply this "fix"
103            # by its own).
104            #
105            # So, if there's a rename of this entry there, change that
106            # to an addition instead, and don't insert any other entry
107
108            # darcs hopefully use forward slashes also under win
109            dirname = entry.name+'/'
110
111            for i,e in enumerate(self.entries):
112                if e.action_kind == e.RENAMED:
113                    if e.old_name == entry.name:
114                        # Unfortunately we have to check if the order if
115                        # messed up, in that case we should not do anything.
116                        # Example: mv a a2; mkdir a; mv a2 a/b
117                        skip = False
118                        for j in self.entries:
119                            if j.action_kind == j.RENAMED and j.name.startswith(dirname):
120                                skip = True
121                                break
122                        # Luckily enough (since removes are the first entries
123                        # in the list, that is) by anticipating the add we
124                        # cure also the case below, when addition follows
125                        # edit.
126                        if not skip:
127                            e.action_kind = e.ADDED
128                            e.old_name = None
129                            e.is_directory = entry.is_directory
130
131                            # Collapse "add A; edit A" into "add A"
132                            for j,oe in enumerate(self.entries):
133                                if oe.action_kind == e.UPDATED and e.name == oe.name:
134                                    del self.entries[j]
135
136                            return e
137
138                    # The "rename A B; add B" into "add B"
139                    if e.name == entry.name:
140                        del self.entries[i]
141
142                # Assert also that add_dir events must preceeds any
143                # add_file and ren_file that have that dir as target,
144                # and that add_file preceeds any edit.
145
146                if ((e.name == entry.name or e.name.startswith(dirname))
147                    or (e.action_kind == e.RENAMED and e.old_name.startswith(dirname))):
148                    self.entries.insert(i, entry)
149                    return entry
150
151        # Likewise, if this is a deletion, and there is a rename of
152        # this entry (such as "rename A B; remove B") then turn the
153        # existing rename into a deletion instead.
154
155        # If instead the removed entry was added by the same patch,
156        # annihilate the two: a bug in darcs (possibly fixed in recent
157        # versions) created patches with ADD+EDIT+REMOVE of a single
158        # file (see tailor ticket #71, or darcs issue185). Too bad
159        # another bug (still present in 1.0.8) hides that and makes
160        # very hard (read: impossible) any workaround on the tailor
161        # side. Luckily I learnt another tiny bit of Haskell and
162        # proposed a fix for that: hopefully the patch will be
163        # accepted by darcs developers. In the meantime, I attached it
164        # to ticket #71: without that, tailor does not have enough
165        # information to do the right thing.
166
167        elif entry.action_kind == entry.DELETED:
168            for i,e in enumerate(self.entries):
169                if e.action_kind == e.RENAMED and e.name == entry.name:
170                    e.action_kind = e.DELETED
171                    e.name = e.old_name
172                    e.old_name = None
173                    e.is_directory = entry.is_directory
174                    return e
175                elif e.action_kind == e.ADDED and e.name == entry.name:
176                    del self.entries[i]
177                    return None
178                elif e.action_kind == e.DELETED and e.is_directory and \
179                         entry.name.startswith(e.name+'/'):
180                    self.entries.insert(i, entry)
181                    return e
182
183        # The "rename A B; rename B C" to "rename A C" part
184        elif entry.action_kind == entry.RENAMED:
185            # Adjust previous renames
186            olddirname = entry.old_name+'/'
187            for e in self.entries:
188                if e.action_kind == e.RENAMED and e.name.startswith(olddirname):
189                    e.name = entry.name + '/' + e.name[len(olddirname):]
190
191            for e in self.entries:
192                if e.action_kind == e.RENAMED and e.name == entry.old_name:
193                    e.name = entry.name
194                    return e
195
196            # The "rename A B; add B" into "add B", part two
197            for i,e in enumerate(self.entries):
198                if e.action_kind == e.ADDED and e.name == entry.name:
199                    return None
200
201        # Ok, it must be either an edit or a rename: the former goes
202        # obviously to the end, and since the latter, as said, come
203        # in very early, appending is just good.
204
205        self.entries.append(entry)
206        return entry
207
208
209def changesets_from_darcschanges(changes, unidiff=False, repodir=None,
210                                 chunksize=2**15, replace_badchars=None):
211    """
212    Parse XML output of ``darcs changes``.
213
214    Return a list of ``Changeset`` instances.
215
216    Filters out the (currently incorrect) tag info from
217    changesets_from_darcschanges_unsafe.
218    """
219
220    csets = changesets_from_darcschanges_unsafe(changes, unidiff,
221                                                repodir, chunksize,
222                                                replace_badchars)
223    for cs in csets:
224        yield cs
225
226def changesets_from_darcschanges_unsafe(changes, unidiff=False, repodir=None,
227                                        chunksize=2**15, replace_badchars=None):
228    """
229    Do the real work of parsing the change log, including tags.
230    Warning: the tag information in the changsets returned by this
231    function are only correct if each darcs tag in the repo depends on
232    all of the patches that precede it.  This is not a valid
233    assumption in general--a tag that does not depend on patch P can
234    be pulled in from another darcs repo after P.  We collect the tag
235    info anyway because DarcsWorkingDir._currentTags() can use it
236    safely despite this problem.  Hopefully the problem will
237    eventually be fixed and this function can be renamed
238    changesets_from_darcschanges.
239    """
240    from xml.sax import make_parser
241    from xml.sax.handler import ContentHandler, ErrorHandler
242    from datetime import datetime
243
244    class DarcsXMLChangesHandler(ContentHandler):
245        def __init__(self):
246            self.changesets = []
247            self.current = None
248            self.current_field = []
249            if unidiff and repodir:
250                cmd = ["darcs", "diff", "--unified", "--repodir", repodir,
251                       "--patch", "%(patchname)s"]
252                self.darcsdiff = ExternalCommand(command=cmd)
253            else:
254                self.darcsdiff = None
255
256        def startElement(self, name, attributes):
257            if name == 'patch':
258                self.current = {}
259                self.current['author'] = attributes['author']
260                date = attributes['date']
261                from time import strptime
262                try:
263                    # 20040619130027
264                    timestamp = datetime(*strptime(date, '%Y%m%d%H%M%S')[:6])
265                except ValueError:
266                    # Old darcs patches use the form Sun Oct 20 20:01:05 EDT 2002
267                    timestamp = datetime(*strptime(date[:19] + date[-5:], '%a %b %d %H:%M:%S %Y')[:6])
268
269                timestamp = timestamp.replace(tzinfo=UTC) # not true for the ValueError case, but oh well
270
271                self.current['date'] = timestamp
272                self.current['comment'] = ''
273                self.current['hash'] = attributes['hash']
274                self.current['entries'] = []
275                self.inverted = (attributes['inverted'] == "True")
276            elif name in ['name', 'comment', 'add_file', 'add_directory',
277                          'modify_file', 'remove_file', 'remove_directory']:
278                self.current_field = []
279            elif name == 'move':
280                self.old_name = attributes['from']
281                self.new_name = attributes['to']
282
283        def endElement(self, name):
284            if name == 'patch':
285                cset = DarcsChangeset(self.current['name'],
286                                      self.current['date'],
287                                      self.current['author'],
288                                      self.current['comment'],
289                                      self.current['entries'],
290                                      tags=self.current.get('tags',[]),
291                                      darcs_hash=self.current['hash'])
292                if self.darcsdiff:
293                    cset.unidiff = self.darcsdiff.execute(TZ='UTC',
294                        stdout=PIPE, patchname=cset.revision)[0].read()
295
296                self.changesets.append(cset)
297                self.current = None
298            elif name in ['name', 'comment']:
299                val = ''.join(self.current_field)
300                if val[:4] == 'TAG ':
301                    self.current.setdefault('tags',[]).append(val[4:])
302                self.current[name] = val
303            elif name == 'move':
304                entry = ChangesetEntry(self.new_name)
305                entry.action_kind = entry.RENAMED
306                entry.old_name = self.old_name
307                self.current['entries'].append(entry)
308            elif name in ['add_file', 'add_directory', 'modify_file',
309                          'remove_file', 'remove_directory']:
310                current_field = ''.join(self.current_field).strip()
311                if self.inverted:
312                    # the filenames in file modifications are outdated
313                    # if there are renames
314                    for i in self.current['entries']:
315                        if i.action_kind == i.RENAMED and current_field.startswith(i.old_name):
316                            current_field = current_field.replace(i.old_name, i.name)
317                entry = ChangesetEntry(current_field)
318                entry.action_kind = { 'add_file': entry.ADDED,
319                                      'add_directory': entry.ADDED,
320                                      'modify_file': entry.UPDATED,
321                                      'remove_file': entry.DELETED,
322                                      'remove_directory': entry.DELETED
323                                    }[name]
324                entry.is_directory = name.endswith('directory')
325                self.current['entries'].append(entry)
326
327        def characters(self, data):
328            self.current_field.append(data)
329
330    parser = make_parser()
331    handler = DarcsXMLChangesHandler()
332    parser.setContentHandler(handler)
333    parser.setErrorHandler(ErrorHandler())
334
335    def fixup_badchars(s, map):
336        if not map:
337            return s
338
339        ret = [map.get(c, c) for c in s]
340        return "".join(ret)
341
342    chunk = fixup_badchars(changes.read(chunksize), replace_badchars)
343    while chunk:
344        parser.feed(chunk)
345        for cs in handler.changesets:
346            yield cs
347        handler.changesets = []
348        chunk = fixup_badchars(changes.read(chunksize), replace_badchars)
349    parser.close()
350    for cs in handler.changesets:
351        yield cs
352
353
354class DarcsSourceWorkingDir(UpdatableSourceWorkingDir):
355    """
356    A source working directory under ``darcs``.
357    """
358
359    is_hash_rx = re.compile('[0-9a-f]{14}-[0-9a-f]{5}-[0-9a-f]{40}\.gz')
360
361    def _getUpstreamChangesets(self, sincerev):
362        """
363        Do the actual work of fetching the upstream changeset.
364        """
365
366        # Use the newer pull --xml-output, if possible
367        use_xml = False
368        if self.repository.darcs_version.startswith('2'):
369            cmd = self.repository.command("pull", "--dry-run", "--xml-output")
370            pull = ExternalCommand(cwd=self.repository.basedir, command=cmd)
371            output,error = pull.execute(self.repository.repository,
372                                        stdout=PIPE, stderr=PIPE, TZ='UTC0')
373            # pull --xml-output was introduced *after* 2.0.0
374            if pull.exit_status:
375                errormsg = error.read()
376                if "unrecognized option `--xml-output'" in errormsg:
377                    self.log.warning('Using darcs 1.0 non-XML parser: it may fail '
378                                     'on patches recorded before november 2003! '
379                                     'I would suggest of upgrading to latest darcs 2.0 '
380                                     '(later than 2.0+233).')
381                    # No way, fall back to old behaviour, that will possibly fail,
382                    # on patches recorded before 2003-11-01... :-|
383                else:
384                    raise GetUpstreamChangesetsFailure(
385                        "%s returned status %d saying\n%s" %
386                        (str(pull), pull.exit_status, errormsg))
387            else:
388                use_xml = True
389
390        if not use_xml:
391            cmd = self.repository.command("pull", "--dry-run")
392            pull = ExternalCommand(cwd=self.repository.basedir, command=cmd)
393            output = pull.execute(self.repository.repository,
394                                  stdout=PIPE, stderr=STDOUT, TZ='UTC0')[0]
395
396            if pull.exit_status:
397                raise GetUpstreamChangesetsFailure(
398                    "%s returned status %d saying\n%s" %
399                    (str(pull), pull.exit_status, output.read()))
400
401            return self._parseDarcsPull(output)
402        else:
403            from cStringIO import StringIO
404
405            # My initial implementation of --xml-output on darcs pull
406            # wasn't perfect, as it was printing useless verbosity before
407            # and after the actual xml. Around 2.0.0+275 I removed that...
408
409            line = output.readline() # Would pull from "/home/lele/wip/darcs-2.0"...
410            if line.startswith('Would pull from '):
411                # Skip the first two lines and drop the last two as well
412                output.readline() # Would pull the following changes:
413                xml = StringIO(''.join(output.readlines()[:-2]))
414                xml.seek(0)
415            else:
416                output.seek(0)
417                xml = output
418
419            badchars = self.repository.replace_badchars
420
421            return changesets_from_darcschanges(xml, replace_badchars=badchars)
422
423    def _parseDarcsPull(self, output):
424        """Process 'darcs pull' output to Changesets.
425        """
426        from datetime import datetime
427        from time import strptime
428        from sha import new
429
430        l = output.readline()
431        while l and not (l.startswith('Would pull the following changes:') or
432                         l == 'No remote changes to pull in!\n'):
433            l = output.readline()
434
435        if l <> 'No remote changes to pull in!\n':
436            ## Sat Jul 17 01:22:08 CEST 2004  lele@nautilus
437            ##   * Refix _getUpstreamChangesets for darcs
438
439            fsep = re.compile('[ :]+')
440            l = output.readline()
441            while not l.startswith('Making no changes:  this is a dry run.'):
442                # Assume it's a line like
443                #    Sun Jan  2 00:24:04 UTC 2005  lele@nautilus.homeip.net
444                # Use a regular expression matching multiple spaces or colons
445                # to split it, and use the first 7 fields to build up a datetime.
446                pieces = fsep.split(l.rstrip(), 8)
447                assert len(pieces)>=7, "Cannot parse %r as a patch timestamp" % l
448                date = ' '.join(pieces[:8])
449                try:
450                    author = pieces[8]
451                except IndexError, s:
452                    # darcs allows patches with empty author
453                    author = ""
454                y,m,d,hh,mm,ss,d1,d2,d3 = strptime(date, "%a %b %d %H %M %S %Z %Y")
455                date = datetime(y,m,d,hh,mm,ss,0,UTC)
456                l = output.readline().rstrip()
457                assert (l.startswith('  *') or
458                        l.startswith('  UNDO:') or
459                        l.startswith('  tagged')), \
460                        "Got %r but expected the start of the log" % l
461
462                if l.startswith('  *'):
463                    name = l[4:]
464                else:
465                    name = l[2:]
466
467                changelog = []
468                l = output.readline()
469                while l.startswith('  '):
470                    changelog.append(l[2:-1])
471                    l = output.readline()
472
473                cset = DarcsChangeset(name, date, author, '\n'.join(changelog))
474                compactdate = date.strftime("%Y%m%d%H%M%S")
475                if name.startswith('UNDO: '):
476                    name = name[6:]
477                    inverted = 't'
478                else:
479                    inverted = 'f'
480
481                if name.startswith('tagged '):
482                    name = name[7:]
483                    if cset.tags is None:
484                        cset.tags = [name]
485                    else:
486                        cset.tags.append(name)
487                    name = "TAG " + name
488
489                phash = new()
490                phash.update(name)
491                phash.update(author)
492                phash.update(compactdate)
493                phash.update(''.join(changelog))
494                phash.update(inverted)
495                cset.darcs_hash = '%s-%s-%s.gz' % (compactdate,
496                                                   new(author).hexdigest()[:5],
497                                                   phash.hexdigest())
498
499
500                yield cset
501
502                while not l.strip():
503                    l = output.readline()
504
505    def _applyChangeset(self, changeset):
506        """
507        Do the actual work of applying the changeset to the working copy.
508        """
509
510        needspatchesopt = False
511        if hasattr(changeset, 'darcs_hash'):
512            selector = '--match'
513            revtag = 'hash ' + changeset.darcs_hash
514        elif changeset.revision.startswith('tagged '):
515            selector = '--tag'
516            revtag = changeset.revision[7:]
517        else:
518            selector = '--match'
519            revtag = 'date "%s" && author "%s"' % (
520                changeset.date.strftime("%Y%m%d%H%M%S"),
521                changeset.author)
522            # The 'exact' matcher doesn't groke double quotes:
523            # """currently there is no provision for escaping a double
524            # quote, so you have to choose between matching double
525            # quotes and matching spaces"""
526            if not '"' in changeset.revision:
527                revtag += ' && exact "%s"' % changeset.revision.replace('%', '%%')
528            else:
529                needspatchesopt = True
530
531        cmd = self.repository.command("pull", "--all", "--quiet",
532                                      selector, revtag)
533
534        if needspatchesopt:
535            cmd.extend(['--patches', re.escape(changeset.revision)])
536
537        pull = ExternalCommand(cwd=self.repository.basedir, command=cmd)
538        output = pull.execute(stdout=PIPE, stderr=STDOUT, input='y')[0]
539
540        if pull.exit_status:
541            raise ChangesetApplicationFailure(
542                "%s returned status %d saying\n%s" %
543                (str(pull), pull.exit_status, output.read()))
544
545        conflicts = []
546        line = output.readline()
547        while line:
548            if line.startswith('We have conflicts in the following files:'):
549                files = output.readline()[:-1].split(' ')
550                self.log.warning("Conflict after 'darcs pull': %s",
551                                 ' '.join(files))
552                conflicts.extend(files)
553            line = output.readline()
554
555        # Complete the changeset with its entries
556
557        cmd = self.repository.command("changes", selector, revtag,
558                                      "--xml-output", "--summ")
559        changes = ExternalCommand(cwd=self.repository.basedir, command=cmd)
560        last = changesets_from_darcschanges(changes.execute(stdout=PIPE)[0],
561                                            replace_badchars=self.repository.replace_badchars)
562        try:
563            entries = last.next().entries
564        except StopIteration:
565            entries = None
566
567        if entries:
568            for e in entries:
569                changeset.addEntry(e, changeset.revision)
570
571        return conflicts
572
573    def _handleConflict(self, changeset, conflicts, conflict):
574        """
575        Handle the conflict raised by the application of the upstream changeset.
576
577        Override parent behaviour: with darcs, we need to execute a revert
578        on the conflicted files, **trashing** local changes, but there should
579        be none of them in tailor context.
580        """
581
582        from os import walk, unlink
583        from os.path import join
584        from re import compile
585
586        self.log.info("Reverting changes to %s, to solve the conflict",
587                      ' '.join(conflict))
588        cmd = self.repository.command("revert", "--all")
589        revert = ExternalCommand(cwd=self.repository.basedir, command=cmd)
590        revert.execute(conflict, input="\n")
591
592        # Remove also the backups made by darcs
593        bckre = compile('-darcs-backup[0-9]+$')
594        for root, dirs, files in walk(self.repository.basedir):
595            backups = [f for f in files if bckre.search(f)]
596            for bck in backups:
597                self.log.debug("Removing backup file %r in %r", bck, root)
598                unlink(join(root, bck))
599
600    def _checkoutUpstreamRevision(self, revision):
601        """
602        Concretely do the checkout of the upstream revision and return
603        the last applied changeset.
604        """
605
606        from os.path import join, exists
607        from os import mkdir
608        from vcpx.source import InvocationError
609
610        if not self.repository.repository:
611            raise InvocationError("Must specify a the darcs source repository")
612
613        if revision == 'INITIAL' or self.is_hash_rx.match(revision):
614            initial = True
615
616            if revision == 'INITIAL':
617                cmd = self.repository.command("changes", "--xml-output",
618                                              "--repo", self.repository.repository,
619                                               "--reverse")
620                changes = ExternalCommand(command=cmd)
621                output = changes.execute(stdout=PIPE)[0]
622
623                if changes.exit_status:
624                    raise ChangesetApplicationFailure(
625                        "%s returned status %d saying\n%s" %
626                        (str(changes), changes.exit_status,
627                         output and output.read() or ''))
628
629                csets = changesets_from_darcschanges(output, replace_badchars=self.repository.replace_badchars)
630                try:
631                    changeset = csets.next()
632                except StopIteration:
633                    # No changesets, no party!
634                    return None
635
636                revision = 'hash %s' % changeset.darcs_hash
637            else:
638                revision = 'hash %s' % revision
639        else:
640            initial = False
641
642        # Darcs 2.0 fails with "darcs get --to-match", see issue885
643        darcs2 = self.repository.darcs_version.startswith('2')
644        if darcs2 or self.repository.subdir == '.' or exists(self.repository.basedir):
645            # This is currently *very* slow, compared to the darcs get
646            # below!
647            if not exists(join(self.repository.basedir, '_darcs')):
648                if not exists(self.repository.basedir):
649                    mkdir(self.repository.basedir)
650
651                cmd = self.repository.command("initialize")
652                init = ExternalCommand(cwd=self.repository.basedir, command=cmd)
653                init.execute()
654
655                if init.exit_status:
656                    raise TargetInitializationFailure(
657                        "%s returned status %s" % (str(init),
658                                                   init.exit_status))
659
660                cmd = self.repository.command("pull", "--all", "--quiet")
661                if revision and revision<>'HEAD':
662                    cmd.extend([initial and "--match" or "--tag", revision])
663                dpull = ExternalCommand(cwd=self.repository.basedir, command=cmd)
664                output = dpull.execute(self.repository.repository,
665                                       stdout=PIPE, stderr=STDOUT)[0]
666
667                if dpull.exit_status:
668                    raise TargetInitializationFailure(
669                        "%s returned status %d saying\n%s" %
670                        (str(dpull), dpull.exit_status, output.read()))
671        else:
672            # Use much faster 'darcs get'
673            cmd = self.repository.command("get", "--quiet")
674            if revision and revision<>'HEAD':
675                cmd.extend([initial and "--to-match" or "--tag", revision])
676            else:
677                cmd.append("--partial")
678            dget = ExternalCommand(command=cmd)
679            output = dget.execute(self.repository.repository, self.repository.basedir,
680                                  stdout=PIPE, stderr=STDOUT)[0]
681
682            if dget.exit_status:
683                raise TargetInitializationFailure(
684                    "%s returned status %d saying\n%s" %
685                    (str(dget), dget.exit_status, output.read()))
686
687        cmd = self.repository.command("changes", "--last", "1",
688                                      "--xml-output")
689        changes = ExternalCommand(cwd=self.repository.basedir, command=cmd)
690        output = changes.execute(stdout=PIPE)[0]
691
692        if changes.exit_status:
693            raise ChangesetApplicationFailure(
694                "%s returned status %d saying\n%s" %
695                (str(changes), changes.exit_status, output.read()))
696
697        try:
698            last = changesets_from_darcschanges(
699                output, replace_badchars=self.repository.replace_badchars).next()
700        except StopIteration:
701            last = None
702
703        return last
Note: See TracBrowser for help on using the repository browser.