source: tailor/vcpx/repository/darcs/target.py @ 1684

Revision 1684, 15.7 KB checked in by lele@…, 11 months ago (diff)

Better check against empty log messages in darcs target

Line 
1# -*- mode: python; coding: utf-8 -*-
2# :Progetto: Tailor -- Darcs peculiarities when used as a target
3# :Creato:   lun 10 lug 2006 00:12:15 CEST
4# :Autore:   Lele Gaifax <lele@nautilus.homeip.net>
5# :Licenza:  GNU General Public License
6#
7
8"""
9This module contains the target specific bits of the darcs backend.
10"""
11
12__docformat__ = 'reStructuredText'
13
14from os.path import join, exists
15import re
16
17from vcpx.shwrap import ExternalCommand, PIPE, STDOUT
18from vcpx.target import ChangesetReplayFailure, SynchronizableTargetWorkingDir, \
19                        PostCommitCheckFailure
20from vcpx.tzinfo import UTC
21
22
23MOTD = """\
24Tailorized equivalent of
25%s
26"""
27
28
29class DarcsTargetWorkingDir(SynchronizableTargetWorkingDir):
30    """
31    A target working directory under ``darcs``.
32    """
33
34    def importFirstRevision(self, source_repo, changeset, initial):
35        from os import walk, sep
36        from vcpx.dualwd import IGNORED_METADIRS
37
38        if not self.repository.split_initial_import_level:
39            super(DarcsTargetWorkingDir, self).importFirstRevision(
40                source_repo, changeset, initial)
41        else:
42            cmd = self.repository.command("add", "--case-ok", "--quiet")
43            add = ExternalCommand(cwd=self.repository.basedir, command=cmd)
44            cmd = self.repository.command("add", "--case-ok", "--recursive",
45                                          "--quiet")
46            addrecurs = ExternalCommand(cwd=self.repository.basedir, command=cmd)
47            for root, dirs, files in walk(self.repository.basedir):
48                subtree = root[len(self.repository.basedir)+1:]
49                if subtree:
50                    log = "Import of subtree %s" % subtree
51                    level = len(subtree.split(sep))
52                else:
53                    log = "Import of first level"
54                    level = 0
55                for excd in IGNORED_METADIRS:
56                    if excd in dirs:
57                        dirs.remove(excd)
58                if level>self.repository.split_initial_import_level:
59                    while dirs:
60                        d = dirs.pop(0)
61                        addrecurs.execute(join(subtree, d))
62                    filenames = [join(subtree, f) for f in files]
63                    if filenames:
64                        add.execute(*filenames)
65                else:
66                    dirnames = [join(subtree, d) for d in dirs]
67                    if dirnames:
68                        add.execute(*dirnames)
69                    filenames = [join(subtree, f) for f in files]
70                    if filenames:
71                        add.execute(*filenames)
72                self._commit(changeset.date, "tailor", "Initial import",
73                             log, isinitialcommit=initial)
74
75            cmd = self.repository.command("tag", "--author", "tailor")
76            ExternalCommand(cwd=self.repository.basedir, command=cmd).execute(
77                "Initial import from %s" % source_repo.repository)
78
79    def _addPathnames(self, names):
80        """
81        Add some new filesystem objects.
82        """
83
84        cmd = self.repository.command("add", "--case-ok", "--not-recursive",
85                                      "--quiet")
86        ExternalCommand(cwd=self.repository.basedir, command=cmd).execute(names)
87
88    def _addSubtree(self, subdir):
89        """
90        Use the --recursive variant of ``darcs add`` to add a subtree.
91        """
92
93        cmd = self.repository.command("add", "--case-ok", "--recursive",
94                                      "--quiet")
95        add = ExternalCommand(cwd=self.repository.basedir, command=cmd,
96                              ok_status=(0,2))
97        output = add.execute(subdir, stdout=PIPE, stderr=STDOUT)[0]
98        if add.exit_status and add.exit_status!=2:
99            self.log.warning("%s returned status %d, saying %s",
100                             str(add), add.exit_status, output.read())
101
102    def _commit(self, date, author, patchname, changelog=None, entries=None,
103                tags = [], isinitialcommit = False):
104        """
105        Commit the changeset.
106        """
107
108        from os import rename, unlink
109
110        logmessage = []
111
112        logmessage.append(date.astimezone(UTC).strftime('%Y/%m/%d %H:%M:%S UTC'))
113        # Paranoid protection against newlines in author
114        logmessage.append(''.join(author.split('\n')))
115        # Patchname cannot start with a newline
116        patchname = patchname.lstrip('\n')
117        if patchname:
118            logmessage.append(patchname)
119        else:
120            # This is possibile also when REMOVE_FIRST_LOG_LINE is in
121            # effect and the changelog starts with newlines: discard
122            # those, otherwise darcs will complain about invalid patch
123            # name
124            if changelog:
125                while changelog.startswith('\n'):
126                    changelog = changelog[1:]
127            if not changelog:
128                # No patch name and no changelog: force non empty one
129                logmessage.append(' ')
130        if changelog:
131            logmessage.append(changelog)
132
133        cmd = self.repository.command("record", "--all", "--pipe", "--ignore-times")
134        if not entries:
135            entries = ['.']
136
137        record = ExternalCommand(cwd=self.repository.basedir, command=cmd)
138        output = record.execute(input=self.repository.encode('\n'.join(logmessage)),
139                                stdout=PIPE, stderr=STDOUT)[0]
140
141        # Repair afterwards, for http://bugs.darcs.net/issue693
142        #
143        # Verified that this is still needed for darcs 2.1.2 (+ 343 patches)
144        # using the config.tailor file that is attached to issue693 above.
145        if record.exit_status == 2:
146            self.log.debug("Trying to repair record failure...")
147            cmd = self.repository.command("repair")
148            repair = ExternalCommand(cwd=self.repository.basedir, command=cmd)
149            repairoutput = repair.execute(stdout=PIPE, stderr=STDOUT)[0]
150            if not repair.exit_status:
151                record.exit_status = repair.exit_status
152            else:
153                self.log.warning("%s returned status %d, saying %s",
154                                 str(repair), repair.exit_status,
155                                 repairoutput.read())
156
157        if record.exit_status:
158            pending = join(self.repository.metadir, 'patches', 'pending')
159            if exists(pending):
160                wrongpending = pending + '.wrong'
161                if exists(wrongpending):
162                    unlink(wrongpending)
163                rename(pending, wrongpending)
164                self.log.debug("Pending file renamed to %s", wrongpending)
165            raise ChangesetReplayFailure(
166                "%s returned status %d, saying: %s" % (str(record),
167                                                       record.exit_status,
168                                                       output.read()))
169
170    def _postCommitCheck(self):
171        # If we are using --look-for-adds on commit this is useless
172        if not self.repository.use_look_for_adds:
173            cmd = self.repository.command("whatsnew", "--summary", "--look-for-add")
174            whatsnew = ExternalCommand(cwd=self.repository.basedir, command=cmd, ok_status=(1,))
175            output = whatsnew.execute(stdout=PIPE, stderr=STDOUT)[0]
176            if not whatsnew.exit_status:
177                raise PostCommitCheckFailure(
178                    "Changes left in working dir after commit:\n%s" % output.read())
179
180    def _replayChangeset(self, changeset):
181        """
182        Instead of using the "darcs mv" command, manually add
183        the rename to the pending file: this is a dirty trick, that
184        allows darcs to handle the case when the source changeset
185        is something like::
186          $ bzr mv A B
187          $ touch A
188          $ bzr add A
189        where A is actually replaced, and old A is now B. Since by the
190        time the changeset gets replayed, the source has already replaced
191        A with its new content, darcs would move the *wrong* A to B...
192        """
193
194        # The "_darcs/patches/pending" file is basically a patch containing
195        # only the changes (hunks, adds...) not yet recorded by darcs: it does
196        # contain either a single change (that is, exactly one line), or a
197        # collection of changes, with opening and closing curl braces.
198        # Filenames must begin with "./", and eventual spaces replaced by '\32\'.
199        # Order is significant!
200
201        pending = join(self.repository.metadir, 'patches', 'pending')
202        if exists(pending):
203            p = open(pending).readlines()
204            if p[0] != '{\n':
205                p.insert(0, '{\n')
206                p.append('}\n')
207        else:
208            p = [ '{\n', '}\n' ]
209
210        entries = []
211
212        while changeset.entries:
213            e = changeset.entries.pop(0)
214            if e.action_kind == e.DELETED:
215                elide = False
216                for j,oe in enumerate(changeset.entries):
217                    if oe.action_kind == oe.ADDED and e.name == oe.name:
218                        self.log.debug('Collapsing a %s and a %s on %s, assuming '
219                                       'an upstream "replacement"',
220                                       e.action_kind, oe.action_kind, oe.name)
221                        del changeset.entries[j]
222                        elide = True
223                        break
224                if not elide:
225                    entries.append(e)
226            elif e.action_kind == e.ADDED:
227                elide = False
228                for j,oe in enumerate(changeset.entries):
229                    if oe.action_kind == oe.DELETED and e.name == oe.name:
230                        self.log.debug('Collapsing a %s and a %s on %s, assuming '
231                                       'an upstream "replacement"',
232                                       e.action_kind, oe.action_kind, oe.name)
233                        del changeset.entries[j]
234                        elide = True
235                        break
236                if not elide:
237                    entries.append(e)
238            else:
239                entries.append(e)
240
241        changed = False
242        for e in entries:
243            if e.action_kind == e.RENAMED:
244                self.log.debug('Mimicing "darcs mv %s %s"',
245                               e.old_name, e.name)
246                oname = e.old_name.replace(' ', '\\32\\')
247                nname = e.name.replace(' ', '\\32\\')
248                p.insert(-1, 'move ./%s ./%s\n' % (oname, nname))
249                changed = True
250            elif e.action_kind == e.ADDED:
251                self.log.debug('Mimicing "darcs add %s"', e.name)
252                name = e.name.replace(' ', '\\32\\')
253                if e.is_directory:
254                    p.insert(-1, 'adddir ./%s\n' % name)
255                else:
256                    p.insert(-1, 'addfile ./%s\n' % name)
257                changed = True
258            elif e.action_kind == e.DELETED:
259                self.log.debug('Mimicing "darcs rm %s"', e.name)
260                name = e.name.replace(' ', '\\32\\')
261                if e.is_directory:
262                    p.insert(-1, 'rmdir ./%s\n' % name)
263                else:
264                    p.insert(-1, 'rmfile ./%s\n' % name)
265                changed = True
266        if changed:
267            open(pending, 'w').writelines(p)
268        return True
269
270    def _prepareTargetRepository(self):
271        """
272        Create the base directory if it doesn't exist, and execute
273        ``darcs initialize`` if needed.
274        """
275
276        if not exists(self.repository.metadir):
277            self.repository.create()
278
279        prefsdir = join(self.repository.metadir, 'prefs')
280        prefsname = join(prefsdir, 'prefs')
281        boringname = join(prefsdir, 'boring')
282        if exists(prefsname):
283            for pref in open(prefsname, 'rU'):
284                if pref:
285                    pname, pvalue = pref.split(' ', 1)
286                    if pname == 'boringfile':
287                        boringname = join(self.repository.basedir, pvalue[:-1])
288
289        boring = open(boringname, 'rU')
290        ignored = boring.read().rstrip().split('\n')
291        boring.close()
292
293        # Build a list of compiled regular expressions, that will be
294        # used later to filter the entries.
295        self.__unwanted_entries = [re.compile(rx) for rx in ignored
296                                   if rx and not rx.startswith('#')]
297
298    def _prepareWorkingDirectory(self, source_repo):
299        """
300        Tweak the default settings of the repository.
301        """
302
303        motd = open(join(self.repository.metadir, 'prefs', 'motd'), 'w')
304        motd.write(MOTD % str(source_repo))
305        motd.close()
306
307    def _adaptEntries(self, changeset):
308        """
309        Filter out boring files.
310        """
311
312        from copy import copy
313
314        adapted = SynchronizableTargetWorkingDir._adaptEntries(self, changeset)
315
316        # If there are no entries or no rules, there's nothing to do
317        if not adapted or not adapted.entries or not self.__unwanted_entries:
318            return adapted
319
320        entries = []
321        skipped = False
322        for e in adapted.entries:
323            skip = False
324            for rx in self.__unwanted_entries:
325                if rx.search(e.name):
326                    skip = True
327                    break
328            if skip:
329                self.log.info('Entry "%s" skipped per boring rules', e.name)
330                skipped = True
331            else:
332                entries.append(e)
333
334        # All entries are gone, don't commit this changeset
335        if not entries:
336            self.log.info('All entries ignored, skipping whole '
337                          'changeset "%s"', changeset.revision)
338            return None
339
340        if skipped:
341            adapted = copy(adapted)
342            adapted.entries = entries
343
344        return adapted
345
346    def _tag(self, tag, date, author):
347        """
348        Apply the given tag to the repository, unless it has already
349        been applied to the current state. (If it has been applied to
350        an earlier state, do apply it; the later tag overrides the
351        earlier one.
352        """
353        if tag not in self._currentTags():
354            cmd = self.repository.command("tag", "--author", "Unknown tagger")
355            ExternalCommand(cwd=self.repository.basedir, command=cmd).execute(tag)
356
357    def _currentTags(self):
358        """
359        Return a list of tags that refer to the repository's current
360        state.  Does not consider tags themselves to be part of the
361        state, so if the repo was tagged with T1 and then T2, then
362        both T1 and T2 are considered to refer to the current state,
363        even though 'darcs get --tag=T1' and 'darcs get --tag=T2'
364        would have different results (the latter creates a repo that
365        contains tag T2, but the former does not).
366
367        This function assumes that a tag depends on all patches that
368        precede it in the "darcs changes" list.  This assumption is
369        valid if tags only come into the repository via tailor; if the
370        user applies a tag by hand in the hybrid repository, or pulls
371        in a tag from another darcs repository, then the assumption
372        could be violated and mistagging could result.
373        """
374
375        from vcpx.repository.darcs.source import changesets_from_darcschanges_unsafe
376
377        cmd = self.repository.command("changes",
378                                      "--from-match", "not name ^TAG",
379                                      "--xml-output", "--reverse")
380        changes =  ExternalCommand(cwd=self.repository.basedir, command=cmd)
381        output = changes.execute(stdout=PIPE)[0]
382        if changes.exit_status:
383            raise ChangesetReplayFailure(
384                "%s returned status %d saying\n%s" %
385                (str(changes), changes.exit_status, output.read()))
386
387        tags = []
388        for cs in changesets_from_darcschanges_unsafe(output):
389            for tag in cs.tags:
390                if tag not in tags:
391                    tags.append(tag)
392        return tags
Note: See TracBrowser for help on using the repository browser.