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

Revision 1604, 28.6 KB checked in by lele@…, 5 years ago (diff)

Ignore stderr when fetching darcs XML output

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