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

Revision 1582, 14.4 KB checked in by lele@…, 5 years ago (diff)

Ouch! The changeset is already adapted at that point...

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
14import re
15
16from vcpx.shwrap import ExternalCommand, PIPE, STDOUT
17from vcpx.target import ChangesetReplayFailure, SynchronizableTargetWorkingDir, \
18                        PostCommitCheckFailure
19from vcpx.tzinfo import UTC
20
21
22MOTD = """\
23Tailorized equivalent of
24%s
25"""
26
27
28class DarcsTargetWorkingDir(SynchronizableTargetWorkingDir):
29    """
30    A target working directory under ``darcs``.
31    """
32
33    def importFirstRevision(self, source_repo, changeset, initial):
34        from os import walk, sep
35        from os.path import join
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        logmessage = []
109
110        logmessage.append(date.astimezone(UTC).strftime('%Y/%m/%d %H:%M:%S UTC'))
111        logmessage.append(author)
112        if patchname:
113            logmessage.append(patchname)
114        else:
115            # This is possibile also when REMOVE_FIRST_LOG_LINE is in
116            # effect and the changelog starts with newlines: discard
117            # those, otherwise darcs will complain about invalid patch
118            # name
119            if changelog and changelog.startswith('\n'):
120                while changelog.startswith('\n'):
121                    changelog = changelog[1:]
122        if changelog:
123            logmessage.append(changelog)
124
125        if not logmessage:
126            logmessage.append('Unnamed patch')
127
128        cmd = self.repository.command("record", "--all", "--pipe")
129        if not entries:
130            entries = ['.']
131
132        record = ExternalCommand(cwd=self.repository.basedir, command=cmd)
133        output = record.execute(input=self.repository.encode('\n'.join(logmessage)),
134                                stdout=PIPE, stderr=STDOUT)[0]
135
136        if record.exit_status:
137            raise ChangesetReplayFailure(
138                "%s returned status %d, saying: %s" % (str(record),
139                                                       record.exit_status,
140                                                       output.read()))
141
142    def _postCommitCheck(self):
143        cmd = self.repository.command("whatsnew", "--summary", "--look-for-add")
144        whatsnew = ExternalCommand(cwd=self.repository.basedir, command=cmd, ok_status=(1,))
145        output = whatsnew.execute(stdout=PIPE, stderr=STDOUT)[0]
146        if not whatsnew.exit_status:
147            raise PostCommitCheckFailure(
148                "Changes left in working dir after commit:\n%s" % output.read())
149
150    def _replayChangeset(self, changeset):
151        """
152        Instead of using the "darcs mv" command, manually add
153        the rename to the pending file: this is a dirty trick, that
154        allows darcs to handle the case when the source changeset
155        is something like::
156          $ bzr mv A B
157          $ touch A
158          $ bzr add A
159        where A is actually replaced, and old A is now B. Since by the
160        time the changeset gets replayed, the source has already replaced
161        A with its new content, darcs would move the *wrong* A to B...
162        """
163
164        from os.path import join, exists
165
166        # The "_darcs/patches/pending" file is basically a patch containing
167        # only the changes (hunks, adds...) not yet recorded by darcs: it does
168        # contain either a single change (that is, exactly one line), or a
169        # collection of changes, with opening and closing curl braces.
170        # Filenames must begin with "./", and eventual spaces replaced by '\32\'.
171        # Order is significant!
172
173        pending = join(self.repository.basedir, '_darcs', 'patches', 'pending')
174        if exists(pending):
175            p = open(pending).readlines()
176            if p[0] != '{\n':
177                p.insert(0, '{\n')
178                p.append('}\n')
179        else:
180            p = [ '{\n', '}\n' ]
181
182        entries = []
183
184        while changeset.entries:
185            e = changeset.entries.pop(0)
186            if e.action_kind == e.DELETED:
187                elide = False
188                for j,oe in enumerate(changeset.entries):
189                    if oe.action_kind == oe.ADDED and e.name == oe.name:
190                        self.log.debug('Collapsing a %s and a %s on %s, assuming '
191                                       'an upstream "replacement"',
192                                       e.action_kind, oe.action_kind, oe.name)
193                        del changeset.entries[j]
194                        elide = True
195                        break
196                if not elide:
197                    entries.append(e)
198            elif e.action_kind == e.ADDED:
199                elide = False
200                for j,oe in enumerate(changeset.entries):
201                    if oe.action_kind == oe.DELETED and e.name == oe.name:
202                        self.log.debug('Collapsing a %s and a %s on %s, assuming '
203                                       'an upstream "replacement"',
204                                       e.action_kind, oe.action_kind, oe.name)
205                        del changeset.entries[j]
206                        elide = True
207                        break
208                if not elide:
209                    entries.append(e)
210            else:
211                entries.append(e)
212
213        changed = False
214        for e in entries:
215            if e.action_kind == e.RENAMED:
216                self.log.debug('Mimicing "darcs mv %s %s"',
217                               e.old_name, e.name)
218                oname = e.old_name.replace(' ', '\\32\\')
219                nname = e.name.replace(' ', '\\32\\')
220                p.insert(-1, 'move ./%s ./%s\n' % (oname, nname))
221                changed = True
222            elif e.action_kind == e.ADDED:
223                self.log.debug('Mimicing "darcs add %s"', e.name)
224                name = e.name.replace(' ', '\\32\\')
225                if e.is_directory:
226                    p.insert(-1, 'adddir ./%s\n' % name)
227                else:
228                    p.insert(-1, 'addfile ./%s\n' % name)
229                changed = True
230            elif e.action_kind == e.DELETED:
231                self.log.debug('Mimicing "darcs rm %s"', e.name)
232                name = e.name.replace(' ', '\\32\\')
233                if e.is_directory:
234                    p.insert(-1, 'rmdir ./%s\n' % name)
235                else:
236                    p.insert(-1, 'rmfile ./%s\n' % name)
237                changed = True
238        if changed:
239            open(pending, 'w').writelines(p)
240        return True
241
242    def _prepareTargetRepository(self):
243        """
244        Create the base directory if it doesn't exist, and execute
245        ``darcs initialize`` if needed.
246        """
247
248        from os.path import join, exists
249
250        metadir = join(self.repository.basedir, '_darcs')
251
252        if not exists(metadir):
253            self.repository.create()
254
255        prefsdir = join(metadir, 'prefs')
256        prefsname = join(prefsdir, 'prefs')
257        boringname = join(prefsdir, 'boring')
258        if exists(prefsname):
259            for pref in open(prefsname, 'rU'):
260                if pref:
261                    pname, pvalue = pref.split(' ', 1)
262                    if pname == 'boringfile':
263                        boringname = join(self.repository.basedir, pvalue[:-1])
264
265        boring = open(boringname, 'rU')
266        ignored = boring.read().rstrip().split('\n')
267        boring.close()
268
269        # Build a list of compiled regular expressions, that will be
270        # used later to filter the entries.
271        self.__unwanted_entries = [re.compile(rx) for rx in ignored
272                                   if rx and not rx.startswith('#')]
273
274    def _prepareWorkingDirectory(self, source_repo):
275        """
276        Tweak the default settings of the repository.
277        """
278
279        from os.path import join
280
281        motd = open(join(self.repository.basedir, '_darcs/prefs/motd'), 'w')
282        motd.write(MOTD % str(source_repo))
283        motd.close()
284
285    def _adaptEntries(self, changeset):
286        """
287        Filter out boring files.
288        """
289
290        from copy import copy
291
292        adapted = SynchronizableTargetWorkingDir._adaptEntries(self, changeset)
293
294        # If there are no entries or no rules, there's nothing to do
295        if not adapted or not adapted.entries or not self.__unwanted_entries:
296            return adapted
297
298        entries = []
299        skipped = False
300        for e in adapted.entries:
301            skip = False
302            for rx in self.__unwanted_entries:
303                if rx.search(e.name):
304                    skip = True
305                    break
306            if skip:
307                self.log.info('Entry "%s" skipped per boring rules', e.name)
308                skipped = True
309            else:
310                entries.append(e)
311
312        # All entries are gone, don't commit this changeset
313        if not entries:
314            self.log.info('All entries ignored, skipping whole '
315                          'changeset "%s"', changeset.revision)
316            return None
317
318        if skipped:
319            adapted = copy(adapted)
320            adapted.entries = entries
321
322        return adapted
323
324    def _tag(self, tag, date, author):
325        """
326        Apply the given tag to the repository, unless it has already
327        been applied to the current state. (If it has been applied to
328        an earlier state, do apply it; the later tag overrides the
329        earlier one.
330        """
331        if tag not in self._currentTags():
332            cmd = self.repository.command("tag", "--author", "Unknown tagger")
333            ExternalCommand(cwd=self.repository.basedir, command=cmd).execute(tag)
334
335    def _currentTags(self):
336        """
337        Return a list of tags that refer to the repository's current
338        state.  Does not consider tags themselves to be part of the
339        state, so if the repo was tagged with T1 and then T2, then
340        both T1 and T2 are considered to refer to the current state,
341        even though 'darcs get --tag=T1' and 'darcs get --tag=T2'
342        would have different results (the latter creates a repo that
343        contains tag T2, but the former does not).
344
345        This function assumes that a tag depends on all patches that
346        precede it in the "darcs changes" list.  This assumption is
347        valid if tags only come into the repository via tailor; if the
348        user applies a tag by hand in the hybrid repository, or pulls
349        in a tag from another darcs repository, then the assumption
350        could be violated and mistagging could result.
351        """
352
353        from vcpx.repository.darcs.source import changesets_from_darcschanges_unsafe
354
355        cmd = self.repository.command("changes",
356                                      "--from-match", "not name ^TAG",
357                                      "--xml-output", "--reverse")
358        changes =  ExternalCommand(cwd=self.repository.basedir, command=cmd)
359        output = changes.execute(stdout=PIPE, stderr=STDOUT)[0]
360        if changes.exit_status:
361            raise ChangesetReplayFailure(
362                "%s returned status %d saying\n%s" %
363                (str(changes), changes.exit_status, output.read()))
364
365        tags = []
366        for cs in changesets_from_darcschanges_unsafe(output):
367            for tag in cs.tags:
368                if tag not in tags:
369                    tags.append(tag)
370        return tags
Note: See TracBrowser for help on using the repository browser.