| 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 | |
|---|
| 24 | Repository: %(source_repository)s%(source_module)s |
|---|
| 25 | Revision: %(revision)s |
|---|
| 26 | """ |
|---|
| 27 | |
|---|
| 28 | class TargetInitializationFailure(Exception): |
|---|
| 29 | "Failure initializing the target VCS" |
|---|
| 30 | |
|---|
| 31 | class SyncronizableTargetWorkingDir(WorkingDir): |
|---|
| 32 | """ |
|---|
| 33 | This is an abstract working dir usable as a *shadow* of another |
|---|
| 34 | kind of VC, sharing the same working directory. |
|---|
| 35 | |
|---|
| 36 | Most interesting entry points are: |
|---|
| 37 | |
|---|
| 38 | replayChangeset |
|---|
| 39 | to replay an already applied changeset, to mimic the actions |
|---|
| 40 | performed by the upstream VC system on the tree such as |
|---|
| 41 | renames, deletions and adds. This is an useful argument to |
|---|
| 42 | feed as ``replay`` to ``applyUpstreamChangesets`` |
|---|
| 43 | |
|---|
| 44 | importFirstRevision |
|---|
| 45 | to initialize a pristine working directory tree under this VC |
|---|
| 46 | system, possibly extracted under a different kind of VC |
|---|
| 47 | |
|---|
| 48 | Subclasses MUST override at least the _underscoredMethods. |
|---|
| 49 | """ |
|---|
| 50 | |
|---|
| 51 | PATCH_NAME_FORMAT = 'Tailorized "%(revision)s"' |
|---|
| 52 | """ |
|---|
| 53 | The format string used to compute the patch name, used by underlying VCS. |
|---|
| 54 | """ |
|---|
| 55 | |
|---|
| 56 | REMOVE_FIRST_LOG_LINE = False |
|---|
| 57 | """ |
|---|
| 58 | When true, remove the first line from the upstream changelog. |
|---|
| 59 | """ |
|---|
| 60 | |
|---|
| 61 | def replayChangeset(self, changeset): |
|---|
| 62 | """ |
|---|
| 63 | Do whatever is needed to replay the changes under the target |
|---|
| 64 | VC, to register the already applied (under the other VC) |
|---|
| 65 | changeset. |
|---|
| 66 | """ |
|---|
| 67 | |
|---|
| 68 | try: |
|---|
| 69 | self._replayChangeset(changeset) |
|---|
| 70 | except: |
|---|
| 71 | self.log_error(str(changeset), exc=True) |
|---|
| 72 | raise |
|---|
| 73 | |
|---|
| 74 | if changeset.log == '': |
|---|
| 75 | firstlogline = 'Empty log message' |
|---|
| 76 | remaininglog = '' |
|---|
| 77 | else: |
|---|
| 78 | loglines = changeset.log.split('\n') |
|---|
| 79 | if len(loglines)>1: |
|---|
| 80 | firstlogline = loglines[0] |
|---|
| 81 | remaininglog = '\n'.join(loglines[1:]) |
|---|
| 82 | else: |
|---|
| 83 | firstlogline = changeset.log |
|---|
| 84 | remaininglog = '' |
|---|
| 85 | |
|---|
| 86 | patchname = self.PATCH_NAME_FORMAT % { |
|---|
| 87 | 'revision': changeset.revision, |
|---|
| 88 | 'author': changeset.author, |
|---|
| 89 | 'date': changeset.date, |
|---|
| 90 | 'firstlogline': firstlogline, |
|---|
| 91 | 'remaininglog': remaininglog} |
|---|
| 92 | if self.REMOVE_FIRST_LOG_LINE: |
|---|
| 93 | changelog = remaininglog |
|---|
| 94 | else: |
|---|
| 95 | changelog = changeset.log |
|---|
| 96 | entries = self._getCommitEntries(changeset) |
|---|
| 97 | self._commit(changeset.date, changeset.author, |
|---|
| 98 | patchname, changelog, entries) |
|---|
| 99 | |
|---|
| 100 | def _getCommitEntries(self, changeset): |
|---|
| 101 | """ |
|---|
| 102 | Extract the names of the entries for the commit phase. |
|---|
| 103 | """ |
|---|
| 104 | |
|---|
| 105 | return [e.name for e in changeset.entries] |
|---|
| 106 | |
|---|
| 107 | def _replayChangeset(self, changeset): |
|---|
| 108 | """ |
|---|
| 109 | Replicate the actions performed by the changeset on the tree of |
|---|
| 110 | files. |
|---|
| 111 | """ |
|---|
| 112 | |
|---|
| 113 | from os.path import join, isdir |
|---|
| 114 | |
|---|
| 115 | added = changeset.addedEntries() |
|---|
| 116 | renamed = changeset.renamedEntries() |
|---|
| 117 | removed = changeset.removedEntries() |
|---|
| 118 | |
|---|
| 119 | # Sort added entries, to be sure that /root/addedDir/ comes |
|---|
| 120 | # before /root/addedDir/addedSubdir |
|---|
| 121 | added.sort(lambda x,y: cmp(x.name, y.name)) |
|---|
| 122 | |
|---|
| 123 | # Sort removes in reverse order, to delete directories after |
|---|
| 124 | # their contents. |
|---|
| 125 | removed.sort(lambda x,y: cmp(y.name, x.name)) |
|---|
| 126 | |
|---|
| 127 | # Replay the actions |
|---|
| 128 | |
|---|
| 129 | if renamed: self._renameEntries(renamed) |
|---|
| 130 | if removed: self._removeEntries(removed) |
|---|
| 131 | if added: self._addEntries(added) |
|---|
| 132 | |
|---|
| 133 | # Finally, deal with "copied" directories. The simple way is |
|---|
| 134 | # executing an _addSubtree on each of them, evenif this may |
|---|
| 135 | # cause "warnings" on items just moved/added above... |
|---|
| 136 | |
|---|
| 137 | while added: |
|---|
| 138 | subdir = added.pop(0).name |
|---|
| 139 | if isdir(join(self.basedir, subdir)): |
|---|
| 140 | self._addSubtree(subdir) |
|---|
| 141 | added = [e for e in added if not e.name.startswith(subdir)] |
|---|
| 142 | |
|---|
| 143 | def _addEntries(self, entries): |
|---|
| 144 | """ |
|---|
| 145 | Add a sequence of entries |
|---|
| 146 | """ |
|---|
| 147 | |
|---|
| 148 | self._addPathnames([e.name for e in entries]) |
|---|
| 149 | |
|---|
| 150 | def _addPathnames(self, names): |
|---|
| 151 | """ |
|---|
| 152 | Add some new filesystem objects. |
|---|
| 153 | """ |
|---|
| 154 | |
|---|
| 155 | raise "%s should override this method" % self.__class__ |
|---|
| 156 | |
|---|
| 157 | def _addSubtree(self, subdir): |
|---|
| 158 | """ |
|---|
| 159 | Add a whole subtree. |
|---|
| 160 | |
|---|
| 161 | This implementation crawl down the whole subtree, adding |
|---|
| 162 | entries (subdirs, skipping the usual VC-specific control |
|---|
| 163 | directories such as ``.svn``, ``_darcs`` or ``CVS``, and |
|---|
| 164 | files). |
|---|
| 165 | |
|---|
| 166 | Subclasses may use a better way, if the backend implements |
|---|
| 167 | a recursive add that skips the various metadata directories. |
|---|
| 168 | """ |
|---|
| 169 | |
|---|
| 170 | from os.path import join |
|---|
| 171 | from os import walk |
|---|
| 172 | from dualwd import IGNORED_METADIRS |
|---|
| 173 | |
|---|
| 174 | exclude = [] |
|---|
| 175 | |
|---|
| 176 | if self.state_file.filename.startswith(self.basedir): |
|---|
| 177 | exclude.append(self.state_file.filename[len(self.basedir)+1:]) |
|---|
| 178 | |
|---|
| 179 | if self.logfile.startswith(self.basedir): |
|---|
| 180 | exclude.append(self.logfile[len(self.basedir)+1:]) |
|---|
| 181 | |
|---|
| 182 | if subdir and subdir<>'.': |
|---|
| 183 | self._addPathnames([subdir]) |
|---|
| 184 | |
|---|
| 185 | for dir, subdirs, files in walk(join(self.basedir, subdir)): |
|---|
| 186 | for excd in IGNORED_METADIRS: |
|---|
| 187 | if excd in subdirs: |
|---|
| 188 | subdirs.remove(excd) |
|---|
| 189 | |
|---|
| 190 | for excf in exclude: |
|---|
| 191 | if excf in files: |
|---|
| 192 | files.remove(excf) |
|---|
| 193 | |
|---|
| 194 | if subdirs or files: |
|---|
| 195 | self._addPathnames([join(dir, df)[len(self.basedir)+1:] |
|---|
| 196 | for df in subdirs + files]) |
|---|
| 197 | |
|---|
| 198 | def _commit(self, date, author, patchname, changelog=None, entries=None): |
|---|
| 199 | """ |
|---|
| 200 | Commit the changeset. |
|---|
| 201 | """ |
|---|
| 202 | |
|---|
| 203 | raise "%s should override this method" % self.__class__ |
|---|
| 204 | |
|---|
| 205 | def _removeEntries(self, entries): |
|---|
| 206 | """ |
|---|
| 207 | Remove a sequence of entries. |
|---|
| 208 | """ |
|---|
| 209 | |
|---|
| 210 | self._removePathnames([e.name for e in entries]) |
|---|
| 211 | |
|---|
| 212 | def _removePathnames(self, names): |
|---|
| 213 | """ |
|---|
| 214 | Remove some filesystem object. |
|---|
| 215 | """ |
|---|
| 216 | |
|---|
| 217 | raise "%s should override this method" % self.__class__ |
|---|
| 218 | |
|---|
| 219 | def _renameEntries(self, entries): |
|---|
| 220 | """ |
|---|
| 221 | Rename a sequence of entries, adding all the parent directories |
|---|
| 222 | of each entry. |
|---|
| 223 | """ |
|---|
| 224 | |
|---|
| 225 | from os.path import split |
|---|
| 226 | |
|---|
| 227 | added = [] |
|---|
| 228 | for e in entries: |
|---|
| 229 | parents = [] |
|---|
| 230 | parent = split(e.name)[0] |
|---|
| 231 | while parent: |
|---|
| 232 | if not parent in added: |
|---|
| 233 | parents.append(parent) |
|---|
| 234 | added.append(parent) |
|---|
| 235 | parent = split(parent)[0] |
|---|
| 236 | if parents: |
|---|
| 237 | parents.reverse() |
|---|
| 238 | self._addPathnames(parents) |
|---|
| 239 | |
|---|
| 240 | self._renamePathname(e.old_name, e.name) |
|---|
| 241 | |
|---|
| 242 | def _renamePathname(self, oldname, newname): |
|---|
| 243 | """ |
|---|
| 244 | Rename a filesystem object to some other name/location. |
|---|
| 245 | """ |
|---|
| 246 | |
|---|
| 247 | raise "%s should override this method" % self.__class__ |
|---|
| 248 | |
|---|
| 249 | def prepareWorkingDirectory(self, source_repo): |
|---|
| 250 | """ |
|---|
| 251 | Do anything required to setup the hosting working directory. |
|---|
| 252 | """ |
|---|
| 253 | |
|---|
| 254 | self._prepareTargetRepository(source_repo) |
|---|
| 255 | self._prepareWorkingDirectory(source_repo) |
|---|
| 256 | |
|---|
| 257 | def _prepareTargetRepository(self, source_repo): |
|---|
| 258 | """ |
|---|
| 259 | Possibly create the repository, when overriden by subclasses. |
|---|
| 260 | """ |
|---|
| 261 | |
|---|
| 262 | def _prepareWorkingDirectory(self, source_repo): |
|---|
| 263 | """ |
|---|
| 264 | Possibly checkout a working copy of the target VC, that will host the |
|---|
| 265 | upstream source tree, when overriden by subclasses. |
|---|
| 266 | """ |
|---|
| 267 | |
|---|
| 268 | def importFirstRevision(self, source_repo, changeset, initial): |
|---|
| 269 | """ |
|---|
| 270 | Initialize a new working directory, just extracted from |
|---|
| 271 | some other VC system, importing everything's there. |
|---|
| 272 | """ |
|---|
| 273 | |
|---|
| 274 | self._initializeWorkingDir() |
|---|
| 275 | revision = changeset.revision |
|---|
| 276 | source_repository = source_repo.repository |
|---|
| 277 | source_module = source_repo.module or '' |
|---|
| 278 | if initial: |
|---|
| 279 | author = changeset.author |
|---|
| 280 | patchname = changeset.log |
|---|
| 281 | log = None |
|---|
| 282 | else: |
|---|
| 283 | author = "%s@%s" % (AUTHOR, HOST) |
|---|
| 284 | patchname = BOOTSTRAP_PATCHNAME |
|---|
| 285 | log = BOOTSTRAP_CHANGELOG % locals() |
|---|
| 286 | self._commit(changeset.date, author, patchname, log) |
|---|
| 287 | |
|---|
| 288 | def _initializeWorkingDir(self): |
|---|
| 289 | """ |
|---|
| 290 | Assuming the ``basedir`` directory contains a working copy ``module`` |
|---|
| 291 | extracted from some VC repository, add it and all its content |
|---|
| 292 | to the target repository. |
|---|
| 293 | |
|---|
| 294 | This implementation recursively add every file in the subtree. |
|---|
| 295 | Subclasses should override this method doing whatever is |
|---|
| 296 | appropriate for the backend. |
|---|
| 297 | """ |
|---|
| 298 | |
|---|
| 299 | self._addSubtree('.') |
|---|