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

Revision 1222, 18.8 KB checked in by lele@…, 7 years ago (diff)

Fix the reordering of bad ordered darcs hunks

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.shwrap import ExternalCommand, PIPE, STDOUT
17from vcpx.source import UpdatableSourceWorkingDir, ChangesetApplicationFailure, \
18                        GetUpstreamChangesetsFailure
19from vcpx.target import TargetInitializationFailure
20from vcpx.tzinfo import UTC
21
22
23def changesets_from_darcschanges(changes, unidiff=False, repodir=None,
24                                 chunksize=2**15):
25    """
26    Parse XML output of ``darcs changes``.
27
28    Return a list of ``Changeset`` instances.
29
30    Filters out the (currently incorrect) tag info from
31    changesets_from_darcschanges_unsafe.
32    """
33
34    csets = changesets_from_darcschanges_unsafe(changes, unidiff,
35                                                repodir, chunksize)
36    for cs in csets:
37        cs.tags = None
38        yield cs
39
40def changesets_from_darcschanges_unsafe(changes, unidiff=False, repodir=None,
41                                        chunksize=2**15):
42    """
43    Do the real work of parsing the change log, including tags.
44    Warning: the tag information in the changsets returned by this
45    function are only correct if each darcs tag in the repo depends on
46    all of the patches that precede it.  This is not a valid
47    assumption in general--a tag that does not depend on patch P can
48    be pulled in from another darcs repo after P.  We collect the tag
49    info anyway because DarcsWorkingDir._currentTags() can use it
50    safely despite this problem.  Hopefully the problem will
51    eventually be fixed and this function can be renamed
52    changesets_from_darcschanges.
53    """
54    from xml.sax import make_parser
55    from xml.sax.handler import ContentHandler, ErrorHandler
56    from datetime import datetime
57    from vcpx.changes import ChangesetEntry, Changeset
58
59    class DarcsXMLChangesHandler(ContentHandler):
60        def __init__(self):
61            self.changesets = []
62            self.current = None
63            self.current_field = []
64            if unidiff and repodir:
65                cmd = ["darcs", "diff", "--unified", "--repodir", repodir,
66                       "--patch", "%(patchname)s"]
67                self.darcsdiff = ExternalCommand(command=cmd)
68            else:
69                self.darcsdiff = None
70
71        def startElement(self, name, attributes):
72            if name == 'patch':
73                self.current = {}
74                self.current['author'] = attributes['author']
75                date = attributes['date']
76                from time import strptime
77                try:
78                    # 20040619130027
79                    timestamp = datetime(*strptime(date, '%Y%m%d%H%M%S')[:6])
80                except ValueError:
81                    # Old darcs patches use the form Sun Oct 20 20:01:05 EDT 2002
82                    timestamp = datetime(*strptime(date[:19] + date[-5:], '%a %b %d %H:%M:%S %Y')[:6])
83
84                timestamp = timestamp.replace(tzinfo=UTC) # not true for the ValueError case, but oh well
85
86                self.current['date'] = timestamp
87                self.current['comment'] = ''
88                self.current['hash'] = attributes['hash']
89                self.current['entries'] = []
90            elif name in ['name', 'comment', 'add_file', 'add_directory',
91                          'modify_file', 'remove_file', 'remove_directory']:
92                self.current_field = []
93            elif name == 'move':
94                self.old_name = attributes['from']
95                self.new_name = attributes['to']
96
97        def endElement(self, name):
98            if name == 'patch':
99                entries = []
100                todo = self.current['entries']
101                # Darcs allows "rename A B; remove B": collapse those
102                # into "remove A"
103                while todo:
104                    e = todo.pop(0)
105                    if e.action_kind == e.RENAMED:
106                        lookfor = e.name
107                        forget = []
108                        for i,n in enumerate(todo):
109                            if n.action_kind == n.DELETED and n.name == lookfor:
110                                e.action_kind = e.DELETED
111                                e.name = e.old_name
112                                e.old_name = None
113                                entries.append(e)
114                                forget.append(i)
115                                forget.reverse()
116                                for i in forget:
117                                    del todo[i]
118                                break
119                            elif n.action_kind == n.RENAMED and n.old_name == lookfor:
120                                forget.append(i)
121                                lookfor = n.name
122                        if not forget:
123                            entries.append(e)
124                    else:
125                        entries.append(e)
126
127                # Darcs changes --xml (as of 1.0.7) emits bad ordered hunks: it
128                # begins with file moves, apparently for no good reason. Do as
129                # little reordering as needed to adjust the meaning, ie moving all
130                # add_dirs before add_file and ren_file that have that dir as
131                # target
132
133                sorted = False
134                while not sorted:
135                    sorted = True
136                    for i,e in enumerate(entries):
137                        if e.action_kind == e.RENAMED:
138                            for j,n in enumerate(entries[i+1:]):
139                                if ((e.name.startswith(n.name+'/') or e.old_name==n.name) and
140                                    (n.action_kind == n.ADDED or n.action_kind == n.RENAMED)):
141                                    m = entries.pop(i+1+j)
142                                    entries.insert(i, m)
143                                    sorted = False
144
145                cset = Changeset(self.current['name'],
146                                 self.current['date'],
147                                 self.current['author'],
148                                 self.current['comment'],
149                                 entries,
150                                 tags=self.current.get('tags',[]))
151                cset.darcs_hash = self.current['hash']
152                if self.darcsdiff:
153                    cset.unidiff = self.darcsdiff.execute(
154                        stdout=PIPE, patchname=cset.revision)[0].read()
155
156                self.changesets.append(cset)
157                self.current = None
158            elif name in ['name', 'comment']:
159                val = ''.join(self.current_field)
160                if val[:4] == 'TAG ':
161                    self.current.setdefault('tags',[]).append(val[4:])
162                self.current[name] = val
163            elif name == 'move':
164                entry = ChangesetEntry(self.new_name)
165                entry.action_kind = entry.RENAMED
166                entry.old_name = self.old_name
167                self.current['entries'].append(entry)
168            elif name in ['add_file', 'add_directory', 'modify_file',
169                          'remove_file', 'remove_directory']:
170                entry = ChangesetEntry(''.join(self.current_field).strip())
171                entry.action_kind = { 'add_file': entry.ADDED,
172                                      'add_directory': entry.ADDED,
173                                      'modify_file': entry.UPDATED,
174                                      'remove_file': entry.DELETED,
175                                      'remove_directory': entry.DELETED
176                                    }[name]
177
178                self.current['entries'].append(entry)
179
180        def characters(self, data):
181            self.current_field.append(data)
182
183    parser = make_parser()
184    handler = DarcsXMLChangesHandler()
185    parser.setContentHandler(handler)
186    parser.setErrorHandler(ErrorHandler())
187
188    chunk = changes.read(chunksize)
189    while chunk:
190        parser.feed(chunk)
191        for cs in handler.changesets:
192            yield cs
193        handler.changesets = []
194        chunk = changes.read(chunksize)
195    parser.close()
196    for cs in handler.changesets:
197        yield cs
198
199
200class DarcsSourceWorkingDir(UpdatableSourceWorkingDir):
201    """
202    A source working directory under ``darcs``.
203    """
204
205    is_hash_rx = re.compile('[0-9a-f]{14}-[0-9a-f]{5}-[0-9a-f]{40}\.gz')
206
207    def _getUpstreamChangesets(self, sincerev):
208        """
209        Do the actual work of fetching the upstream changeset.
210        """
211
212        from datetime import datetime
213        from time import strptime
214        from sha import new
215        from vcpx.changes import Changeset
216
217        cmd = self.repository.command("pull", "--dry-run")
218        pull = ExternalCommand(cwd=self.repository.basedir, command=cmd)
219        output = pull.execute(self.repository.repository,
220                              stdout=PIPE, stderr=STDOUT, TZ='UTC0')[0]
221
222        if pull.exit_status:
223            raise GetUpstreamChangesetsFailure(
224                "%s returned status %d saying\n%s" %
225                (str(pull), pull.exit_status, output.read()))
226
227        l = output.readline()
228        while l and not (l.startswith('Would pull the following changes:') or
229                         l == 'No remote changes to pull in!\n'):
230            l = output.readline()
231
232        if l <> 'No remote changes to pull in!\n':
233            ## Sat Jul 17 01:22:08 CEST 2004  lele@nautilus
234            ##   * Refix _getUpstreamChangesets for darcs
235
236            l = output.readline()
237            while not l.startswith('Making no changes:  this is a dry run.'):
238                # Assume it's a line like
239                #    Sun Jan  2 00:24:04 UTC 2005  lele@nautilus.homeip.net
240                # we used to split on the double space before the email,
241                # but in this case this is wrong. Waiting for xml output,
242                # is it really sane asserting date's length to 28 chars?
243                date = l[:28]
244                author = l[30:-1]
245                y,m,d,hh,mm,ss,d1,d2,d3 = strptime(date, "%a %b %d %H:%M:%S %Z %Y")
246                date = datetime(y,m,d,hh,mm,ss,0,UTC)
247                l = output.readline()
248                assert (l.startswith('  * ') or
249                        l.startswith('  UNDO:') or
250                        l.startswith('  tagged'))
251
252                if l.startswith('  *'):
253                    name = l[4:-1]
254                else:
255                    name = l[2:-1]
256
257                changelog = []
258                l = output.readline()
259                while l.startswith('  '):
260                    changelog.append(l[2:-1])
261                    l = output.readline()
262
263                cset = Changeset(name, date, author, '\n'.join(changelog))
264                compactdate = date.strftime("%Y%m%d%H%M%S")
265                if name.startswith('UNDO: '):
266                    name = name[6:]
267                    inverted = 't'
268                else:
269                    inverted = 'f'
270                phash = new()
271                phash.update(name)
272                phash.update(author)
273                phash.update(compactdate)
274                phash.update(''.join(changelog))
275                phash.update(inverted)
276                cset.darcs_hash = '%s-%s-%s.gz' % (compactdate,
277                                                   new(author).hexdigest()[:5],
278                                                   phash.hexdigest())
279
280                if name.startswith('tagged'):
281                    self.log.warning("Skipping tag %s because I don't "
282                                     "propagate tags from darcs.", name)
283                else:
284                    yield cset
285
286                while not l.strip():
287                    l = output.readline()
288
289    def _applyChangeset(self, changeset):
290        """
291        Do the actual work of applying the changeset to the working copy.
292        """
293
294        needspatchesopt = False
295        if hasattr(changeset, 'darcs_hash'):
296            selector = '--match'
297            revtag = 'hash ' + changeset.darcs_hash
298        elif changeset.revision.startswith('tagged '):
299            selector = '--tag'
300            revtag = changeset.revision[7:]
301        else:
302            selector = '--match'
303            revtag = 'date "%s" && author "%s"' % (
304                changeset.date.strftime("%Y%m%d%H%M%S"),
305                changeset.author)
306            # The 'exact' matcher doesn't groke double quotes:
307            # """currently there is no provision for escaping a double
308            # quote, so you have to choose between matching double
309            # quotes and matching spaces"""
310            if not '"' in changeset.revision:
311                revtag += ' && exact "%s"' % changeset.revision.replace('%', '%%')
312            else:
313                needspatchesopt = True
314
315        cmd = self.repository.command("pull", "--all", "--quiet",
316                                      selector, revtag)
317
318        if needspatchesopt:
319            cmd.extend(['--patches', re.escape(changeset.revision)])
320
321        pull = ExternalCommand(cwd=self.repository.basedir, command=cmd)
322        output = pull.execute(stdout=PIPE, stderr=STDOUT, input='y')[0]
323
324        if pull.exit_status:
325            raise ChangesetApplicationFailure(
326                "%s returned status %d saying\n%s" %
327                (str(pull), pull.exit_status, output.read()))
328
329        conflicts = []
330        line = output.readline()
331        while line:
332            if line.startswith('We have conflicts in the following files:'):
333                files = output.readline()[:-1].split('./')[1:]
334                self.log.warning("Conflict after 'darcs pull': %s",
335                                 ' '.join(files))
336                conflicts.extend(['./' + f for f in files])
337            line = output.readline()
338
339        cmd = self.repository.command("changes", selector, revtag,
340                                      "--xml-output", "--summ")
341        changes = ExternalCommand(cwd=self.repository.basedir, command=cmd)
342        last = changesets_from_darcschanges(changes.execute(stdout=PIPE)[0])
343        try:
344            changeset.entries.extend(last.next().entries)
345        except StopIteration:
346            pass
347
348        return conflicts
349
350    def _handleConflict(self, changeset, conflicts, conflict):
351        """
352        Handle the conflict raised by the application of the upstream changeset.
353
354        Override parent behaviour: with darcs, we need to execute a revert
355        on the conflicted files, **trashing** local changes, but there should
356        be none of them in tailor context.
357        """
358
359        self.log.info("Reverting changes to %s, to solve the conflict",
360                      ' '.join(conflict))
361        cmd = self.repository.command("revert", "--all")
362        revert = ExternalCommand(cwd=self.repository.basedir, command=cmd)
363        revert.execute(conflict)
364
365    def _checkoutUpstreamRevision(self, revision):
366        """
367        Concretely do the checkout of the upstream revision and return
368        the last applied changeset.
369        """
370
371        from os.path import join, exists
372        from os import mkdir
373        from vcpx.source import InvocationError
374
375        if not self.repository.repository:
376            raise InvocationError("Must specify a the darcs source repository")
377
378        if revision == 'INITIAL' or self.is_hash_rx.match(revision):
379            initial = True
380
381            if revision == 'INITIAL':
382                cmd = self.repository.command("changes", "--xml-output",
383                                              "--repo", self.repository.repository,
384                                               "--reverse")
385                changes = ExternalCommand(command=cmd)
386                output = changes.execute(stdout=PIPE, stderr=STDOUT)[0]
387
388                if changes.exit_status:
389                    raise ChangesetApplicationFailure(
390                        "%s returned status %d saying\n%s" %
391                        (str(changes), changes.exit_status,
392                         output and output.read() or ''))
393
394                csets = changesets_from_darcschanges(output)
395                changeset = csets.next()
396
397                revision = 'hash %s' % changeset.darcs_hash
398            else:
399                revision = 'hash %s' % revision
400        else:
401            initial = False
402
403        if self.repository.subdir == '.' or exists(self.repository.basedir):
404            # This is currently *very* slow, compared to the darcs get
405            # below!
406            if not exists(join(self.repository.basedir, '_darcs')):
407                if not exists(self.repository.basedir):
408                    mkdir(self.repository.basedir)
409
410                cmd = self.repository.command("initialize")
411                init = ExternalCommand(cwd=self.repository.basedir, command=cmd)
412                init.execute()
413
414                if init.exit_status:
415                    raise TargetInitializationFailure(
416                        "%s returned status %s" % (str(init),
417                                                   init.exit_status))
418
419                cmd = self.repository.command("pull", "--all", "--quiet")
420                if revision and revision<>'HEAD':
421                    cmd.extend([initial and "--match" or "--tag", revision])
422                dpull = ExternalCommand(cwd=self.repository.basedir, command=cmd)
423                output = dpull.execute(self.repository.repository,
424                                       stdout=PIPE, stderr=STDOUT)[0]
425
426                if dpull.exit_status:
427                    raise TargetInitializationFailure(
428                        "%s returned status %d saying\n%s" %
429                        (str(dpull), dpull.exit_status, output.read()))
430        else:
431            # Use much faster 'darcs get'
432            cmd = self.repository.command("get", "--quiet")
433            if revision and revision<>'HEAD':
434                cmd.extend([initial and "--to-match" or "--tag", revision])
435            else:
436                cmd.append("--partial")
437            dget = ExternalCommand(command=cmd)
438            output = dget.execute(self.repository.repository, self.repository.basedir,
439                                  stdout=PIPE, stderr=STDOUT)[0]
440
441            if dget.exit_status:
442                raise TargetInitializationFailure(
443                    "%s returned status %d saying\n%s" %
444                    (str(dget), dget.exit_status, output.read()))
445
446        cmd = self.repository.command("changes", "--last", "1",
447                                      "--xml-output")
448        changes = ExternalCommand(cwd=self.repository.basedir, command=cmd)
449        output = changes.execute(stdout=PIPE, stderr=STDOUT)[0]
450
451        if changes.exit_status:
452            raise ChangesetApplicationFailure(
453                "%s returned status %d saying\n%s" %
454                (str(changes), changes.exit_status, output.read()))
455
456        last = changesets_from_darcschanges(output)
457
458        return last.next()
Note: See TracBrowser for help on using the repository browser.