source: tailor/vcpx/darcs.py @ 1162

Revision 1162, 27.2 KB checked in by lele@…, 7 years ago (diff)

Remove useless imports noticed by pyflakes

Line 
1# -*- mode: python; coding: utf-8 -*-
2# :Progetto: vcpx -- Darcs details
3# :Creato:   ven 18 giu 2004 14:45:28 CEST
4# :Autore:   Lele Gaifax <lele@nautilus.homeip.net>
5# :Licenza:  GNU General Public License
6#
7
8"""
9This module contains supporting classes for the ``darcs`` versioning system.
10"""
11
12__docformat__ = 'reStructuredText'
13
14from shwrap import ExternalCommand, PIPE, STDOUT
15from source import UpdatableSourceWorkingDir, ChangesetApplicationFailure, \
16     GetUpstreamChangesetsFailure
17from target import SynchronizableTargetWorkingDir, TargetInitializationFailure
18import re
19
20MOTD = """\
21Tailorized equivalent of
22%s
23"""
24
25def changesets_from_darcschanges(changes, unidiff=False, repodir=None,
26                                 chunksize=2**15):
27    """
28    Parse XML output of ``darcs changes``.
29
30    Return a list of ``Changeset`` instances.
31
32    Filters out the (currently incorrect) tag info from
33    changesets_from_darcschanges_unsafe.
34    """
35
36    csets = changesets_from_darcschanges_unsafe(changes, unidiff,
37                                                repodir, chunksize)
38    for cs in csets:
39        cs.tags = None
40        yield cs
41
42def changesets_from_darcschanges_unsafe(changes, unidiff=False, repodir=None,
43                                        chunksize=2**15):
44    """
45    Do the real work of parsing the change log, including tags.
46    Warning: the tag information in the changsets returned by this
47    function are only correct if each darcs tag in the repo depends on
48    all of the patches that precede it.  This is not a valid
49    assumption in general--a tag that does not depend on patch P can
50    be pulled in from another darcs repo after P.  We collect the tag
51    info anyway because DarcsWorkingDir._currentTags() can use it
52    safely despite this problem.  Hopefully the problem will
53    eventually be fixed and this function can be renamed
54    changesets_from_darcschanges.
55    """
56    from xml.sax import make_parser
57    from xml.sax.handler import ContentHandler, ErrorHandler
58    from changes import ChangesetEntry, Changeset
59    from datetime import datetime
60
61    class DarcsXMLChangesHandler(ContentHandler):
62        def __init__(self):
63            self.changesets = []
64            self.current = None
65            self.current_field = []
66            if unidiff and repodir:
67                cmd = ["darcs", "diff", "--unified", "--repodir", repodir,
68                       "--patch", "%(patchname)s"]
69                self.darcsdiff = ExternalCommand(command=cmd)
70            else:
71                self.darcsdiff = None
72
73        def startElement(self, name, attributes):
74            if name == 'patch':
75                self.current = {}
76                self.current['author'] = attributes['author']
77                date = attributes['date']
78                from time import strptime
79                try:
80                    # 20040619130027
81                    timestamp = datetime(*strptime(date, '%Y%m%d%H%M%S')[:6])
82                except ValueError:
83                    # Old darcs patches use the form Sun Oct 20 20:01:05 EDT 2002
84                    timestamp = datetime(*strptime(date[:19] + date[-5:], '%a %b %d %H:%M:%S %Y')[:6])
85                self.current['date'] = timestamp
86                self.current['comment'] = ''
87                self.current['hash'] = attributes['hash']
88                self.current['entries'] = []
89            elif name in ['name', 'comment', 'add_file', 'add_directory',
90                          'modify_file', 'remove_file', 'remove_directory']:
91                self.current_field = []
92            elif name == 'move':
93                self.old_name = attributes['from']
94                self.new_name = attributes['to']
95
96        def endElement(self, name):
97            if name == 'patch':
98                entries = []
99                todo = self.current['entries']
100                # Darcs allows "rename A B; remove B": collapse those
101                # into "remove A"
102                while todo:
103                    e = todo.pop(0)
104                    if e.action_kind == e.RENAMED:
105                        lookfor = e.name
106                        forget = []
107                        for i,n in enumerate(todo):
108                            if n.action_kind == n.DELETED and n.name == lookfor:
109                                e.action_kind = e.DELETED
110                                e.name = e.old_name
111                                e.old_name = None
112                                entries.append(e)
113                                forget.append(i)
114                                forget.reverse()
115                                for i in forget:
116                                    del todo[i]
117                                break
118                            elif n.action_kind == n.RENAMED and n.old_name == lookfor:
119                                forget.append(i)
120                                lookfor = n.name
121                        if not forget:
122                            entries.append(e)
123                    else:
124                        entries.append(e)
125
126                # Darcs changes --xml (as of 1.0.7) emits bad ordered hunks: it
127                # begins with file moves, apparently for no good reason. Do as
128                # little reordering as needed to adjust the meaning, ie moving all
129                # add_dirs before add_file and ren_file that have that dir as
130                # target
131
132                sorted = False
133                while not sorted:
134                    sorted = True
135                    for i,e in enumerate(entries):
136                        if e.action_kind == e.RENAMED:
137                            for j,n in enumerate(entries[i+1:]):
138                                if (e.name.startswith(n.name+'/') and
139                                    (n.action_kind == n.ADDED or
140                                     n.action_kind == n.RENAMED)):
141                                    m = entries.pop(i+1+j)
142                                    entries.insert(i, m)
143                                    sorted = False
144
145                name = self.current['name']
146                log = self.current['comment']
147                if log:
148                    changelog = name + '\n' + log
149                else:
150                    changelog = name
151                cset = Changeset(name,
152                                 self.current['date'],
153                                 self.current['author'],
154                                 changelog,
155                                 entries,
156                                 tags=self.current.get('tags',[]))
157                cset.darcs_hash = self.current['hash']
158                if self.darcsdiff:
159                    cset.unidiff = self.darcsdiff.execute(
160                        stdout=PIPE, patchname=cset.revision)[0].read()
161
162                self.changesets.append(cset)
163                self.current = None
164            elif name in ['name', 'comment']:
165                val = ''.join(self.current_field)
166                if val[:4] == 'TAG ':
167                    self.current.setdefault('tags',[]).append(val[4:])
168                self.current[name] = val
169            elif name == 'move':
170                entry = ChangesetEntry(self.new_name)
171                entry.action_kind = entry.RENAMED
172                entry.old_name = self.old_name
173                self.current['entries'].append(entry)
174            elif name in ['add_file', 'add_directory', 'modify_file',
175                          'remove_file', 'remove_directory']:
176                entry = ChangesetEntry(''.join(self.current_field).strip())
177                entry.action_kind = { 'add_file': entry.ADDED,
178                                      'add_directory': entry.ADDED,
179                                      'modify_file': entry.UPDATED,
180                                      'remove_file': entry.DELETED,
181                                      'remove_directory': entry.DELETED
182                                    }[name]
183
184                self.current['entries'].append(entry)
185
186        def characters(self, data):
187            self.current_field.append(data)
188
189    parser = make_parser()
190    handler = DarcsXMLChangesHandler()
191    parser.setContentHandler(handler)
192    parser.setErrorHandler(ErrorHandler())
193
194    chunk = changes.read(chunksize)
195    while chunk:
196        parser.feed(chunk)
197        for cs in handler.changesets:
198            yield cs
199        handler.changesets = []
200        chunk = changes.read(chunksize)
201    parser.close()
202    for cs in handler.changesets:
203        yield cs
204
205class DarcsWorkingDir(UpdatableSourceWorkingDir,SynchronizableTargetWorkingDir):
206    """
207    A working directory under ``darcs``.
208    """
209
210    is_hash_rx = re.compile('[0-9a-f]{14}-[0-9a-f]{5}-[0-9a-f]{40}\.gz')
211
212    ## UpdatableSourceWorkingDir
213
214    def _getUpstreamChangesets(self, sincerev):
215        """
216        Do the actual work of fetching the upstream changeset.
217        """
218
219        from datetime import datetime
220        from time import strptime
221        from changes import Changeset
222        from sha import new
223
224        cmd = self.repository.command("pull", "--dry-run")
225        pull = ExternalCommand(cwd=self.basedir, command=cmd)
226        output = pull.execute(self.repository.repository,
227                              stdout=PIPE, stderr=STDOUT, TZ='UTC0')[0]
228
229        if pull.exit_status:
230            raise GetUpstreamChangesetsFailure(
231                "%s returned status %d saying\n%s" %
232                (str(pull), pull.exit_status, output.read()))
233
234        l = output.readline()
235        while l and not (l.startswith('Would pull the following changes:') or
236                         l == 'No remote changes to pull in!\n'):
237            l = output.readline()
238
239        if l <> 'No remote changes to pull in!\n':
240            ## Sat Jul 17 01:22:08 CEST 2004  lele@nautilus
241            ##   * Refix _getUpstreamChangesets for darcs
242
243            l = output.readline()
244            while not l.startswith('Making no changes:  this is a dry run.'):
245                # Assume it's a line like
246                #    Sun Jan  2 00:24:04 UTC 2005  lele@nautilus.homeip.net
247                # we used to split on the double space before the email,
248                # but in this case this is wrong. Waiting for xml output,
249                # is it really sane asserting date's length to 28 chars?
250                date = l[:28]
251                author = l[30:-1]
252                y,m,d,hh,mm,ss,d1,d2,d3 = strptime(date, "%a %b %d %H:%M:%S %Z %Y")
253                date = datetime(y,m,d,hh,mm,ss)
254                l = output.readline()
255                assert (l.startswith('  * ') or
256                        l.startswith('  UNDO:') or
257                        l.startswith('  tagged'))
258
259                if l.startswith('  *'):
260                    name = l[4:-1]
261                else:
262                    name = l[2:-1]
263
264                changelog = []
265                l = output.readline()
266                while l.startswith('  '):
267                    changelog.append(l[2:-1])
268                    l = output.readline()
269
270                cset = Changeset(name, date, author, '\n'.join(changelog))
271                compactdate = date.strftime("%Y%m%d%H%M%S")
272                if name.startswith('UNDO: '):
273                    name = name[6:]
274                    inverted = 't'
275                else:
276                    inverted = 'f'
277                phash = new()
278                phash.update(name)
279                phash.update(author)
280                phash.update(compactdate)
281                phash.update(''.join(changelog))
282                phash.update(inverted)
283                cset.darcs_hash = '%s-%s-%s.gz' % (compactdate,
284                                                   new(author).hexdigest()[:5],
285                                                   phash.hexdigest())
286
287                if name.startswith('tagged'):
288                    self.log.warning("Skipping tag %s because I don't "
289                                     "propagate tags from darcs.", name)
290                else:
291                    yield cset
292
293                while not l.strip():
294                    l = output.readline()
295
296    def _applyChangeset(self, changeset):
297        """
298        Do the actual work of applying the changeset to the working copy.
299        """
300
301        needspatchesopt = False
302        if hasattr(changeset, 'darcs_hash'):
303            selector = '--match'
304            revtag = 'hash ' + changeset.darcs_hash
305        elif changeset.revision.startswith('tagged '):
306            selector = '--tag'
307            revtag = changeset.revision[7:]
308        else:
309            selector = '--match'
310            revtag = 'date "%s" && author "%s"' % (
311                changeset.date.strftime("%Y%m%d%H%M%S"),
312                changeset.author)
313            # The 'exact' matcher doesn't groke double quotes:
314            # """currently there is no provision for escaping a double
315            # quote, so you have to choose between matching double
316            # quotes and matching spaces"""
317            if not '"' in changeset.revision:
318                revtag += ' && exact "%s"' % changeset.revision.replace('%', '%%')
319            else:
320                needspatchesopt = True
321
322        cmd = self.repository.command("pull", "--all", "--quiet",
323                                      selector, revtag)
324
325        if needspatchesopt:
326            cmd.extend(['--patches', re.escape(changeset.revision)])
327
328        pull = ExternalCommand(cwd=self.basedir, command=cmd)
329        output = pull.execute(stdout=PIPE, stderr=STDOUT, input='y')[0]
330
331        if pull.exit_status:
332            raise ChangesetApplicationFailure(
333                "%s returned status %d saying\n%s" %
334                (str(pull), pull.exit_status, output.read()))
335
336        conflicts = []
337        line = output.readline()
338        while line:
339            if line.startswith('We have conflicts in the following files:'):
340                files = output.readline()[:-1].split('./')[1:]
341                self.log.warning("Conflict after 'darcs pull': %s",
342                                 ' '.join(files))
343                conflicts.extend(['./' + f for f in files])
344            line = output.readline()
345
346        cmd = self.repository.command("changes", selector, revtag,
347                                      "--xml-output", "--summ")
348        changes = ExternalCommand(cwd=self.basedir, command=cmd)
349        last = changesets_from_darcschanges(changes.execute(stdout=PIPE)[0])
350        try:
351            changeset.entries.extend(last.next().entries)
352        except StopIteration:
353            pass
354
355        return conflicts
356
357    def _handleConflict(self, changeset, conflicts, conflict):
358        """
359        Handle the conflict raised by the application of the upstream changeset.
360
361        Override parent behaviour: with darcs, we need to execute a revert
362        on the conflicted files, **trashing** local changes, but there should
363        be none of them in tailor context.
364        """
365
366        self.log.info("Reverting changes to %s, to solve the conflict",
367                      ' '.join(conflict))
368        cmd = self.repository.command("revert", "--all")
369        revert = ExternalCommand(cwd=self.basedir, command=cmd)
370        revert.execute(conflict)
371
372    def _checkoutUpstreamRevision(self, revision):
373        """
374        Concretely do the checkout of the upstream revision and return
375        the last applied changeset.
376        """
377
378        from os.path import join, exists
379        from os import mkdir
380        from source import InvocationError
381
382        if not self.repository.repository:
383            raise InvocationError("Must specify a the darcs source repository")
384
385        if revision == 'INITIAL' or self.is_hash_rx.match(revision):
386            initial = True
387
388            if revision == 'INITIAL':
389                cmd = self.repository.command("changes", "--xml-output",
390                                              "--repo", self.repository.repository,
391                                               "--reverse")
392                changes = ExternalCommand(command=cmd)
393                output = changes.execute(stdout=PIPE, stderr=STDOUT)[0]
394
395                if changes.exit_status:
396                    raise ChangesetApplicationFailure(
397                        "%s returned status %d saying\n%s" %
398                        (str(changes), changes.exit_status,
399                         output and output.read() or ''))
400
401                csets = changesets_from_darcschanges(output)
402                changeset = csets.next()
403
404                revision = 'hash %s' % changeset.darcs_hash
405            else:
406                revision = 'hash %s' % revision
407        else:
408            initial = False
409
410        if self.repository.subdir == '.' or exists(self.basedir):
411            # This is currently *very* slow, compared to the darcs get
412            # below!
413            if not exists(join(self.basedir, '_darcs')):
414                if not exists(self.basedir):
415                    mkdir(self.basedir)
416
417                cmd = self.repository.command("initialize")
418                init = ExternalCommand(cwd=self.basedir, command=cmd)
419                init.execute()
420
421                if init.exit_status:
422                    raise TargetInitializationFailure(
423                        "%s returned status %s" % (str(init),
424                                                   init.exit_status))
425
426                cmd = self.repository.command("pull", "--all", "--quiet")
427                if revision and revision<>'HEAD':
428                    cmd.extend([initial and "--match" or "--tag", revision])
429                dpull = ExternalCommand(cwd=self.basedir, command=cmd)
430                output = dpull.execute(self.repository.repository,
431                                       stdout=PIPE, stderr=STDOUT)[0]
432
433                if dpull.exit_status:
434                    raise TargetInitializationFailure(
435                        "%s returned status %d saying\n%s" %
436                        (str(dpull), dpull.exit_status, output.read()))
437        else:
438            # Use much faster 'darcs get'
439            cmd = self.repository.command("get", "--quiet")
440            if revision and revision<>'HEAD':
441                cmd.extend([initial and "--to-match" or "--tag", revision])
442            else:
443                cmd.append("--partial")
444            dget = ExternalCommand(command=cmd)
445            output = dget.execute(self.repository.repository, self.basedir,
446                                  stdout=PIPE, stderr=STDOUT)[0]
447
448            if dget.exit_status:
449                raise TargetInitializationFailure(
450                    "%s returned status %d saying\n%s" %
451                    (str(dget), dget.exit_status, output.read()))
452
453        cmd = self.repository.command("changes", "--last", "1",
454                                      "--xml-output")
455        changes = ExternalCommand(cwd=self.basedir, command=cmd)
456        output = changes.execute(stdout=PIPE, stderr=STDOUT)[0]
457
458        if changes.exit_status:
459            raise ChangesetApplicationFailure(
460                "%s returned status %d saying\n%s" %
461                (str(changes), changes.exit_status, output.read()))
462
463        last = changesets_from_darcschanges(output)
464
465        return last.next()
466
467
468    ## SynchronizableTargetWorkingDir
469
470    def _addPathnames(self, names):
471        """
472        Add some new filesystem objects.
473        """
474
475        cmd = self.repository.command("add", "--case-ok", "--not-recursive",
476                                      "--quiet")
477        ExternalCommand(cwd=self.basedir, command=cmd).execute(names)
478
479    def _addSubtree(self, subdir):
480        """
481        Use the --recursive variant of ``darcs add`` to add a subtree.
482        """
483
484        cmd = self.repository.command("add", "--case-ok", "--recursive",
485                                      "--quiet")
486        ExternalCommand(cwd=self.basedir, command=cmd).execute(subdir)
487
488    def _commit(self, date, author, patchname, changelog=None, entries=None):
489        """
490        Commit the changeset.
491        """
492
493        logmessage = []
494
495        logmessage.append(date.strftime('%Y/%m/%d %H:%M:%S UTC'))
496        logmessage.append(author)
497        if patchname:
498            logmessage.append(patchname)
499        if changelog:
500            logmessage.append(changelog)
501        if not patchname and not changelog:
502            logmessage.append('Unnamed patch')
503
504        cmd = self.repository.command("record", "--all", "--pipe")
505        if not entries:
506            entries = ['.']
507
508        record = ExternalCommand(cwd=self.basedir, command=cmd)
509        record.execute(input=self.repository.encode('\n'.join(logmessage)))
510
511        if record.exit_status:
512            raise ChangesetApplicationFailure(
513                "%s returned status %d" % (str(record), record.exit_status))
514
515    def _removePathnames(self, names):
516        """
517        Remove some filesystem object.
518        """
519
520        from os.path import join, exists
521
522        # darcs raises status 512 when it does not find the entry,
523        # removed by source. Since sometime a directory is left there
524        # because it's not empty, darcs fails. So, do an explicit
525        # remove on items that are still there.
526
527        c = ExternalCommand(cwd=self.basedir,
528                            command=self.repository.command("remove"))
529        existing = [n for n in names if exists(join(self.basedir, n))]
530        if existing:
531            c.execute(existing)
532
533    def _renamePathname(self, oldname, newname):
534        """
535        Rename a filesystem object.
536        """
537
538        cmd = self.repository.command("mv")
539        ExternalCommand(cwd=self.basedir, command=cmd).execute(oldname, newname)
540
541    def _prepareTargetRepository(self):
542        """
543        Create the base directory if it doesn't exist, and execute
544        ``darcs initialize`` if needed.
545        """
546
547        from os.path import join, exists
548        from dualwd import IGNORED_METADIRS
549
550        metadir = join(self.basedir, '_darcs')
551        prefsdir = join(metadir, 'prefs')
552        prefsname = join(prefsdir, 'prefs')
553        boringname = join(prefsdir, 'boring')
554        if exists(prefsname):
555            for pref in open(prefsname, 'rU'):
556                if pref:
557                    pname, pvalue = pref.split(' ', 1)
558                    if pname == 'boringfile':
559                        boringname = join(self.basedir, pvalue[:-1])
560
561        if not exists(metadir):
562            cmd = self.repository.command("initialize")
563            init = ExternalCommand(cwd=self.basedir, command=cmd)
564            init.execute()
565
566            if init.exit_status:
567                raise TargetInitializationFailure(
568                    "%s returned status %s" % (str(init), init.exit_status))
569
570            boring = open(boringname, 'rU')
571            ignored = boring.read().rstrip().split('\n')
572            boring.close()
573
574            # Augment the boring file, that contains a regexp per line
575            # with all known VCs metadirs to be skipped.
576            ignored.extend(['(^|/)%s($|/)' % re.escape(md)
577                            for md in IGNORED_METADIRS])
578
579            # Eventually omit our own log...
580            logfile = self.repository.projectref().logfile
581            if logfile.startswith(self.basedir):
582                ignored.append('^%s$' %
583                               re.escape(logfile[len(self.basedir)+1:]))
584
585            # ... and state file
586            sfname = self.repository.projectref().state_file.filename
587            if sfname.startswith(self.basedir):
588                sfrelname = sfname[len(self.basedir)+1:]
589                ignored.append('^%s$' % re.escape(sfrelname))
590                ignored.append('^%s$' % re.escape(sfrelname+'.old'))
591                ignored.append('^%s$' % re.escape(sfrelname+'.journal'))
592
593            boring = open(boringname, 'wU')
594            boring.write('\n'.join(ignored))
595            boring.write('\n')
596            boring.close()
597        else:
598            boring = open(boringname, 'rU')
599            ignored = boring.read().rstrip().split('\n')
600            boring.close()
601
602        # Build a list of compiled regular expressions, that will be
603        # used later to filter the entries.
604        self.__unwanted_entries = [re.compile(rx) for rx in ignored
605                                   if rx and not rx.startswith('#')]
606
607    def _prepareWorkingDirectory(self, source_repo):
608        """
609        Tweak the default settings of the repository.
610        """
611
612        from os.path import join
613
614        motd = open(join(self.basedir, '_darcs/prefs/motd'), 'w')
615        motd.write(MOTD % str(source_repo))
616        motd.close()
617
618    def _adaptEntries(self, changeset):
619        """
620        Filter out boring files.
621        """
622
623        from copy import copy
624
625        adapted = SynchronizableTargetWorkingDir._adaptEntries(self, changeset)
626
627        # If there are no entries or no rules, there's nothing to do
628        if not adapted or not adapted.entries or not self.__unwanted_entries:
629            return adapted
630
631        entries = []
632        skipped = False
633        for e in adapted.entries:
634            skip = False
635            for rx in self.__unwanted_entries:
636                if rx.search(e.name):
637                    skip = True
638                    break
639            if skip:
640                self.log.info('Entry "%s" skipped per boring rules', e.name)
641                skipped = True
642            else:
643                entries.append(e)
644
645        # All entries are gone, don't commit this changeset
646        if not entries:
647            self.log.info('All entries ignored, skipping whole '
648                          'changeset "%s"', changeset.revision)
649            return None
650
651        if skipped:
652            adapted = copy(adapted)
653            adapted.entries = entries
654
655        return adapted
656
657    def _tag(self, tag):
658        """
659        Apply the given tag to the repository, unless it has already
660        been applied to the current state. (If it has been applied to
661        an earlier state, do apply it; the later tag overrides the
662        earlier one.
663        """
664        if tag not in self._currentTags():
665            cmd = self.repository.command("tag", "--author", "Unknown tagger")
666            ExternalCommand(cwd=self.basedir, command=cmd).execute(tag)
667
668    def _currentTags(self):
669        """
670        Return a list of tags that refer to the repository's current
671        state.  Does not consider tags themselves to be part of the
672        state, so if the repo was tagged with T1 and then T2, then
673        both T1 and T2 are considered to refer to the current state,
674        even though 'darcs get --tag=T1' and 'darcs get --tag=T2'
675        would have different results (the latter creates a repo that
676        contains tag T2, but the former does not).
677
678        This function assumes that a tag depends on all patches that
679        precede it in the "darcs changes" list.  This assumption is
680        valid if tags only come into the repository via tailor; if the
681        user applies a tag by hand in the hybrid repository, or pulls
682        in a tag from another darcs repository, then the assumption
683        could be violated and mistagging could result.
684        """
685        cmd = self.repository.command("changes",
686                                      "--from-match", "not name ^TAG",
687                                      "--xml-output", "--reverse")
688        changes =  ExternalCommand(cwd=self.basedir, command=cmd)
689        output = changes.execute(stdout=PIPE, stderr=STDOUT)[0]
690        if changes.exit_status:
691            raise ChangesetApplicationFailure(
692                "%s returned status %d saying\n%s" %
693                (str(changes), changes.exit_status, output.read()))
694
695        tags = []
696        for cs in changesets_from_darcschanges_unsafe(output):
697            for tag in cs.tags:
698                if tag not in tags:
699                    tags.append(tag)
700        return tags
Note: See TracBrowser for help on using the repository browser.