source: tailor/vcpx/target.py @ 1142

Revision 1142, 17.3 KB checked in by lele@…, 7 years ago (diff)

Don't be too lazy dispatching the action
It didn't handle the modified files, effectively ignore them.

RevLine 
[11]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>
[305]5# :Licenza:  GNU General Public License
[443]6#
[11]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
[42]15import socket
[972]16from signal import signal, SIGINT, SIG_IGN
[516]17from workdir import WorkingDir
[14]18
[42]19HOST = socket.getfqdn()
20AUTHOR = "tailor"
[538]21BOOTSTRAP_PATCHNAME = 'Tailorization'
[85]22BOOTSTRAP_CHANGELOG = """\
[344]23Import of the upstream sources from
[664]24%(source_repository)s
[659]25   Revision: %(revision)s
[85]26"""
27
[46]28class TargetInitializationFailure(Exception):
[194]29    "Failure initializing the target VCS"
[822]30
31class ChangesetReplayFailure(Exception):
32    "Failure replaying the changeset on the target system"
[46]33
[1113]34class SynchronizableTargetWorkingDir(WorkingDir):
[11]35    """
[20]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
[311]45        feed as ``replay`` to ``applyUpstreamChangesets``
[20]46
[607]47    importFirstRevision
[20]48        to initialize a pristine working directory tree under this VC
49        system, possibly extracted under a different kind of VC
[443]50
[20]51    Subclasses MUST override at least the _underscoredMethods.
[11]52    """
53
[837]54    PATCH_NAME_FORMAT = '[%(project)s @ %(revision)s]'
[230]55    """
56    The format string used to compute the patch name, used by underlying VCS.
57    """
[255]58
59    REMOVE_FIRST_LOG_LINE = False
60    """
61    When true, remove the first line from the upstream changelog.
62    """
[443]63
[826]64    def __getPatchNameAndLog(self, changeset):
[11]65        """
[826]66        Return a tuple (patchname, changelog) interpolating changeset's
67        information with the template above.
[11]68        """
69
[511]70        if changeset.log == '':
71            firstlogline = 'Empty log message'
72            remaininglog = ''
[116]73        else:
[511]74            loglines = changeset.log.split('\n')
75            if len(loglines)>1:
76                firstlogline = loglines[0]
77                remaininglog = '\n'.join(loglines[1:])
[255]78            else:
[511]79                firstlogline = changeset.log
80                remaininglog = ''
[116]81
[513]82        patchname = self.PATCH_NAME_FORMAT % {
[948]83            'project': self.repository.projectref().name,
[511]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
[826]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
[940]102        try:
103            changeset = self._adaptChangeset(changeset)
104        except:
105            self.log.exception("Failure adapting: %s", str(changeset))
106            raise
107
[826]108        if changeset is None:
109            return
110
111        try:
112            self._replayChangeset(changeset)
113        except:
[940]114            self.log.exception("Failure replaying: %s", str(changeset))
[826]115            raise
116        patchname, log = self.__getPatchNameAndLog(changeset)
[511]117        entries = self._getCommitEntries(changeset)
[972]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)
[912]127
[940]128        try:
129            self._dismissChangeset(changeset)
130        except:
131            self.log.exception("Failure dismissing: %s", str(changeset))
132            raise
[639]133
[757]134    def __getPrefixToSource(self):
[827]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
[948]140        project = self.repository.projectref()
[1051]141        ssubdir = project.source.subdir
[948]142        tsubdir = project.target.subdir
[757]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
[844]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
[757]175    def __adaptEntriesPath(self, changeset):
176        """
[763]177        If the source basedir is a subdirectory of the target, adjust
178        all the pathnames adding the prefix computed by difference.
[757]179        """
180
181        from copy import deepcopy
182        from os.path import join
[763]183
184        if not changeset.entries:
185            return changeset
[757]186
187        prefix = self.__getPrefixToSource()
[844]188        adapted = deepcopy(changeset)
189        for e in adapted.entries:
190            if prefix:
[757]191                e.name = join(prefix, e.name)
192                if e.old_name:
193                    e.old_name = join(prefix, e.old_name)
[844]194            self._normalizeEntryPaths(e)
195        return adapted
[757]196
[763]197    def _adaptEntries(self, changeset):
198        """
199        Do whatever is needed to adapt entries to the target system.
200
[844]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.
[763]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
[639]215    def _adaptChangeset(self, changeset):
216        """
[827]217        Do whatever needed before replay and return the adapted changeset.
[639]218
[763]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.
[639]224        """
225
226        from copy import copy
227
[763]228        adapted = self._adaptEntries(changeset)
229        if adapted:
[948]230            project = self.repository.projectref()
231            if project.before_commit:
[763]232                adapted = copy(adapted)
[639]233
[948]234                for adapter in project.before_commit:
[763]235                    if not adapter(self, adapted):
236                        return None
[757]237        return adapted
[639]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
[948]248        project = self.repository.projectref()
249        if project.after_commit:
250            for farewell in project.after_commit:
[639]251                farewell(self, changeset)
[235]252
253    def _getCommitEntries(self, changeset):
254        """
255        Extract the names of the entries for the commit phase.
256        """
[393]257
[235]258        return [e.name for e in changeset.entries]
[443]259
[527]260    def _replayChangeset(self, changeset):
[16]261        """
262        Replicate the actions performed by the changeset on the tree of
263        files.
264        """
[116]265
[240]266        from os.path import join, isdir
[1135]267        from changes import ChangesetEntry
[443]268
[1135]269        added = []
270        actions = { ChangesetEntry.ADDED: self._addEntries,
271                    ChangesetEntry.DELETED: self._removeEntries,
272                    ChangesetEntry.RENAMED: self._renameEntries
273                    }
[893]274
[1135]275        last = None
276        group = []
277        for e in changeset.entries:
278            if last is None or last.action_kind == e.action_kind:
279                last = e
280                group.append(e)
281            if last.action_kind != e.action_kind:
[1142]282                action = actions.get(last.action_kind)
283                if action is not None:
284                    action(group)
[1135]285                group = [e]
286            if e.action_kind == e.ADDED:
287                added.append(e)
288        if group:
[1142]289            action = actions.get(group[0].action_kind)
290            if action is not None:
291                action(group)
[443]292
[295]293
294        # Finally, deal with "copied" directories. The simple way is
295        # executing an _addSubtree on each of them, evenif this may
296        # cause "warnings" on items just moved/added above...
297
[382]298        while added:
299            subdir = added.pop(0).name
[527]300            if isdir(join(self.basedir, subdir)):
301                self._addSubtree(subdir)
[382]302                added = [e for e in added if not e.name.startswith(subdir)]
[295]303
[527]304    def _addEntries(self, entries):
[215]305        """
306        Add a sequence of entries
307        """
308
[527]309        self._addPathnames([e.name for e in entries])
[443]310
[527]311    def _addPathnames(self, names):
[11]312        """
[291]313        Add some new filesystem objects.
[11]314        """
315
316        raise "%s should override this method" % self.__class__
317
[527]318    def _addSubtree(self, subdir):
[293]319        """
320        Add a whole subtree.
321
322        This implementation crawl down the whole subtree, adding
323        entries (subdirs, skipping the usual VC-specific control
324        directories such as ``.svn``, ``_darcs`` or ``CVS``, and
325        files).
326
327        Subclasses may use a better way, if the backend implements
328        a recursive add that skips the various metadata directories.
329        """
[443]330
[327]331        from os.path import join
[293]332        from os import walk
[383]333        from dualwd import IGNORED_METADIRS
[293]334
[543]335        exclude = []
336
337        if self.state_file.filename.startswith(self.basedir):
[632]338            sfrelname = self.state_file.filename[len(self.basedir)+1:]
339            exclude.append(sfrelname)
[992]340            exclude.append(sfrelname+'.old')
[632]341            exclude.append(sfrelname+'.journal')
[543]342
343        if self.logfile.startswith(self.basedir):
344            exclude.append(self.logfile[len(self.basedir)+1:])
345
[527]346        if subdir and subdir<>'.':
347            self._addPathnames([subdir])
[293]348
[527]349        for dir, subdirs, files in walk(join(self.basedir, subdir)):
[383]350            for excd in IGNORED_METADIRS:
[293]351                if excd in subdirs:
352                    subdirs.remove(excd)
353
[543]354            for excf in exclude:
[293]355                if excf in files:
356                    files.remove(excf)
357
358            if subdirs or files:
[527]359                self._addPathnames([join(dir, df)[len(self.basedir)+1:]
360                                    for df in subdirs + files])
[293]361
[527]362    def _commit(self, date, author, patchname, changelog=None, entries=None):
[11]363        """
364        Commit the changeset.
365        """
[443]366
[11]367        raise "%s should override this method" % self.__class__
[215]368
[527]369    def _removeEntries(self, entries):
[215]370        """
371        Remove a sequence of entries.
372        """
[291]373
[527]374        self._removePathnames([e.name for e in entries])
[443]375
[527]376    def _removePathnames(self, names):
[11]377        """
[291]378        Remove some filesystem object.
[11]379        """
380
381        raise "%s should override this method" % self.__class__
382
[527]383    def _renameEntries(self, entries):
[215]384        """
[382]385        Rename a sequence of entries, adding all the parent directories
386        of each entry.
[215]387        """
[443]388
[1135]389        from os import rename, unlink
[966]390        from os.path import split, join, exists
[382]391
392        added = []
[215]393        for e in entries:
[382]394            parents = []
395            parent = split(e.name)[0]
396            while parent:
397                if not parent in added:
398                    parents.append(parent)
399                    added.append(parent)
400                parent = split(parent)[0]
401            if parents:
402                parents.reverse()
[527]403                self._addPathnames(parents)
[382]404
[1135]405            if self.shared_basedirs:
406                # Check to see if the oldentry is still there. If it is,
407                # that probably means one thing: it's been moved and then
408                # replaced, see svn 'R' event. In this case, rename the
409                # existing old entry to something else to trick targets
410                # (that will assume the move was already done manually) and
411                # finally restore its name.
412
413                absold = join(self.basedir, e.old_name)
414                renamed = exists(absold)
415                if renamed:
416                    rename(absold, absold + '-TAILOR-HACKED-TEMP-NAME')
417            else:
418                # With disjunct directories, old entries are *always*
419                # there because we dropped the --delete option to rsync.
420                # So, instead of renaming the old entry, we temporarily
421                # rename the new one, perform the target system rename
422                # and replace back the real content (it may be a
423                # renamed+edited event).
424                renamed = False
425                absnew = join(self.basedir, e.name)
426                renamed = exists(absnew)
427                if renamed:
428                    rename(absnew, absnew + '-TAILOR-HACKED-TEMP-NAME')
[966]429
430            try:
431                self._renamePathname(e.old_name, e.name)
432            finally:
433                if renamed:
[1135]434                    if self.shared_basedirs:
435                        rename(absold + '-TAILOR-HACKED-TEMP-NAME', absold)
436                    else:
437                        unlink(absnew)
438                        rename(absnew + '-TAILOR-HACKED-TEMP-NAME', absnew)
[443]439
[527]440    def _renamePathname(self, oldname, newname):
[11]441        """
[291]442        Rename a filesystem object to some other name/location.
[11]443        """
444
445        raise "%s should override this method" % self.__class__
446
[533]447    def prepareWorkingDirectory(self, source_repo):
[452]448        """
449        Do anything required to setup the hosting working directory.
450        """
451
[533]452        self._prepareWorkingDirectory(source_repo)
[452]453
[533]454    def _prepareWorkingDirectory(self, source_repo):
[452]455        """
456        Possibly checkout a working copy of the target VC, that will host the
457        upstream source tree, when overriden by subclasses.
458        """
459
[752]460    def prepareTargetRepository(self):
461        """
462        Do anything required to host the target repository.
463        """
464
465        from os import makedirs
466        from os.path import join, exists
467
468        if not exists(self.basedir):
469            makedirs(self.basedir)
470
471        self._prepareTargetRepository()
472
473        prefix = self.__getPrefixToSource()
474        if prefix:
475            if not exists(join(self.basedir, prefix)):
[762]476                # At bootstrap time, we assume that if the user
477                # extracted the source manually, she added
478                # the subdir, before doing that.
[752]479                makedirs(join(self.basedir, prefix))
[762]480                self._addPathnames([prefix])
[752]481
482    def _prepareTargetRepository(self):
483        """
484        Possibly create or connect to the repository, when overriden
485        by subclasses.
486        """
487
[607]488    def importFirstRevision(self, source_repo, changeset, initial):
[14]489        """
[46]490        Initialize a new working directory, just extracted from
[16]491        some other VC system, importing everything's there.
[14]492        """
[527]493
[826]494        self._initializeWorkingDir()
495        # Execute the precommit hooks, but ignore None results
[426]496        changeset = self._adaptChangeset(changeset) or changeset
[664]497        revision = changeset.revision
[431]498        source_repository = str(source_repo)
499        if initial:
[826]500            author = changeset.author
[431]501            patchname, log = self.__getPatchNameAndLog(changeset)
502        else:
[538]503            author = "%s@%s" % (AUTHOR, HOST)
[431]504            patchname = BOOTSTRAP_PATCHNAME
[527]505            log = BOOTSTRAP_CHANGELOG % locals()
[912]506        self._commit(changeset.date, author, patchname, log)
507
508        if changeset.tags:
509            for tag in changeset.tags:
510                self._tag(tag)
[826]511
[14]512        self._dismissChangeset(changeset)
[527]513
[14]514    def _initializeWorkingDir(self):
[527]515        """
[46]516        Assuming the ``basedir`` directory contains a working copy ``module``
517        extracted from some VC repository, add it and all its content
[20]518        to the target repository.
[289]519
520        This implementation recursively add every file in the subtree.
521        Subclasses should override this method doing whatever is
[14]522        appropriate for the backend.
[19]523        """
[527]524
[19]525        self._addSubtree('.')
[883]526
527    def _tag(self, tagname):
528        """
529        Tag the current version, if the VC type supports it, otherwise
530        do nothing.
531        """
532        pass
Note: See TracBrowser for help on using the repository browser.