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

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

Move common import stmt at top level

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        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", "--ignore-times")
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        # If we are using --look-for-adds on commit this is useless
144        if not self.repository.use_look_for_adds:
145            cmd = self.repository.command("whatsnew", "--summary", "--look-for-add")
146            whatsnew = ExternalCommand(cwd=self.repository.basedir, command=cmd, ok_status=(1,))
147            output = whatsnew.execute(stdout=PIPE, stderr=STDOUT)[0]
148            if not whatsnew.exit_status:
149                raise PostCommitCheckFailure(
150                    "Changes left in working dir after commit:\n%s" % output.read())
151
152    def _replayChangeset(self, changeset):
153        """
154        Instead of using the "darcs mv" command, manually add
155        the rename to the pending file: this is a dirty trick, that
156        allows darcs to handle the case when the source changeset
157        is something like::
158          $ bzr mv A B
159          $ touch A
160          $ bzr add A
161        where A is actually replaced, and old A is now B. Since by the
162        time the changeset gets replayed, the source has already replaced
163        A with its new content, darcs would move the *wrong* A to B...
164        """
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        metadir = join(self.repository.basedir, '_darcs')
249
250        if not exists(metadir):
251            self.repository.create()
252
253        prefsdir = join(metadir, 'prefs')
254        prefsname = join(prefsdir, 'prefs')
255        boringname = join(prefsdir, 'boring')
256        if exists(prefsname):
257            for pref in open(prefsname, 'rU'):
258                if pref:
259                    pname, pvalue = pref.split(' ', 1)
260                    if pname == 'boringfile':
261                        boringname = join(self.repository.basedir, pvalue[:-1])
262
263        boring = open(boringname, 'rU')
264        ignored = boring.read().rstrip().split('\n')
265        boring.close()
266
267        # Build a list of compiled regular expressions, that will be
268        # used later to filter the entries.
269        self.__unwanted_entries = [re.compile(rx) for rx in ignored
270                                   if rx and not rx.startswith('#')]
271
272    def _prepareWorkingDirectory(self, source_repo):
273        """
274        Tweak the default settings of the repository.
275        """
276
277        motd = open(join(self.repository.basedir, '_darcs/prefs/motd'), 'w')
278        motd.write(MOTD % str(source_repo))
279        motd.close()
280
281    def _adaptEntries(self, changeset):
282        """
283        Filter out boring files.
284        """
285
286        from copy import copy
287
288        adapted = SynchronizableTargetWorkingDir._adaptEntries(self, changeset)
289
290        # If there are no entries or no rules, there's nothing to do
291        if not adapted or not adapted.entries or not self.__unwanted_entries:
292            return adapted
293
294        entries = []
295        skipped = False
296        for e in adapted.entries:
297            skip = False
298            for rx in self.__unwanted_entries:
299                if rx.search(e.name):
300                    skip = True
301                    break
302            if skip:
303                self.log.info('Entry "%s" skipped per boring rules', e.name)
304                skipped = True
305            else:
306                entries.append(e)
307
308        # All entries are gone, don't commit this changeset
309        if not entries:
310            self.log.info('All entries ignored, skipping whole '
311                          'changeset "%s"', changeset.revision)
312            return None
313
314        if skipped:
315            adapted = copy(adapted)
316            adapted.entries = entries
317
318        return adapted
319
320    def _tag(self, tag, date, author):
321        """
322        Apply the given tag to the repository, unless it has already
323        been applied to the current state. (If it has been applied to
324        an earlier state, do apply it; the later tag overrides the
325        earlier one.
326        """
327        if tag not in self._currentTags():
328            cmd = self.repository.command("tag", "--author", "Unknown tagger")
329            ExternalCommand(cwd=self.repository.basedir, command=cmd).execute(tag)
330
331    def _currentTags(self):
332        """
333        Return a list of tags that refer to the repository's current
334        state.  Does not consider tags themselves to be part of the
335        state, so if the repo was tagged with T1 and then T2, then
336        both T1 and T2 are considered to refer to the current state,
337        even though 'darcs get --tag=T1' and 'darcs get --tag=T2'
338        would have different results (the latter creates a repo that
339        contains tag T2, but the former does not).
340
341        This function assumes that a tag depends on all patches that
342        precede it in the "darcs changes" list.  This assumption is
343        valid if tags only come into the repository via tailor; if the
344        user applies a tag by hand in the hybrid repository, or pulls
345        in a tag from another darcs repository, then the assumption
346        could be violated and mistagging could result.
347        """
348
349        from vcpx.repository.darcs.source import changesets_from_darcschanges_unsafe
350
351        cmd = self.repository.command("changes",
352                                      "--from-match", "not name ^TAG",
353                                      "--xml-output", "--reverse")
354        changes =  ExternalCommand(cwd=self.repository.basedir, command=cmd)
355        output = changes.execute(stdout=PIPE)[0]
356        if changes.exit_status:
357            raise ChangesetReplayFailure(
358                "%s returned status %d saying\n%s" %
359                (str(changes), changes.exit_status, output.read()))
360
361        tags = []
362        for cs in changesets_from_darcschanges_unsafe(output):
363            for tag in cs.tags:
364                if tag not in tags:
365                    tags.append(tag)
366        return tags
Note: See TracBrowser for help on using the repository browser.