source: tailor/vcpx/darcs.py @ 632

Revision 632, 19.1 KB checked in by lele@…, 8 years ago (diff)

Ignore the journal file when it resides under the basedir

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 = """\
21This is the Darcs equivalent of
22%s
23"""
24
25def changesets_from_darcschanges(changes, unidiff=False, repodir=None):
26    """
27    Parse XML output of ``darcs changes``.
28
29    Return a list of ``Changeset`` instances.
30    """
31
32    from xml.sax import parse
33    from xml.sax.handler import ContentHandler
34    from changes import ChangesetEntry, Changeset
35    from datetime import datetime
36
37    class DarcsXMLChangesHandler(ContentHandler):
38        def __init__(self):
39            self.changesets = []
40            self.current = None
41            self.current_field = []
42            if unidiff and repodir:
43                cmd = ["darcs", "diff", "--unified", "--repodir", repodir,
44                       "--patch", "%(patchname)s"]
45                self.darcsdiff = ExternalCommand(command=cmd)
46            else:
47                self.darcsdiff = None
48
49        def startElement(self, name, attributes):
50            if name == 'patch':
51                self.current = {}
52                self.current['author'] = attributes['author']
53                date = attributes['date']
54                # 20040619130027
55                y = int(date[:4])
56                m = int(date[4:6])
57                d = int(date[6:8])
58                hh = int(date[8:10])
59                mm = int(date[10:12])
60                ss = int(date[12:14])
61                timestamp = datetime(y, m, d, hh, mm, ss)
62                self.current['date'] = timestamp
63                self.current['comment'] = ''
64                self.current['hash'] = attributes['hash']
65                self.current['entries'] = []
66            elif name in ['name', 'comment', 'add_file', 'add_directory',
67                          'modify_file', 'remove_file', 'remove_directory']:
68                self.current_field = []
69            elif name == 'move':
70                self.old_name = attributes['from']
71                self.new_name = attributes['to']
72
73        def endElement(self, name):
74            if name == 'patch':
75                # Sort the paths to make tests easier
76                self.current['entries'].sort(lambda x,y: cmp(x.name, y.name))
77                name = self.current['name']
78                log = self.current['comment']
79                if log:
80                    changelog = name + '\n' + log
81                else:
82                    changelog = name
83                cset = Changeset(name,
84                                 self.current['date'],
85                                 self.current['author'],
86                                 changelog,
87                                 self.current['entries'])
88                cset.darcs_hash = self.current['hash']
89                if self.darcsdiff:
90                    cset.unidiff = self.darcsdiff.execute(
91                        stdout=PIPE, patchname=cset.revision)[0].read()
92
93                self.changesets.append(cset)
94                self.current = None
95            elif name in ['name', 'comment']:
96                self.current[name] = ''.join(self.current_field)
97            elif name == 'move':
98                entry = ChangesetEntry(self.new_name)
99                entry.action_kind = entry.RENAMED
100                entry.old_name = self.old_name
101                self.current['entries'].append(entry)
102            elif name in ['add_file', 'add_directory', 'modify_file',
103                          'remove_file', 'remove_directory']:
104                entry = ChangesetEntry(''.join(self.current_field).strip())
105                entry.action_kind = { 'add_file': entry.ADDED,
106                                      'add_directory': entry.ADDED,
107                                      'modify_file': entry.UPDATED,
108                                      'remove_file': entry.DELETED,
109                                      'remove_directory': entry.DELETED
110                                    }[name]
111
112                self.current['entries'].append(entry)
113
114        def characters(self, data):
115            self.current_field.append(data)
116
117
118    handler = DarcsXMLChangesHandler()
119    parse(changes, handler)
120    changesets = handler.changesets
121
122    # sort changeset by date
123    changesets.sort(lambda x, y: cmp(x.date, y.date))
124
125    return changesets
126
127
128class DarcsWorkingDir(UpdatableSourceWorkingDir,SyncronizableTargetWorkingDir):
129    """
130    A working directory under ``darcs``.
131    """
132
133    ## UpdatableSourceWorkingDir
134
135    def _getUpstreamChangesets(self, sincerev):
136        """
137        Do the actual work of fetching the upstream changeset.
138        """
139
140        from datetime import datetime
141        from time import strptime
142        from changes import Changeset
143
144        cmd = [self.repository.DARCS_CMD, "pull", "--dry-run"]
145        pull = ExternalCommand(cwd=self.basedir, command=cmd)
146        output = pull.execute(self.repository.repository,
147                              stdout=PIPE, stderr=STDOUT, TZ='UTC')[0]
148
149        if pull.exit_status:
150            raise GetUpstreamChangesetsFailure(
151                "%s returned status %d saying \"%s\"" %
152                (str(pull), pull.exit_status, output.read()))
153
154        l = output.readline()
155        while l and not (l.startswith('Would pull the following changes:') or
156                         l == 'No remote changes to pull in!\n'):
157            l = output.readline()
158
159        changesets = []
160
161        if l <> 'No remote changes to pull in!\n':
162            ## Sat Jul 17 01:22:08 CEST 2004  lele@nautilus
163            ##   * Refix _getUpstreamChangesets for darcs
164
165            l = output.readline()
166            while not l.startswith('Making no changes:  this is a dry run.'):
167                # Assume it's a line like
168                #    Sun Jan  2 00:24:04 UTC 2005  lele@nautilus.homeip.net
169                # we used to split on the double space before the email,
170                # but in this case this is wrong. Waiting for xml output,
171                # is it really sane asserting date's length to 28 chars?
172                date = l[:28]
173                author = l[30:-1]
174                y,m,d,hh,mm,ss,d1,d2,d3 = strptime(date, "%a %b %d %H:%M:%S %Z %Y")
175                date = datetime(y,m,d,hh,mm,ss)
176                l = output.readline()
177                assert (l.startswith('  * ') or
178                        l.startswith('  UNDO:') or
179                        l.startswith('  tagged'))
180
181                if l.startswith('  *'):
182                    name = l[4:-1]
183                else:
184                    name = l[2:-1]
185
186                changelog = []
187                l = output.readline()
188                while l.startswith(' '):
189                    changelog.append(l.strip())
190                    l = output.readline()
191
192                changesets.append(Changeset(name, date, author, '\n'.join(changelog)))
193
194                while not l.strip():
195                    l = output.readline()
196
197        return changesets
198
199    def _applyChangeset(self, changeset):
200        """
201        Do the actual work of applying the changeset to the working copy.
202        """
203
204        from re import escape
205
206        needspatchesopt = False
207        if hasattr(changeset, 'darcs_hash'):
208            selector = '--match'
209            revtag = 'hash ' + changeset.darcs_hash
210        elif changeset.revision.startswith('tagged '):
211            selector = '--tag'
212            revtag = changeset.revision[7:]
213        else:
214            selector = '--match'
215            revtag = 'date "%s" && author "%s"' % (
216                changeset.date.strftime("%a %b %d %H:%M:%S UTC %Y"),
217                changeset.author)
218            # The 'exact' matcher doesn't groke double quotes:
219            # """currently there is no provision for escaping a double
220            # quote, so you have to choose between matching double
221            # quotes and matching spaces"""
222            if not '"' in changeset.revision:
223                revtag += '&& exact "%s"' % changeset.revision
224            else:
225                needspatchesopt = True
226
227        cmd = [self.repository.DARCS_CMD, "pull", "--all", "--quiet",
228               selector, revtag]
229
230        if needspatchesopt:
231            cmd.extend(['--patches', escape(changeset.revision)])
232
233        pull = ExternalCommand(cwd=self.basedir, command=cmd)
234        output = pull.execute(stdout=PIPE, stderr=STDOUT)[0]
235
236        if pull.exit_status:
237            raise ChangesetApplicationFailure(
238                "%s returned status %d saying \"%s\"" %
239                (str(pull), pull.exit_status, output.read()))
240
241        conflicts = []
242        line = output.readline()
243        while line:
244            if line.startswith('We have conflicts in the following files:'):
245                files = output.readline()[:-1].split('./')[1:]
246                self.log_info("Conflict after 'darcs pull': '%s'" %
247                              ' '.join(files))
248                conflicts.extend(['./' + f for f in files])
249            line = output.readline()
250
251        cmd = [self.repository.DARCS_CMD, "changes", selector, revtag,
252               "--xml-output", "--summ"]
253        changes = ExternalCommand(cwd=self.basedir, command=cmd)
254        last = changesets_from_darcschanges(changes.execute(stdout=PIPE)[0])
255        if last:
256            changeset.entries.extend(last[0].entries)
257
258        return conflicts
259
260    def _handleConflict(self, changeset, conflicts, conflict):
261        """
262        Handle the conflict raised by the application of the upstream changeset.
263
264        Override parent behaviour: with darcs, we need to execute a revert
265        on the conflicted files, **trashing** local changes, but there should
266        be none of them in tailor context.
267        """
268
269        self.log_info("Reverting changes to '%s', to solve the conflict" %
270                      ' '.join(conflict))
271        cmd = [self.repository.DARCS_CMD, "revert", "--all"]
272        revert = ExternalCommand(cwd=self.basedir, command=cmd)
273        revert.execute(conflict)
274
275    def _checkoutUpstreamRevision(self, revision):
276        """
277        Concretely do the checkout of the upstream revision and return
278        the last applied changeset.
279        """
280
281        from os.path import join, exists
282        from os import mkdir
283        from re import escape
284
285        if revision == 'INITIAL':
286            initial = True
287            cmd = [self.repository.DARCS_CMD, "changes", "--xml-output",
288                   "--repo", self.repository.repository]
289            changes = ExternalCommand(command=cmd)
290            output = changes.execute(stdout=PIPE, stderr=STDOUT)[0]
291
292            if changes.exit_status:
293                raise ChangesetApplicationFailure(
294                    "%s returned status %d saying \"%s\"" %
295                    (str(changes), changes.exit_status,
296                     output and output.read() or ''))
297
298            csets = changesets_from_darcschanges(output)
299            changeset = csets[0]
300
301            revision = 'hash %s' % changeset.darcs_hash
302        else:
303            initial = False
304
305        if self.repository.subdir == '.':
306            # This is currently *very* slow, compared to the darcs get
307            # below!
308            if not exists(join(self.basedir, '_darcs')):
309                if not exists(self.basedir):
310                    mkdir(self.basedir)
311
312                init = ExternalCommand(cwd=self.basedir,
313                                       command=[self.repository.DARCS_CMD,
314                                                "initialize"])
315                init.execute()
316
317                if init.exit_status:
318                    raise TargetInitializationFailure(
319                        "%s returned status %s" % (str(init),
320                                                   init.exit_status))
321
322                cmd = [self.repository.DARCS_CMD, "pull", "--all", "--quiet"]
323                if revision and revision<>'HEAD':
324                    cmd.extend([initial and "--match" or "--tag", revision])
325                dpull = ExternalCommand(cwd=self.basedir, command=cmd)
326                output = dpull.execute(self.repository.repository,
327                                       stdout=PIPE, stderr=STDOUT)[0]
328
329                if dpull.exit_status:
330                    raise TargetInitializationFailure(
331                        "%s returned status %d saying \"%s\"" %
332                        (str(dpull), dpull.exit_status, output.read()))
333        else:
334            # Use much faster 'darcs get'
335            cmd = [self.repository.DARCS_CMD, "get", "--partial", "--quiet"]
336            if revision and revision<>'HEAD':
337                cmd.extend([initial and "--to-match" or "--tag", revision])
338            dget = ExternalCommand(command=cmd)
339            output = dget.execute(self.repository.repository, self.basedir,
340                                  stdout=PIPE, stderr=STDOUT)[0]
341
342            if dget.exit_status:
343                raise TargetInitializationFailure(
344                    "%s returned status %d saying \"%s\"" %
345                    (str(dget), dget.exit_status, output.read()))
346
347        cmd = [self.repository.DARCS_CMD, "changes", "--last", "1",
348               "--xml-output"]
349        changes = ExternalCommand(cwd=self.basedir, command=cmd)
350        output = changes.execute(stdout=PIPE, stderr=STDOUT)[0]
351
352        if changes.exit_status:
353            raise ChangesetApplicationFailure(
354                "%s returned status %d saying \"%s\"" %
355                (str(changes), changes.exit_status, output.read()))
356
357        last = changesets_from_darcschanges(output)
358
359        return last[0]
360
361
362    ## SyncronizableTargetWorkingDir
363
364    def _addPathnames(self, names):
365        """
366        Add some new filesystems objects.
367        """
368
369        cmd = [self.repository.DARCS_CMD, "add", "--case-ok",
370               "--not-recursive", "--quiet"]
371        ExternalCommand(cwd=self.basedir, command=cmd).execute(names)
372
373    def _addSubtree(self, subdir):
374        """
375        Use the --recursive variant of ``darcs add`` to add a subtree.
376        """
377
378        cmd = [self.repository.DARCS_CMD, "add", "--case-ok", "--recursive",
379               "--quiet"]
380        ExternalCommand(cwd=self.basedir, command=cmd).execute(subdir)
381
382    def _commit(self, date, author, patchname, changelog=None, entries=None):
383        """
384        Commit the changeset.
385        """
386
387        from sys import getdefaultencoding
388
389        encoding = ExternalCommand.FORCE_ENCODING or getdefaultencoding()
390
391        logmessage = []
392
393        logmessage.append(date.strftime('%Y/%m/%d %H:%M:%S UTC'))
394        logmessage.append(author.encode(encoding))
395        logmessage.append(patchname and patchname.encode(encoding) or 'Unnamed patch')
396        logmessage.append(changelog and changelog.encode(encoding) or '')
397        logmessage.append('')
398
399        cmd = [self.repository.DARCS_CMD, "record", "--all", "--pipe"]
400        if not entries:
401            entries = ['.']
402
403        record = ExternalCommand(cwd=self.basedir, command=cmd)
404        record.execute(entries, input='\n'.join(logmessage))
405
406        if record.exit_status:
407            raise ChangesetApplicationFailure(
408                "%s returned status %d" % (str(record), record.exit_status))
409
410    def _removePathnames(self, names):
411        """
412        Remove some filesystem object.
413        """
414
415        from os.path import join, exists
416
417        # darcs raises status 512 when it does not finding the entry,
418        # removed by source. Since sometime a directory is left there
419        # because it's not empty, darcs fails. So, do an explicit
420        # remove on items that are still there.
421
422        c = ExternalCommand(cwd=self.basedir,
423                            command=[self.repository.DARCS_CMD, "remove"])
424        c.execute([n for n in names if exists(join(self.basedir, n))])
425
426
427    def _renamePathname(self, oldname, newname):
428        """
429        Rename a filesystem object.
430        """
431
432        from os.path import join, exists
433        from os import rename
434
435        # Check to see if the oldentry is still there. If it does,
436        # that probably means one thing: it's been moved and then
437        # replaced, see svn 'R' event. In this case, rename the
438        # existing old entry to something else to trick "darcs mv"
439        # (that will assume the move was already done manually) and
440        # finally restore its name.
441
442        renamed = exists(join(self.basedir, oldname))
443        if renamed:
444            rename(oldname, oldname + '-TAILOR-HACKED-TEMP-NAME')
445
446        try:
447            cmd = [self.repository.DARCS_CMD, "mv"]
448            ExternalCommand(cwd=self.basedir, command=cmd).execute(oldname,
449                                                                   newname)
450        finally:
451            if renamed:
452                rename(oldname + '-TAILOR-HACKED-TEMP-NAME', oldname)
453
454    def _prepareTargetRepository(self):
455        """
456        Create the base directory if it doesn't exist, and execute
457        ``darcs initialize`` if needed.
458        """
459
460        from os import makedirs
461        from os.path import join, exists
462
463        if not exists(self.basedir):
464            makedirs(self.basedir)
465
466        if not exists(join(self.basedir, self.repository.METADIR)):
467            init = ExternalCommand(cwd=self.basedir,
468                                   command=[self.repository.DARCS_CMD,
469                                            "initialize"])
470            init.execute()
471
472            if init.exit_status:
473                raise TargetInitializationFailure(
474                    "%s returned status %s" % (str(init), init.exit_status))
475
476    def _prepareWorkingDirectory(self, source_repo):
477        """
478        Tweak the default settings of the repository.
479        """
480
481        from os.path import join
482        from re import escape
483        from dualwd import IGNORED_METADIRS
484
485        motd = open(join(self.basedir, '_darcs/prefs/motd'), 'w')
486        motd.write(MOTD % str(source_repo))
487        motd.close()
488
489        # Remove .cvsignore from default boring file
490        boring = open(join(self.basedir, '_darcs/prefs/boring'), 'r')
491        ignored = [line for line in boring if line <> '\.cvsignore$\n']
492        boring.close()
493
494        # Augment the boring file, that contains a regexp per line
495        # with all known VCs metadirs to be skipped.
496        boring = open(join(self.basedir, '_darcs/prefs/boring'), 'w')
497        boring.write(''.join(ignored))
498        boring.write('\n'.join(['(^|/)%s($|/)' % escape(md)
499                                for md in IGNORED_METADIRS]))
500        boring.write('\n')
501        if self.logfile.startswith(self.basedir):
502            boring.write('^')
503            boring.write(self.logfile[len(self.basedir)+1:])
504            boring.write('$\n')
505        if self.state_file.filename.startswith(self.basedir):
506            sfrelname = self.state_file.filename[len(self.basedir)+1:]
507            boring.write('^')
508            boring.write(sfrelname)
509            boring.write('$\n')
510            boring.write('^')
511            boring.write(sfrelname+'.journal')
512            boring.write('$\n')
513        boring.close()
Note: See TracBrowser for help on using the repository browser.