source: tailor/vcpx/target.py @ 1144

Revision 1144, 17.4 KB checked in by lele@…, 7 years ago (diff)

Fix the grouping loop

Line 
1# -*- mode: python; coding: utf-8 -*-
2# :Progetto: vcpx -- Syncable targets
3# :Creato:   ven 04 giu 2004 00:27:07 CEST
4# :Autore:   Lele Gaifax <lele@nautilus.homeip.net>
5# :Licenza:  GNU General Public License
6#
7
8"""
9Syncronizable targets are the simplest abstract wrappers around a
10working directory under two different version control systems.
11"""
12
13__docformat__ = 'reStructuredText'
14
15import socket
16from signal import signal, SIGINT, SIG_IGN
17from workdir import WorkingDir
18
19HOST = socket.getfqdn()
20AUTHOR = "tailor"
21BOOTSTRAP_PATCHNAME = 'Tailorization'
22BOOTSTRAP_CHANGELOG = """\
23Import of the upstream sources from
24%(source_repository)s
25   Revision: %(revision)s
26"""
27
28class TargetInitializationFailure(Exception):
29    "Failure initializing the target VCS"
30
31class ChangesetReplayFailure(Exception):
32    "Failure replaying the changeset on the target system"
33
34class SynchronizableTargetWorkingDir(WorkingDir):
35    """
36    This is an abstract working dir usable as a *shadow* of another
37    kind of VC, sharing the same working directory.
38
39    Most interesting entry points are:
40
41    replayChangeset
42        to replay an already applied changeset, to mimic the actions
43        performed by the upstream VC system on the tree such as
44        renames, deletions and adds.  This is an useful argument to
45        feed as ``replay`` to ``applyUpstreamChangesets``
46
47    importFirstRevision
48        to initialize a pristine working directory tree under this VC
49        system, possibly extracted under a different kind of VC
50
51    Subclasses MUST override at least the _underscoredMethods.
52    """
53
54    PATCH_NAME_FORMAT = '[%(project)s @ %(revision)s]'
55    """
56    The format string used to compute the patch name, used by underlying VCS.
57    """
58
59    REMOVE_FIRST_LOG_LINE = False
60    """
61    When true, remove the first line from the upstream changelog.
62    """
63
64    def __getPatchNameAndLog(self, changeset):
65        """
66        Return a tuple (patchname, changelog) interpolating changeset's
67        information with the template above.
68        """
69
70        if changeset.log == '':
71            firstlogline = 'Empty log message'
72            remaininglog = ''
73        else:
74            loglines = changeset.log.split('\n')
75            if len(loglines)>1:
76                firstlogline = loglines[0]
77                remaininglog = '\n'.join(loglines[1:])
78            else:
79                firstlogline = changeset.log
80                remaininglog = ''
81
82        patchname = self.PATCH_NAME_FORMAT % {
83            'project': self.repository.projectref().name,
84            'revision': changeset.revision,
85            'author': changeset.author,
86            'date': changeset.date,
87            'firstlogline': firstlogline,
88            'remaininglog': remaininglog}
89        if self.REMOVE_FIRST_LOG_LINE:
90            changelog = remaininglog
91        else:
92            changelog = changeset.log
93        return patchname, changelog
94
95    def replayChangeset(self, changeset):
96        """
97        Do whatever is needed to replay the changes under the target
98        VC, to register the already applied (under the other VC)
99        changeset.
100        """
101
102        try:
103            changeset = self._adaptChangeset(changeset)
104        except:
105            self.log.exception("Failure adapting: %s", str(changeset))
106            raise
107
108        if changeset is None:
109            return
110
111        try:
112            self._replayChangeset(changeset)
113        except:
114            self.log.exception("Failure replaying: %s", str(changeset))
115            raise
116        patchname, log = self.__getPatchNameAndLog(changeset)
117        entries = self._getCommitEntries(changeset)
118        previous = signal(SIGINT, SIG_IGN)
119        try:
120            self._commit(changeset.date, changeset.author, patchname, log,
121                         entries)
122            if changeset.tags:
123                for tag in changeset.tags:
124                    self._tag(tag)
125        finally:
126            signal(SIGINT, previous)
127
128        try:
129            self._dismissChangeset(changeset)
130        except:
131            self.log.exception("Failure dismissing: %s", str(changeset))
132            raise
133
134    def __getPrefixToSource(self):
135        """
136        Compute and return the "offset" between source and target basedirs,
137        or None when not using shared directories, or there's no offset.
138        """
139
140        project = self.repository.projectref()
141        ssubdir = project.source.subdir
142        tsubdir = project.target.subdir
143        if self.shared_basedirs and ssubdir <> tsubdir:
144            if tsubdir == '.':
145                prefix = ssubdir
146            else:
147                if not tsubdir.endswith('/'):
148                    tsubdir += '/'
149                prefix = ssubdir[len(tsubdir):]
150            return prefix
151        else:
152            return None
153
154    def _normalizeEntryPaths(self, entry):
155        """
156        Normalize the name and old_name of an entry.
157
158        The ``name`` and ``old_name`` of an entry are pathnames coming
159        from the upstream system, and is usually (although there is no
160        guarantee it actually is) a UNIX style path with forward
161        slashes "/" as separators.
162
163        This implementation uses normpath to adapt the path to the
164        actual OS convention, but subclasses may eventually override
165        this to use their own canonicalization of ``name`` and
166        ``old_name``.
167        """
168
169        from os.path import normpath
170
171        entry.name = normpath(entry.name)
172        if entry.old_name:
173            entry.old_name = normpath(entry.old_name)
174
175    def __adaptEntriesPath(self, changeset):
176        """
177        If the source basedir is a subdirectory of the target, adjust
178        all the pathnames adding the prefix computed by difference.
179        """
180
181        from copy import deepcopy
182        from os.path import join
183
184        if not changeset.entries:
185            return changeset
186
187        prefix = self.__getPrefixToSource()
188        adapted = deepcopy(changeset)
189        for e in adapted.entries:
190            if prefix:
191                e.name = join(prefix, e.name)
192                if e.old_name:
193                    e.old_name = join(prefix, e.old_name)
194            self._normalizeEntryPaths(e)
195        return adapted
196
197    def _adaptEntries(self, changeset):
198        """
199        Do whatever is needed to adapt entries to the target system.
200
201        This implementation adds a prefix to each path if needed, when
202        the target basedir *contains* the source basedir. Also, each
203        path is normalized thru ``normpath()`` or whatever equivalent
204        operation provided by the specific target. It operates on and
205        returns a copy of the given changeset.
206
207        Subclasses shall eventually extend this to exclude unwanted
208        entries, eventually returning None when all entries were
209        excluded, to avoid the commit on target of an empty changeset.
210        """
211
212        adapted = self.__adaptEntriesPath(changeset)
213        return adapted
214
215    def _adaptChangeset(self, changeset):
216        """
217        Do whatever needed before replay and return the adapted changeset.
218
219        This implementation calls ``self._adaptEntries()``, then
220        executes the adapters defined by before-commit on the project:
221        each adapter is run in turn, and may return False to indicate
222        that the changeset shouldn't be replayed at all. They are
223        otherwise free to alter the changeset in any meaningful way.
224        """
225
226        from copy import copy
227
228        adapted = self._adaptEntries(changeset)
229        if adapted:
230            project = self.repository.projectref()
231            if project.before_commit:
232                adapted = copy(adapted)
233
234                for adapter in project.before_commit:
235                    if not adapter(self, adapted):
236                        return None
237        return adapted
238
239    def _dismissChangeset(self, changeset):
240        """
241        Do whatever needed after commit.
242
243        This execute the adapters defined by after-commit on the project,
244        for example tagging in some way the target repository upon some
245        particular kind of changeset.
246        """
247
248        project = self.repository.projectref()
249        if project.after_commit:
250            for farewell in project.after_commit:
251                farewell(self, changeset)
252
253    def _getCommitEntries(self, changeset):
254        """
255        Extract the names of the entries for the commit phase.
256        """
257
258        return [e.name for e in changeset.entries]
259
260    def _replayChangeset(self, changeset):
261        """
262        Replicate the actions performed by the changeset on the tree of
263        files.
264        """
265
266        from os.path import join, isdir
267        from changes import ChangesetEntry
268
269        added = []
270        actions = { ChangesetEntry.ADDED: self._addEntries,
271                    ChangesetEntry.DELETED: self._removeEntries,
272                    ChangesetEntry.RENAMED: self._renameEntries
273                    }
274
275        # Group the changes by kind and perform the corresponding action
276
277        last = None
278        group = []
279        for e in changeset.entries:
280            if last is None or last.action_kind == e.action_kind:
281                last = e
282                group.append(e)
283            if last.action_kind != e.action_kind:
284                action = actions.get(last.action_kind)
285                if action is not None:
286                    action(group)
287                group = [e]
288                last = e
289            if e.action_kind == e.ADDED:
290                added.append(e)
291        if group:
292            action = actions.get(group[0].action_kind)
293            if action is not None:
294                action(group)
295
296        # Finally, deal with "copied" directories. The simple way is
297        # executing an _addSubtree on each of them, evenif this may
298        # cause "warnings" on items just moved/added above...
299
300        while added:
301            subdir = added.pop(0).name
302            if isdir(join(self.basedir, subdir)):
303                self._addSubtree(subdir)
304                added = [e for e in added if not e.name.startswith(subdir)]
305
306    def _addEntries(self, entries):
307        """
308        Add a sequence of entries
309        """
310
311        self._addPathnames([e.name for e in entries])
312
313    def _addPathnames(self, names):
314        """
315        Add some new filesystem objects.
316        """
317
318        raise "%s should override this method" % self.__class__
319
320    def _addSubtree(self, subdir):
321        """
322        Add a whole subtree.
323
324        This implementation crawl down the whole subtree, adding
325        entries (subdirs, skipping the usual VC-specific control
326        directories such as ``.svn``, ``_darcs`` or ``CVS``, and
327        files).
328
329        Subclasses may use a better way, if the backend implements
330        a recursive add that skips the various metadata directories.
331        """
332
333        from os.path import join
334        from os import walk
335        from dualwd import IGNORED_METADIRS
336
337        exclude = []
338
339        if self.state_file.filename.startswith(self.basedir):
340            sfrelname = self.state_file.filename[len(self.basedir)+1:]
341            exclude.append(sfrelname)
342            exclude.append(sfrelname+'.old')
343            exclude.append(sfrelname+'.journal')
344
345        if self.logfile.startswith(self.basedir):
346            exclude.append(self.logfile[len(self.basedir)+1:])
347
348        if subdir and subdir<>'.':
349            self._addPathnames([subdir])
350
351        for dir, subdirs, files in walk(join(self.basedir, subdir)):
352            for excd in IGNORED_METADIRS:
353                if excd in subdirs:
354                    subdirs.remove(excd)
355
356            for excf in exclude:
357                if excf in files:
358                    files.remove(excf)
359
360            if subdirs or files:
361                self._addPathnames([join(dir, df)[len(self.basedir)+1:]
362                                    for df in subdirs + files])
363
364    def _commit(self, date, author, patchname, changelog=None, entries=None):
365        """
366        Commit the changeset.
367        """
368
369        raise "%s should override this method" % self.__class__
370
371    def _removeEntries(self, entries):
372        """
373        Remove a sequence of entries.
374        """
375
376        self._removePathnames([e.name for e in entries])
377
378    def _removePathnames(self, names):
379        """
380        Remove some filesystem object.
381        """
382
383        raise "%s should override this method" % self.__class__
384
385    def _renameEntries(self, entries):
386        """
387        Rename a sequence of entries, adding all the parent directories
388        of each entry.
389        """
390
391        from os import rename, unlink
392        from os.path import split, join, exists
393
394        added = []
395        for e in entries:
396            parents = []
397            parent = split(e.name)[0]
398            while parent:
399                if not parent in added:
400                    parents.append(parent)
401                    added.append(parent)
402                parent = split(parent)[0]
403            if parents:
404                parents.reverse()
405                self._addPathnames(parents)
406
407            if self.shared_basedirs:
408                # Check to see if the oldentry is still there. If it is,
409                # that probably means one thing: it's been moved and then
410                # replaced, see svn 'R' event. In this case, rename the
411                # existing old entry to something else to trick targets
412                # (that will assume the move was already done manually) and
413                # finally restore its name.
414
415                absold = join(self.basedir, e.old_name)
416                renamed = exists(absold)
417                if renamed:
418                    rename(absold, absold + '-TAILOR-HACKED-TEMP-NAME')
419            else:
420                # With disjunct directories, old entries are *always*
421                # there because we dropped the --delete option to rsync.
422                # So, instead of renaming the old entry, we temporarily
423                # rename the new one, perform the target system rename
424                # and replace back the real content (it may be a
425                # renamed+edited event).
426                renamed = False
427                absnew = join(self.basedir, e.name)
428                renamed = exists(absnew)
429                if renamed:
430                    rename(absnew, absnew + '-TAILOR-HACKED-TEMP-NAME')
431
432            try:
433                self._renamePathname(e.old_name, e.name)
434            finally:
435                if renamed:
436                    if self.shared_basedirs:
437                        rename(absold + '-TAILOR-HACKED-TEMP-NAME', absold)
438                    else:
439                        unlink(absnew)
440                        rename(absnew + '-TAILOR-HACKED-TEMP-NAME', absnew)
441
442    def _renamePathname(self, oldname, newname):
443        """
444        Rename a filesystem object to some other name/location.
445        """
446
447        raise "%s should override this method" % self.__class__
448
449    def prepareWorkingDirectory(self, source_repo):
450        """
451        Do anything required to setup the hosting working directory.
452        """
453
454        self._prepareWorkingDirectory(source_repo)
455
456    def _prepareWorkingDirectory(self, source_repo):
457        """
458        Possibly checkout a working copy of the target VC, that will host the
459        upstream source tree, when overriden by subclasses.
460        """
461
462    def prepareTargetRepository(self):
463        """
464        Do anything required to host the target repository.
465        """
466
467        from os import makedirs
468        from os.path import join, exists
469
470        if not exists(self.basedir):
471            makedirs(self.basedir)
472
473        self._prepareTargetRepository()
474
475        prefix = self.__getPrefixToSource()
476        if prefix:
477            if not exists(join(self.basedir, prefix)):
478                # At bootstrap time, we assume that if the user
479                # extracted the source manually, she added
480                # the subdir, before doing that.
481                makedirs(join(self.basedir, prefix))
482                self._addPathnames([prefix])
483
484    def _prepareTargetRepository(self):
485        """
486        Possibly create or connect to the repository, when overriden
487        by subclasses.
488        """
489
490    def importFirstRevision(self, source_repo, changeset, initial):
491        """
492        Initialize a new working directory, just extracted from
493        some other VC system, importing everything's there.
494        """
495
496        self._initializeWorkingDir()
497        # Execute the precommit hooks, but ignore None results
498        changeset = self._adaptChangeset(changeset) or changeset
499        revision = changeset.revision
500        source_repository = str(source_repo)
501        if initial:
502            author = changeset.author
503            patchname, log = self.__getPatchNameAndLog(changeset)
504        else:
505            author = "%s@%s" % (AUTHOR, HOST)
506            patchname = BOOTSTRAP_PATCHNAME
507            log = BOOTSTRAP_CHANGELOG % locals()
508        self._commit(changeset.date, author, patchname, log)
509
510        if changeset.tags:
511            for tag in changeset.tags:
512                self._tag(tag)
513
514        self._dismissChangeset(changeset)
515
516    def _initializeWorkingDir(self):
517        """
518        Assuming the ``basedir`` directory contains a working copy ``module``
519        extracted from some VC repository, add it and all its content
520        to the target repository.
521
522        This implementation recursively add every file in the subtree.
523        Subclasses should override this method doing whatever is
524        appropriate for the backend.
525        """
526
527        self._addSubtree('.')
528
529    def _tag(self, tagname):
530        """
531        Tag the current version, if the VC type supports it, otherwise
532        do nothing.
533        """
534        pass
Note: See TracBrowser for help on using the repository browser.