source: tailor/vcpx/target.py @ 443

Revision 443, 11.2 KB checked in by lele@…, 8 years ago (diff)

M-x whitespace-cleanup

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: %(repository)s
24  Module:     %(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, repository, module, subdir,
319                                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, repository, module, subdir)
326        revision = changeset.revision
327        if initial:
328            author = changeset.author
329            remark = changeset.log
330            log = None
331        else:
332            author = "%s@%s" % (AUTHOR, HOST)
333            remark = BOOTSTRAP_PATCHNAME % module
334            log = BOOTSTRAP_CHANGELOG % locals()
335        self._commit(root, changeset.date, author, remark, log,
336                     entries=[subdir])
337
338    def _initializeWorkingDir(self, root, repository, module, subdir):
339        """
340        Assuming the ``root`` directory contains a working copy ``module``
341        extracted from some VC repository, add it and all its content
342        to the target repository.
343
344        This implementation recursively add every file in the subtree.
345        Subclasses should override this method doing whatever is
346        appropriate for the backend.
347        """
348
349        self._addSubtree(root, subdir)
Note: See TracBrowser for help on using the repository browser.