source: tailor/vcpx/darcs.py @ 764

Revision 764, 20.6 KB checked in by lele@…, 8 years ago (diff)

Skip entries matched by boring regular expressions

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    """
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("%Y%m%d%H%M%S"),
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.replace('%', '%%')
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        if changelog:
397            logmessage.append(changelog and changelog.encode(encoding))
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 is,
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        absold = join(self.basedir, oldname)
443        renamed = exists(absold)
444        if renamed:
445            rename(absold, absold + '-TAILOR-HACKED-TEMP-NAME')
446
447        try:
448            cmd = [self.repository.DARCS_CMD, "mv"]
449            ExternalCommand(cwd=self.basedir, command=cmd).execute(oldname,
450                                                                   newname)
451        finally:
452            if renamed:
453                rename(absold + '-TAILOR-HACKED-TEMP-NAME', absold)
454
455    def _prepareTargetRepository(self):
456        """
457        Create the base directory if it doesn't exist, and execute
458        ``darcs initialize`` if needed.
459        """
460
461        from os.path import join, exists
462        from re import escape, compile
463        from dualwd import IGNORED_METADIRS
464
465        if not exists(join(self.basedir, self.repository.METADIR)):
466            init = ExternalCommand(cwd=self.basedir,
467                                   command=[self.repository.DARCS_CMD,
468                                            "initialize"])
469            init.execute()
470
471            if init.exit_status:
472                raise TargetInitializationFailure(
473                    "%s returned status %s" % (str(init), init.exit_status))
474
475            boring = open(join(self.basedir, '_darcs/prefs/boring'), 'rU')
476            ignored = [line[:-1] for line in boring]
477            boring.close()
478
479            # Augment the boring file, that contains a regexp per line
480            # with all known VCs metadirs to be skipped.
481            ignored.extend(['(^|/)%s($|/)' % escape(md)
482                            for md in IGNORED_METADIRS])
483
484            # Eventually omit our own log...
485            logfile = self.repository.project.logfile
486            if logfile.startswith(self.basedir):
487                ignored.append('^%s$' %
488                               escape(logfile[len(self.basedir)+1:]))
489
490            # ... and state file
491            sfname = self.repository.project.state_file.filename
492            if sfname.startswith(self.basedir):
493                sfrelname = sfname[len(self.basedir)+1:]
494                ignored.append('^%s$' % escape(sfrelname))
495                ignored.append('^%s$' % escape(sfrelname+'.journal'))
496
497            boring = open(join(self.basedir, '_darcs/prefs/boring'), 'wU')
498            boring.write('\n'.join(ignored))
499            boring.close()
500        else:
501            boring = open(join(self.basedir, '_darcs/prefs/boring'), 'rU')
502            ignored = [line[:-1] for line in boring]
503            boring.close()
504
505        # Build a list of compiled regular expressions, that will be
506        # used later to filter the entries.
507        self.__unwanted_entries = [compile(rx) for rx in ignored
508                                   if rx and not rx.startswith('#')]
509
510    def _prepareWorkingDirectory(self, source_repo):
511        """
512        Tweak the default settings of the repository.
513        """
514
515        from os.path import join
516
517        motd = open(join(self.basedir, '_darcs/prefs/motd'), 'w')
518        motd.write(MOTD % str(source_repo))
519        motd.close()
520
521    def _adaptEntries(self, changeset):
522        """
523        Filter out boring files.
524        """
525
526        from copy import copy
527
528        adapted = SyncronizableTargetWorkingDir._adaptEntries(self, changeset)
529
530        # If there are no entries or no rules, there's nothing to do
531        if not adapted or not adapted.entries or not self.__unwanted_entries:
532            return adapted
533
534        entries = []
535        skipped = False
536        for e in adapted.entries:
537            skip = False
538            for rx in self.__unwanted_entries:
539                if rx.search(e.name):
540                    skip = True
541                    break
542            if skip:
543                self.log_info('Entry %r skipped per boring rules' %
544                              e.name)
545                skipped = True
546            else:
547                entries.append(e)
548
549        # All entries are gone, don't commit this changeset
550        if not entries:
551            self.log_info('All entries ignored, skipping whole '
552                          'changeset %r' % changeset.revision)
553            return None
554
555        if skipped:
556            adapted = copy(adapted)
557            adapted.entries = entries
558
559        return adapted
Note: See TracBrowser for help on using the repository browser.