| 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 | """ |
|---|
| 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 |
|---|
| 22 | |
|---|
| 23 | Repository: %(repository)s |
|---|
| 24 | Module: %(module)s |
|---|
| 25 | 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 | 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) |
|---|