source: tailor/vcpx/darcs.py @ 1071

Revision 1071, 24.8 KB checked in by lele@…, 7 years ago (diff)

Separate option name from its value

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