source: tailor/vcpx/target.py @ 445

Revision 445, 11.3 KB checked in by lele@…, 8 years ago (diff)

Renamed the arguments for clearity

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
16
17HOST = socket.getfqdn()
18AUTHOR = "tailor"
19BOOTSTRAP_PATCHNAME = 'Tailorization of %s'
20BOOTSTRAP_CHANGELOG = """\
21Import of the upstream sources from
22
23  Repository: %(source_repository)s
24  Module:     %(source_module)s
25  Revision:   %(revision)s
26"""
27
28class TargetInitializationFailure(Exception):
29    "Failure initializing the target VCS"
30
31    pass
32
33class SyncronizableTargetWorkingDir(object):
34    """
35    This is an abstract working dir usable as a *shadow* of another
36    kind of VC, sharing the same working directory.
37
38    Most interesting entry points are:
39
40    replayChangeset
41        to replay an already applied changeset, to mimic the actions
42        performed by the upstream VC system on the tree such as
43        renames, deletions and adds.  This is an useful argument to
44        feed as ``replay`` to ``applyUpstreamChangesets``
45
46    initializeNewWorkingDir
47        to initialize a pristine working directory tree under this VC
48        system, possibly extracted under a different kind of VC
49
50    Subclasses MUST override at least the _underscoredMethods.
51    """
52
53    PATCH_NAME_FORMAT = '%(module)s: changeset %(revision)s'
54    """
55    The format string used to compute the patch name, used by underlying VCS.
56    """
57
58    REMOVE_FIRST_LOG_LINE = False
59    """
60    When true, remove the first line from the upstream changelog.
61    """
62
63    def replayChangeset(self, root, module, changeset,
64                        delayed_commit=False, logger=None):
65        """
66        Do whatever is needed to replay the changes under the target
67        VC, to register the already applied (under the other VC)
68        changeset.
69
70        If ``delayed_commit`` is not True, the changeset is committed
71        to the target VC right after a successful application; otherwise
72        the various information get registered and will be reused later,
73        by commitDelayedChangesets().
74        """
75
76        try:
77            self._replayChangeset(root, changeset, logger)
78        except:
79            if logger: logger.critical(str(changeset))
80            raise
81
82        if delayed_commit:
83            self.__registerAppliedChangeset(changeset)
84        else:
85            if changeset.log == '':
86                firstlogline = 'Empty log message'
87                remaininglog = ''
88            else:
89                loglines = changeset.log.split('\n')
90                if len(loglines)>1:
91                    firstlogline = loglines[0]
92                    remaininglog = '\n'.join(loglines[1:])
93                else:
94                    firstlogline = changeset.log
95                    remaininglog = ''
96
97            remark = self.PATCH_NAME_FORMAT % {
98                'module': module,
99                'revision': changeset.revision,
100                'author': changeset.author,
101                'date': changeset.date,
102                'firstlogline': firstlogline,
103                'remaininglog': remaininglog}
104            if self.REMOVE_FIRST_LOG_LINE:
105                changelog = remaininglog
106            else:
107                changelog = changeset.log
108            entries = self._getCommitEntries(changeset)
109            self._commit(root, changeset.date, changeset.author,
110                         remark, changelog, entries)
111
112    def commitDelayedChangesets(self, root, concatenate_logs=True):
113        """
114        If there are changesets pending to be committed, do a single
115        commit of all changed entries.
116
117        With ``concatenate_logs`` there's control over the folded
118        changesets message log: if True every changelog is appended in
119        order of application, otherwise it will contain just the name
120        of the patches.
121        """
122
123        from datetime import datetime
124
125        if not hasattr(self, '_registered_cs'):
126            return
127
128        mindate = maxdate = None
129        combined_entries = {}
130        combined_log = []
131        combined_authors = {}
132        for cs in self._registered_cs:
133            if not mindate or mindate>cs.date:
134                mindate = cs.date
135            if not maxdate or maxdate<cs.date:
136                maxdate = cs.date
137
138            if concatenate_logs:
139                msg = 'changeset %s by %s' % (cs.revision, cs.author)
140                combined_log.append(msg)
141                combined_log.append('=' * len(msg))
142                combined_log.append(cs.log)
143            else:
144                combined_log.append('* changeset %s by %s' % (cs.revision,
145                                                              cs.author))
146            combined_authors[cs.author] = True
147
148            for e in self._getCommitEntries(cs):
149                combined_entries[e] = True
150
151        authors = ', '.join(combined_authors.keys())
152        remark = (self.PATCH_NAME_FORMAT or
153                  'Merged %(nchangesets) changesets '
154                  'from %(mindate)s to %(maxdate)s') % {
155            'module': module,
156            'nchangesets': len(self._registered_cs),
157            'authors': authors,
158            'mindate': mindate,
159            'maxdate': maxdate}
160        changelog = '\n'.join(combined_log)
161        entries = combined_entries.keys()
162        self._commit(root, datetime.now(), authors,
163                         remark, changelog, entries)
164
165    def _getCommitEntries(self, changeset):
166        """
167        Extract the names of the entries for the commit phase.
168        """
169
170        return [e.name for e in changeset.entries]
171
172    def _replayChangeset(self, root, changeset, logger):
173        """
174        Replicate the actions performed by the changeset on the tree of
175        files.
176        """
177
178        from os.path import join, isdir
179
180        added = changeset.addedEntries()
181        renamed = changeset.renamedEntries()
182        removed = changeset.removedEntries()
183
184        # Sort added entries, to be sure that /root/addedDir/ comes
185        # before /root/addedDir/addedSubdir
186        added.sort(lambda x,y: cmp(x.name, y.name))
187
188        # Sort removes in reverse order, to delete directories after
189        # their contents.
190        removed.sort(lambda x,y: cmp(y.name, x.name))
191
192        # Replay the actions
193
194        if renamed: self._renameEntries(root, renamed)
195        if removed: self._removeEntries(root, removed)
196        if added: self._addEntries(root, added)
197
198        # Finally, deal with "copied" directories. The simple way is
199        # executing an _addSubtree on each of them, evenif this may
200        # cause "warnings" on items just moved/added above...
201
202        while added:
203            subdir = added.pop(0).name
204            if isdir(join(root, subdir)):
205                self._addSubtree(root, subdir)
206                added = [e for e in added if not e.name.startswith(subdir)]
207
208    def __registerAppliedChangeset(self, changeset):
209        """
210        Remember about an already applied but not committed changeset,
211        to be done later.
212        """
213
214        if not hasattr(self, '_registered_cs'):
215            self._registered_cs = []
216
217        self._registered_cs.append(changeset)
218
219    def _addEntries(self, root, entries):
220        """
221        Add a sequence of entries
222        """
223
224        self._addPathnames(root, [e.name for e in entries])
225
226    def _addPathnames(self, root, names):
227        """
228        Add some new filesystem objects.
229        """
230
231        raise "%s should override this method" % self.__class__
232
233    def _addSubtree(self, root, subdir):
234        """
235        Add a whole subtree.
236
237        This implementation crawl down the whole subtree, adding
238        entries (subdirs, skipping the usual VC-specific control
239        directories such as ``.svn``, ``_darcs`` or ``CVS``, and
240        files).
241
242        Subclasses may use a better way, if the backend implements
243        a recursive add that skips the various metadata directories.
244        """
245
246        from os.path import join
247        from os import walk
248        from dualwd import IGNORED_METADIRS
249
250        if subdir<>'.':
251            self._addPathnames(root, [subdir])
252
253        for dir, subdirs, files in walk(join(root, subdir)):
254            for excd in IGNORED_METADIRS:
255                if excd in subdirs:
256                    subdirs.remove(excd)
257
258            # Uhm, is this really desiderable?
259            for excf in ['tailor.info', 'tailor.log']:
260                if excf in files:
261                    files.remove(excf)
262
263            if subdirs or files:
264                self._addPathnames(dir, subdirs + files)
265
266    def _commit(self, root, date, author, remark,
267                changelog=None, entries=None):
268        """
269        Commit the changeset.
270        """
271
272        raise "%s should override this method" % self.__class__
273
274    def _removeEntries(self, root, entries):
275        """
276        Remove a sequence of entries.
277        """
278
279        self._removePathnames(root, [e.name for e in entries])
280
281    def _removePathnames(self, root, names):
282        """
283        Remove some filesystem object.
284        """
285
286        raise "%s should override this method" % self.__class__
287
288    def _renameEntries(self, root, entries):
289        """
290        Rename a sequence of entries, adding all the parent directories
291        of each entry.
292        """
293
294        from os.path import split
295
296        added = []
297        for e in entries:
298            parents = []
299            parent = split(e.name)[0]
300            while parent:
301                if not parent in added:
302                    parents.append(parent)
303                    added.append(parent)
304                parent = split(parent)[0]
305            if parents:
306                parents.reverse()
307                self._addPathnames(root, parents)
308
309            self._renamePathname(root, e.old_name, e.name)
310
311    def _renamePathname(self, root, oldname, newname):
312        """
313        Rename a filesystem object to some other name/location.
314        """
315
316        raise "%s should override this method" % self.__class__
317
318    def initializeNewWorkingDir(self, root, source_repository,
319                                source_module, subdir, changeset, initial):
320        """
321        Initialize a new working directory, just extracted from
322        some other VC system, importing everything's there.
323        """
324
325        self._initializeWorkingDir(root, source_repository, source_module,
326                                   subdir)
327        revision = changeset.revision
328        if initial:
329            author = changeset.author
330            remark = changeset.log
331            log = None
332        else:
333            author = "%s@%s" % (AUTHOR, HOST)
334            remark = BOOTSTRAP_PATCHNAME % module
335            log = BOOTSTRAP_CHANGELOG % locals()
336        self._commit(root, changeset.date, author, remark, log,
337                     entries=[subdir])
338
339    def _initializeWorkingDir(self, root, repository, module, subdir):
340        """
341        Assuming the ``root`` directory contains a working copy ``module``
342        extracted from some VC repository, add it and all its content
343        to the target repository.
344
345        This implementation recursively add every file in the subtree.
346        Subclasses should override this method doing whatever is
347        appropriate for the backend.
348        """
349
350        self._addSubtree(root, subdir)
Note: See TracBrowser for help on using the repository browser.