| 1 | #! /usr/bin/python |
|---|
| 2 | # -*- mode: python; coding: utf-8 -*- |
|---|
| 3 | # :Progetto: vcpx -- Syncable targets |
|---|
| 4 | # :Creato: ven 04 giu 2004 00:27:07 CEST |
|---|
| 5 | # :Autore: Lele Gaifax <lele@nautilus.homeip.net> |
|---|
| 6 | # |
|---|
| 7 | |
|---|
| 8 | """ |
|---|
| 9 | Syncronizable targets are the simplest abstract wrappers around a |
|---|
| 10 | working directory under two different version control systems. |
|---|
| 11 | """ |
|---|
| 12 | |
|---|
| 13 | __docformat__ = 'reStructuredText' |
|---|
| 14 | |
|---|
| 15 | import socket |
|---|
| 16 | |
|---|
| 17 | HOST = socket.getfqdn() |
|---|
| 18 | AUTHOR = "tailor" |
|---|
| 19 | BOOTSTRAP_PATCHNAME = 'Tailorization of %s' |
|---|
| 20 | BOOTSTRAP_CHANGELOG = """\ |
|---|
| 21 | Import of the upstream sources from the repository |
|---|
| 22 | |
|---|
| 23 | %(repository)s |
|---|
| 24 | |
|---|
| 25 | as of revision %(revision)s |
|---|
| 26 | """ |
|---|
| 27 | |
|---|
| 28 | class TargetInitializationFailure(Exception): |
|---|
| 29 | "Failure initializing the target VCS" |
|---|
| 30 | |
|---|
| 31 | pass |
|---|
| 32 | |
|---|
| 33 | class 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 | def replayChangeset(self, root, module, changeset, |
|---|
| 54 | delayed_commit=False, logger=None): |
|---|
| 55 | """ |
|---|
| 56 | Do whatever is needed to replay the changes under the target |
|---|
| 57 | VC, to register the already applied (under the other VC) |
|---|
| 58 | changeset. |
|---|
| 59 | |
|---|
| 60 | If `delayed_commit` is not True, the changeset is committed |
|---|
| 61 | to the target VC right after a successful application; otherwise |
|---|
| 62 | the various information get registered and will be reused later, |
|---|
| 63 | by commitDelayedChangesets(). |
|---|
| 64 | """ |
|---|
| 65 | |
|---|
| 66 | try: |
|---|
| 67 | self._replayChangeset(root, changeset, logger) |
|---|
| 68 | except: |
|---|
| 69 | if logger: logger.critical(str(changeset)) |
|---|
| 70 | raise |
|---|
| 71 | |
|---|
| 72 | if delayed_commit: |
|---|
| 73 | self.__registerAppliedChangeset(changeset) |
|---|
| 74 | else: |
|---|
| 75 | from os.path import split |
|---|
| 76 | |
|---|
| 77 | remark = '%s: changeset %s' % (module, changeset.revision) |
|---|
| 78 | changelog = changeset.log |
|---|
| 79 | entries = [e.name for e in changeset.entries] |
|---|
| 80 | self._commit(root, changeset.date, changeset.author, |
|---|
| 81 | remark, changelog, entries) |
|---|
| 82 | |
|---|
| 83 | def commitDelayedChangesets(self, root, concatenate_logs=True): |
|---|
| 84 | """ |
|---|
| 85 | If there are changesets pending to be committed, do a single |
|---|
| 86 | commit of all changed entries. |
|---|
| 87 | |
|---|
| 88 | With `concatenate_logs` there's control over the folded |
|---|
| 89 | changesets message log: if True every changelog is appended in |
|---|
| 90 | order of application, otherwise it will contain just the name |
|---|
| 91 | of the patches. |
|---|
| 92 | """ |
|---|
| 93 | |
|---|
| 94 | from datetime import datetime |
|---|
| 95 | |
|---|
| 96 | if not hasattr(self, '_registered_cs'): |
|---|
| 97 | return |
|---|
| 98 | |
|---|
| 99 | mindate = maxdate = None |
|---|
| 100 | combined_entries = {} |
|---|
| 101 | combined_log = [] |
|---|
| 102 | combined_authors = {} |
|---|
| 103 | for cs in self._registered_cs: |
|---|
| 104 | if not mindate or mindate>cs.date: |
|---|
| 105 | mindate = cs.date |
|---|
| 106 | if not maxdate or maxdate<cs.date: |
|---|
| 107 | maxdate = cs.date |
|---|
| 108 | |
|---|
| 109 | if concatenate_logs: |
|---|
| 110 | msg = 'changeset %s by %s' % (cs.revision, cs.author) |
|---|
| 111 | combined_log.append(msg) |
|---|
| 112 | combined_log.append('=' * len(msg)) |
|---|
| 113 | combined_log.append(cs.log) |
|---|
| 114 | else: |
|---|
| 115 | combined_log.append('* changeset %s by %s' % (cs.revision, |
|---|
| 116 | cs.author)) |
|---|
| 117 | combined_authors[cs.author] = True |
|---|
| 118 | |
|---|
| 119 | for e in [e.name for e in cs.entries]: |
|---|
| 120 | combined_entries[e] = True |
|---|
| 121 | |
|---|
| 122 | authors = ', '.join(combined_authors.keys()) |
|---|
| 123 | remark = 'Merged %d changesets from %s to %s' % ( |
|---|
| 124 | len(self._registered_cs), mindate, maxdate) |
|---|
| 125 | changelog = '\n'.join(combined_log) |
|---|
| 126 | entries = combined_entries.keys() |
|---|
| 127 | self._commit(root, datetime.now(), authors, |
|---|
| 128 | remark, changelog, entries) |
|---|
| 129 | |
|---|
| 130 | def _replayChangeset(self, root, changeset, logger): |
|---|
| 131 | """ |
|---|
| 132 | Replicate the actions performed by the changeset on the tree of |
|---|
| 133 | files. |
|---|
| 134 | """ |
|---|
| 135 | |
|---|
| 136 | added = changeset.addedEntries() |
|---|
| 137 | renamed = changeset.renamedEntries() |
|---|
| 138 | removed = changeset.removedEntries() |
|---|
| 139 | |
|---|
| 140 | # Sort entries, to be sure added directories come before their |
|---|
| 141 | # entries. |
|---|
| 142 | added.sort(lambda x,y: cmp(x.name, y.name)) |
|---|
| 143 | |
|---|
| 144 | # Likewise, sort removed one, but in reverse order |
|---|
| 145 | removed.sort(lambda x,y: cmp(y.name, x.name)) |
|---|
| 146 | |
|---|
| 147 | for e in added: |
|---|
| 148 | self._addEntry(root, e.name) |
|---|
| 149 | |
|---|
| 150 | for e in renamed: |
|---|
| 151 | self._renameEntry(root, e.old_name, e.name) |
|---|
| 152 | |
|---|
| 153 | for e in removed: |
|---|
| 154 | self._removeEntry(root, e.name) |
|---|
| 155 | |
|---|
| 156 | def __registerAppliedChangeset(self, changeset): |
|---|
| 157 | """ |
|---|
| 158 | Remember about an already applied but not committed changeset, |
|---|
| 159 | to be done later. |
|---|
| 160 | """ |
|---|
| 161 | |
|---|
| 162 | if not hasattr(self, '_registered_cs'): |
|---|
| 163 | self._registered_cs = [] |
|---|
| 164 | |
|---|
| 165 | self._registered_cs.append(changeset) |
|---|
| 166 | |
|---|
| 167 | def _addEntry(self, root, entry): |
|---|
| 168 | """ |
|---|
| 169 | Add a new entry. |
|---|
| 170 | """ |
|---|
| 171 | |
|---|
| 172 | raise "%s should override this method" % self.__class__ |
|---|
| 173 | |
|---|
| 174 | def _commit(self, root, date, author, remark, |
|---|
| 175 | changelog=None, entries=None): |
|---|
| 176 | """ |
|---|
| 177 | Commit the changeset. |
|---|
| 178 | """ |
|---|
| 179 | |
|---|
| 180 | raise "%s should override this method" % self.__class__ |
|---|
| 181 | |
|---|
| 182 | def _removeEntry(self, root, entry): |
|---|
| 183 | """ |
|---|
| 184 | Remove an entry. |
|---|
| 185 | """ |
|---|
| 186 | |
|---|
| 187 | raise "%s should override this method" % self.__class__ |
|---|
| 188 | |
|---|
| 189 | def _renameEntry(self, root, oldentry, newentry): |
|---|
| 190 | """ |
|---|
| 191 | Rename an entry. |
|---|
| 192 | """ |
|---|
| 193 | |
|---|
| 194 | raise "%s should override this method" % self.__class__ |
|---|
| 195 | |
|---|
| 196 | def initializeNewWorkingDir(self, root, repository, module, subdir, revision): |
|---|
| 197 | """ |
|---|
| 198 | Initialize a new working directory, just extracted from |
|---|
| 199 | some other VC system, importing everything's there. |
|---|
| 200 | """ |
|---|
| 201 | |
|---|
| 202 | from datetime import datetime |
|---|
| 203 | |
|---|
| 204 | now = datetime.now() |
|---|
| 205 | self._initializeWorkingDir(root, repository, module, subdir) |
|---|
| 206 | self._commit(root, now, '%s@%s' % (AUTHOR, HOST), |
|---|
| 207 | BOOTSTRAP_PATCHNAME % module, |
|---|
| 208 | BOOTSTRAP_CHANGELOG % locals(), |
|---|
| 209 | entries=[subdir]) |
|---|
| 210 | |
|---|
| 211 | def _initializeWorkingDir(self, root, repository, module, subdir, addentry=None): |
|---|
| 212 | """ |
|---|
| 213 | Assuming the `root` directory contains a working copy `module` |
|---|
| 214 | extracted from some VC repository, add it and all its content |
|---|
| 215 | to the target repository. |
|---|
| 216 | |
|---|
| 217 | This implementation first runs the given `addentry` |
|---|
| 218 | *SystemCommand* on the `root` directory, then it walks down |
|---|
| 219 | the `root` tree executing the same command on each entry |
|---|
| 220 | excepted the usual VC-specific control directories such as |
|---|
| 221 | ``.svn``, ``_darcs`` or ``CVS``. |
|---|
| 222 | |
|---|
| 223 | If this does make sense, subclasses should just call this |
|---|
| 224 | method with the right `addentry` command. |
|---|
| 225 | """ |
|---|
| 226 | |
|---|
| 227 | assert addentry, "Subclass should have specified something as addentry" |
|---|
| 228 | |
|---|
| 229 | from os.path import split, join |
|---|
| 230 | from os import walk |
|---|
| 231 | |
|---|
| 232 | if module: |
|---|
| 233 | c = addentry(working_dir=root) |
|---|
| 234 | c(entry=repr(module)) |
|---|
| 235 | |
|---|
| 236 | for dir, subdirs, files in walk(join(root, module or '')): |
|---|
| 237 | for excd in ['.svn', '_darcs', 'CVS']: |
|---|
| 238 | if excd in subdirs: |
|---|
| 239 | subdirs.remove(excd) |
|---|
| 240 | |
|---|
| 241 | # Uhm, is this really desiderable? |
|---|
| 242 | for excf in ['tailor.info', 'tailor.log']: |
|---|
| 243 | if excf in files: |
|---|
| 244 | files.remove(excf) |
|---|
| 245 | |
|---|
| 246 | c = addentry(working_dir=dir) |
|---|
| 247 | c(entry=' '.join([repr(e) for e in subdirs+files])) |
|---|
| 248 | |
|---|