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

Revision 1571, 26.3 KB checked in by lele@…, 5 years ago (diff)

Initialize the darcs_hash attribute

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