| 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 | from workdir import WorkingDir |
|---|
| 17 | |
|---|
| 18 | HOST = socket.getfqdn() |
|---|
| 19 | AUTHOR = "tailor" |
|---|
| 20 | BOOTSTRAP_PATCHNAME = 'Tailorization' |
|---|
| 21 | BOOTSTRAP_CHANGELOG = """\ |
|---|
| 22 | Import of the upstream sources from |
|---|
| 23 | %(source_repository)s |
|---|
| 24 | Revision: %(revision)s |
|---|
| 25 | """ |
|---|
| 26 | |
|---|
| 27 | class TargetInitializationFailure(Exception): |
|---|
| 28 | "Failure initializing the target VCS" |
|---|
| 29 | |
|---|
| 30 | class ChangesetReplayFailure(Exception): |
|---|
| 31 | "Failure replaying the changeset on the target system" |
|---|
| 32 | |
|---|
| 33 | class SyncronizableTargetWorkingDir(WorkingDir): |
|---|
| 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 | importFirstRevision |
|---|
| 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 = '[%(project)s @ %(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 __getPatchNameAndLog(self, changeset): |
|---|
| 64 | """ |
|---|
| 65 | Return a tuple (patchname, changelog) interpolating changeset's |
|---|
| 66 | information with the template above. |
|---|
| 67 | """ |
|---|
| 68 | |
|---|
| 69 | if changeset.log == '': |
|---|
| 70 | firstlogline = 'Empty log message' |
|---|
| 71 | remaininglog = '' |
|---|
| 72 | else: |
|---|
| 73 | loglines = changeset.log.split('\n') |
|---|
| 74 | if len(loglines)>1: |
|---|
| 75 | firstlogline = loglines[0] |
|---|
| 76 | remaininglog = '\n'.join(loglines[1:]) |
|---|
| 77 | else: |
|---|
| 78 | firstlogline = changeset.log |
|---|
| 79 | remaininglog = '' |
|---|
| 80 | |
|---|
| 81 | patchname = self.PATCH_NAME_FORMAT % { |
|---|
| 82 | 'project': self.repository.project.name, |
|---|
| 83 | 'revision': changeset.revision, |
|---|
| 84 | 'author': changeset.author, |
|---|
| 85 | 'date': changeset.date, |
|---|
| 86 | 'firstlogline': firstlogline, |
|---|
| 87 | 'remaininglog': remaininglog} |
|---|
| 88 | if self.REMOVE_FIRST_LOG_LINE: |
|---|
| 89 | changelog = remaininglog |
|---|
| 90 | else: |
|---|
| 91 | changelog = changeset.log |
|---|
| 92 | return patchname, changelog |
|---|
| 93 | |
|---|
| 94 | def replayChangeset(self, changeset): |
|---|
| 95 | """ |
|---|
| 96 | Do whatever is needed to replay the changes under the target |
|---|
| 97 | VC, to register the already applied (under the other VC) |
|---|
| 98 | changeset. |
|---|
| 99 | """ |
|---|
| 100 | |
|---|
| 101 | changeset = self._adaptChangeset(changeset) |
|---|
| 102 | if changeset is None: |
|---|
| 103 | return |
|---|
| 104 | |
|---|
| 105 | try: |
|---|
| 106 | self._replayChangeset(changeset) |
|---|
| 107 | except: |
|---|
| 108 | self.log_error(str(changeset), exc=True) |
|---|
| 109 | raise |
|---|
| 110 | patchname, log = self.__getPatchNameAndLog(changeset) |
|---|
| 111 | entries = self._getCommitEntries(changeset) |
|---|
| 112 | self._commit(changeset.date, changeset.author, patchname, log, entries) |
|---|
| 113 | for tag in changeset.tags: |
|---|
| 114 | self._tag(tag) |
|---|
| 115 | self._dismissChangeset(changeset) |
|---|
| 116 | |
|---|
| 117 | def __getPrefixToSource(self): |
|---|
| 118 | """ |
|---|
| 119 | Compute and return the "offset" between source and target basedirs, |
|---|
| 120 | or None when not using shared directories, or there's no offset. |
|---|
| 121 | """ |
|---|
| 122 | |
|---|
| 123 | ssubdir = self.repository.project.source.subdir |
|---|
| 124 | tsubdir = self.repository.project.target.subdir |
|---|
| 125 | if self.shared_basedirs and ssubdir <> tsubdir: |
|---|
| 126 | if tsubdir == '.': |
|---|
| 127 | prefix = ssubdir |
|---|
| 128 | else: |
|---|
| 129 | if not tsubdir.endswith('/'): |
|---|
| 130 | tsubdir += '/' |
|---|
| 131 | prefix = ssubdir[len(tsubdir):] |
|---|
| 132 | return prefix |
|---|
| 133 | else: |
|---|
| 134 | return None |
|---|
| 135 | |
|---|
| 136 | def _normalizeEntryPaths(self, entry): |
|---|
| 137 | """ |
|---|
| 138 | Normalize the name and old_name of an entry. |
|---|
| 139 | |
|---|
| 140 | The ``name`` and ``old_name`` of an entry are pathnames coming |
|---|
| 141 | from the upstream system, and is usually (although there is no |
|---|
| 142 | guarantee it actually is) a UNIX style path with forward |
|---|
| 143 | slashes "/" as separators. |
|---|
| 144 | |
|---|
| 145 | This implementation uses normpath to adapt the path to the |
|---|
| 146 | actual OS convention, but subclasses may eventually override |
|---|
| 147 | this to use their own canonicalization of ``name`` and |
|---|
| 148 | ``old_name``. |
|---|
| 149 | """ |
|---|
| 150 | |
|---|
| 151 | from os.path import normpath |
|---|
| 152 | |
|---|
| 153 | entry.name = normpath(entry.name) |
|---|
| 154 | if entry.old_name: |
|---|
| 155 | entry.old_name = normpath(entry.old_name) |
|---|
| 156 | |
|---|
| 157 | def __adaptEntriesPath(self, changeset): |
|---|
| 158 | """ |
|---|
| 159 | If the source basedir is a subdirectory of the target, adjust |
|---|
| 160 | all the pathnames adding the prefix computed by difference. |
|---|
| 161 | """ |
|---|
| 162 | |
|---|
| 163 | from copy import deepcopy |
|---|
| 164 | from os.path import join |
|---|
| 165 | |
|---|
| 166 | if not changeset.entries: |
|---|
| 167 | return changeset |
|---|
| 168 | |
|---|
| 169 | prefix = self.__getPrefixToSource() |
|---|
| 170 | adapted = deepcopy(changeset) |
|---|
| 171 | for e in adapted.entries: |
|---|
| 172 | if prefix: |
|---|
| 173 | e.name = join(prefix, e.name) |
|---|
| 174 | if e.old_name: |
|---|
| 175 | e.old_name = join(prefix, e.old_name) |
|---|
| 176 | self._normalizeEntryPaths(e) |
|---|
| 177 | return adapted |
|---|
| 178 | |
|---|
| 179 | def _adaptEntries(self, changeset): |
|---|
| 180 | """ |
|---|
| 181 | Do whatever is needed to adapt entries to the target system. |
|---|
| 182 | |
|---|
| 183 | This implementation adds a prefix to each path if needed, when |
|---|
| 184 | the target basedir *contains* the source basedir. Also, each |
|---|
| 185 | path is normalized thru ``normpath()`` or whatever equivalent |
|---|
| 186 | operation provided by the specific target. It operates on and |
|---|
| 187 | returns a copy of the given changeset. |
|---|
| 188 | |
|---|
| 189 | Subclasses shall eventually extend this to exclude unwanted |
|---|
| 190 | entries, eventually returning None when all entries were |
|---|
| 191 | excluded, to avoid the commit on target of an empty changeset. |
|---|
| 192 | """ |
|---|
| 193 | |
|---|
| 194 | adapted = self.__adaptEntriesPath(changeset) |
|---|
| 195 | return adapted |
|---|
| 196 | |
|---|
| 197 | def _adaptChangeset(self, changeset): |
|---|
| 198 | """ |
|---|
| 199 | Do whatever needed before replay and return the adapted changeset. |
|---|
| 200 | |
|---|
| 201 | This implementation calls ``self._adaptEntries()``, then |
|---|
| 202 | executes the adapters defined by before-commit on the project: |
|---|
| 203 | each adapter is run in turn, and may return False to indicate |
|---|
| 204 | that the changeset shouldn't be replayed at all. They are |
|---|
| 205 | otherwise free to alter the changeset in any meaningful way. |
|---|
| 206 | """ |
|---|
| 207 | |
|---|
| 208 | from copy import copy |
|---|
| 209 | |
|---|
| 210 | adapted = self._adaptEntries(changeset) |
|---|
| 211 | if adapted: |
|---|
| 212 | if self.repository.project.before_commit: |
|---|
| 213 | adapted = copy(adapted) |
|---|
| 214 | |
|---|
| 215 | for adapter in self.repository.project.before_commit: |
|---|
| 216 | if not adapter(self, adapted): |
|---|
| 217 | return None |
|---|
| 218 | return adapted |
|---|
| 219 | |
|---|
| 220 | def _dismissChangeset(self, changeset): |
|---|
| 221 | """ |
|---|
| 222 | Do whatever needed after commit. |
|---|
| 223 | |
|---|
| 224 | This execute the adapters defined by after-commit on the project, |
|---|
| 225 | for example tagging in some way the target repository upon some |
|---|
| 226 | particular kind of changeset. |
|---|
| 227 | """ |
|---|
| 228 | |
|---|
| 229 | if self.repository.project.after_commit: |
|---|
| 230 | for farewell in self.repository.project.after_commit: |
|---|
| 231 | farewell(self, changeset) |
|---|
| 232 | |
|---|
| 233 | def _getCommitEntries(self, changeset): |
|---|
| 234 | """ |
|---|
| 235 | Extract the names of the entries for the commit phase. |
|---|
| 236 | """ |
|---|
| 237 | |
|---|
| 238 | return [e.name for e in changeset.entries] |
|---|
| 239 | |
|---|
| 240 | def _replayChangeset(self, changeset): |
|---|
| 241 | """ |
|---|
| 242 | Replicate the actions performed by the changeset on the tree of |
|---|
| 243 | files. |
|---|
| 244 | """ |
|---|
| 245 | |
|---|
| 246 | from os.path import join, isdir |
|---|
| 247 | |
|---|
| 248 | added = changeset.addedEntries() |
|---|
| 249 | renamed = changeset.renamedEntries() |
|---|
| 250 | removed = changeset.removedEntries() |
|---|
| 251 | |
|---|
| 252 | # Sort added entries, to be sure that /root/addedDir/ comes |
|---|
| 253 | # before /root/addedDir/addedSubdir |
|---|
| 254 | added.sort(lambda x,y: cmp(x.name, y.name)) |
|---|
| 255 | |
|---|
| 256 | # Sort removes in reverse order, to delete directories after |
|---|
| 257 | # their contents. |
|---|
| 258 | removed.sort(lambda x,y: cmp(y.name, x.name)) |
|---|
| 259 | |
|---|
| 260 | # Replay the actions |
|---|
| 261 | |
|---|
| 262 | if renamed and removed: |
|---|
| 263 | # Handle the "replace" operation, that is a remove+rename |
|---|
| 264 | |
|---|
| 265 | renames = [e.name for e in renamed] |
|---|
| 266 | removesfirst = [] |
|---|
| 267 | for rem in removed: |
|---|
| 268 | if rem in renames: |
|---|
| 269 | removesfirst.append(rem) |
|---|
| 270 | |
|---|
| 271 | if removedfirst: |
|---|
| 272 | self._removeEntries(removedfirst) |
|---|
| 273 | for rem in removesfirst: |
|---|
| 274 | removed.remove(rem) |
|---|
| 275 | |
|---|
| 276 | if renamed: self._renameEntries(renamed) |
|---|
| 277 | if removed: self._removeEntries(removed) |
|---|
| 278 | if added: self._addEntries(added) |
|---|
| 279 | |
|---|
| 280 | # Finally, deal with "copied" directories. The simple way is |
|---|
| 281 | # executing an _addSubtree on each of them, evenif this may |
|---|
| 282 | # cause "warnings" on items just moved/added above... |
|---|
| 283 | |
|---|
| 284 | while added: |
|---|
| 285 | subdir = added.pop(0).name |
|---|
| 286 | if isdir(join(self.basedir, subdir)): |
|---|
| 287 | self._addSubtree(subdir) |
|---|
| 288 | added = [e for e in added if not e.name.startswith(subdir)] |
|---|
| 289 | |
|---|
| 290 | def _addEntries(self, entries): |
|---|
| 291 | """ |
|---|
| 292 | Add a sequence of entries |
|---|
| 293 | """ |
|---|
| 294 | |
|---|
| 295 | self._addPathnames([e.name for e in entries]) |
|---|
| 296 | |
|---|
| 297 | def _addPathnames(self, names): |
|---|
| 298 | """ |
|---|
| 299 | Add some new filesystem objects. |
|---|
| 300 | """ |
|---|
| 301 | |
|---|
| 302 | raise "%s should override this method" % self.__class__ |
|---|
| 303 | |
|---|
| 304 | def _addSubtree(self, subdir): |
|---|
| 305 | """ |
|---|
| 306 | Add a whole subtree. |
|---|
| 307 | |
|---|
| 308 | This implementation crawl down the whole subtree, adding |
|---|
| 309 | entries (subdirs, skipping the usual VC-specific control |
|---|
| 310 | directories such as ``.svn``, ``_darcs`` or ``CVS``, and |
|---|
| 311 | files). |
|---|
| 312 | |
|---|
| 313 | Subclasses may use a better way, if the backend implements |
|---|
| 314 | a recursive add that skips the various metadata directories. |
|---|
| 315 | """ |
|---|
| 316 | |
|---|
| 317 | from os.path import join |
|---|
| 318 | from os import walk |
|---|
| 319 | from dualwd import IGNORED_METADIRS |
|---|
| 320 | |
|---|
| 321 | exclude = [] |
|---|
| 322 | |
|---|
| 323 | if self.state_file.filename.startswith(self.basedir): |
|---|
| 324 | sfrelname = self.state_file.filename[len(self.basedir)+1:] |
|---|
| 325 | exclude.append(sfrelname) |
|---|
| 326 | exclude.append(sfrelname+'.journal') |
|---|
| 327 | |
|---|
| 328 | if self.logfile.startswith(self.basedir): |
|---|
| 329 | exclude.append(self.logfile[len(self.basedir)+1:]) |
|---|
| 330 | |
|---|
| 331 | if subdir and subdir<>'.': |
|---|
| 332 | self._addPathnames([subdir]) |
|---|
| 333 | |
|---|
| 334 | for dir, subdirs, files in walk(join(self.basedir, subdir)): |
|---|
| 335 | for excd in IGNORED_METADIRS: |
|---|
| 336 | if excd in subdirs: |
|---|
| 337 | subdirs.remove(excd) |
|---|
| 338 | |
|---|
| 339 | for excf in exclude: |
|---|
| 340 | if excf in files: |
|---|
| 341 | files.remove(excf) |
|---|
| 342 | |
|---|
| 343 | if subdirs or files: |
|---|
| 344 | self._addPathnames([join(dir, df)[len(self.basedir)+1:] |
|---|
| 345 | for df in subdirs + files]) |
|---|
| 346 | |
|---|
| 347 | def _commit(self, date, author, patchname, changelog=None, entries=None): |
|---|
| 348 | """ |
|---|
| 349 | Commit the changeset. |
|---|
| 350 | """ |
|---|
| 351 | |
|---|
| 352 | raise "%s should override this method" % self.__class__ |
|---|
| 353 | |
|---|
| 354 | def _removeEntries(self, entries): |
|---|
| 355 | """ |
|---|
| 356 | Remove a sequence of entries. |
|---|
| 357 | """ |
|---|
| 358 | |
|---|
| 359 | self._removePathnames([e.name for e in entries]) |
|---|
| 360 | |
|---|
| 361 | def _removePathnames(self, names): |
|---|
| 362 | """ |
|---|
| 363 | Remove some filesystem object. |
|---|
| 364 | """ |
|---|
| 365 | |
|---|
| 366 | raise "%s should override this method" % self.__class__ |
|---|
| 367 | |
|---|
| 368 | def _renameEntries(self, entries): |
|---|
| 369 | """ |
|---|
| 370 | Rename a sequence of entries, adding all the parent directories |
|---|
| 371 | of each entry. |
|---|
| 372 | """ |
|---|
| 373 | |
|---|
| 374 | from os.path import split |
|---|
| 375 | |
|---|
| 376 | added = [] |
|---|
| 377 | for e in entries: |
|---|
| 378 | parents = [] |
|---|
| 379 | parent = split(e.name)[0] |
|---|
| 380 | while parent: |
|---|
| 381 | if not parent in added: |
|---|
| 382 | parents.append(parent) |
|---|
| 383 | added.append(parent) |
|---|
| 384 | parent = split(parent)[0] |
|---|
| 385 | if parents: |
|---|
| 386 | parents.reverse() |
|---|
| 387 | self._addPathnames(parents) |
|---|
| 388 | |
|---|
| 389 | self._renamePathname(e.old_name, e.name) |
|---|
| 390 | |
|---|
| 391 | def _renamePathname(self, oldname, newname): |
|---|
| 392 | """ |
|---|
| 393 | Rename a filesystem object to some other name/location. |
|---|
| 394 | """ |
|---|
| 395 | |
|---|
| 396 | raise "%s should override this method" % self.__class__ |
|---|
| 397 | |
|---|
| 398 | def prepareWorkingDirectory(self, source_repo): |
|---|
| 399 | """ |
|---|
| 400 | Do anything required to setup the hosting working directory. |
|---|
| 401 | """ |
|---|
| 402 | |
|---|
| 403 | self._prepareWorkingDirectory(source_repo) |
|---|
| 404 | |
|---|
| 405 | def _prepareWorkingDirectory(self, source_repo): |
|---|
| 406 | """ |
|---|
| 407 | Possibly checkout a working copy of the target VC, that will host the |
|---|
| 408 | upstream source tree, when overriden by subclasses. |
|---|
| 409 | """ |
|---|
| 410 | |
|---|
| 411 | def prepareTargetRepository(self): |
|---|
| 412 | """ |
|---|
| 413 | Do anything required to host the target repository. |
|---|
| 414 | """ |
|---|
| 415 | |
|---|
| 416 | from os import makedirs |
|---|
| 417 | from os.path import join, exists |
|---|
| 418 | |
|---|
| 419 | if not exists(self.basedir): |
|---|
| 420 | makedirs(self.basedir) |
|---|
| 421 | |
|---|
| 422 | self._prepareTargetRepository() |
|---|
| 423 | |
|---|
| 424 | prefix = self.__getPrefixToSource() |
|---|
| 425 | if prefix: |
|---|
| 426 | if not exists(join(self.basedir, prefix)): |
|---|
| 427 | # At bootstrap time, we assume that if the user |
|---|
| 428 | # extracted the source manually, she added |
|---|
| 429 | # the subdir, before doing that. |
|---|
| 430 | makedirs(join(self.basedir, prefix)) |
|---|
| 431 | self._addPathnames([prefix]) |
|---|
| 432 | |
|---|
| 433 | def _prepareTargetRepository(self): |
|---|
| 434 | """ |
|---|
| 435 | Possibly create or connect to the repository, when overriden |
|---|
| 436 | by subclasses. |
|---|
| 437 | """ |
|---|
| 438 | |
|---|
| 439 | def importFirstRevision(self, source_repo, changeset, initial): |
|---|
| 440 | """ |
|---|
| 441 | Initialize a new working directory, just extracted from |
|---|
| 442 | some other VC system, importing everything's there. |
|---|
| 443 | """ |
|---|
| 444 | |
|---|
| 445 | self._initializeWorkingDir() |
|---|
| 446 | # Execute the precommit hooks, but ignore None results |
|---|
| 447 | changeset = self._adaptChangeset(changeset) or changeset |
|---|
| 448 | revision = changeset.revision |
|---|
| 449 | source_repository = str(source_repo) |
|---|
| 450 | if initial: |
|---|
| 451 | author = changeset.author |
|---|
| 452 | patchname, log = self.__getPatchNameAndLog(changeset) |
|---|
| 453 | else: |
|---|
| 454 | author = "%s@%s" % (AUTHOR, HOST) |
|---|
| 455 | patchname = BOOTSTRAP_PATCHNAME |
|---|
| 456 | log = BOOTSTRAP_CHANGELOG % locals() |
|---|
| 457 | self._commit(changeset.date, author, patchname, log) |
|---|
| 458 | for tag in changeset.tags: |
|---|
| 459 | self._tag(tag) |
|---|
| 460 | self._dismissChangeset(changeset) |
|---|
| 461 | |
|---|
| 462 | def _initializeWorkingDir(self): |
|---|
| 463 | """ |
|---|
| 464 | Assuming the ``basedir`` directory contains a working copy ``module`` |
|---|
| 465 | extracted from some VC repository, add it and all its content |
|---|
| 466 | to the target repository. |
|---|
| 467 | |
|---|
| 468 | This implementation recursively add every file in the subtree. |
|---|
| 469 | Subclasses should override this method doing whatever is |
|---|
| 470 | appropriate for the backend. |
|---|
| 471 | """ |
|---|
| 472 | |
|---|
| 473 | self._addSubtree('.') |
|---|
| 474 | |
|---|
| 475 | def _tag(self, tagname): |
|---|
| 476 | """ |
|---|
| 477 | Tag the current version, if the VC type supports it, otherwise |
|---|
| 478 | do nothing. |
|---|
| 479 | """ |
|---|
| 480 | pass |
|---|