source: tailor/vcpx/target.py @ 1209

Revision 1209, 18.7 KB checked in by ydirson@…, 7 years ago (diff)

Move basedir attribute from WorkingDir to Repository

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