| 1 | # -*- mode: python; coding: utf-8 -*- |
|---|
| 2 | # :Progetto: Tailor -- Darcs peculiarities when used as a target |
|---|
| 3 | # :Creato: lun 10 lug 2006 00:12:15 CEST |
|---|
| 4 | # :Autore: Lele Gaifax <lele@nautilus.homeip.net> |
|---|
| 5 | # :Licenza: GNU General Public License |
|---|
| 6 | # |
|---|
| 7 | |
|---|
| 8 | """ |
|---|
| 9 | This module contains the target specific bits of the darcs backend. |
|---|
| 10 | """ |
|---|
| 11 | |
|---|
| 12 | __docformat__ = 'reStructuredText' |
|---|
| 13 | |
|---|
| 14 | from os.path import join, exists |
|---|
| 15 | import re |
|---|
| 16 | |
|---|
| 17 | from vcpx.shwrap import ExternalCommand, PIPE, STDOUT |
|---|
| 18 | from vcpx.target import ChangesetReplayFailure, SynchronizableTargetWorkingDir, \ |
|---|
| 19 | PostCommitCheckFailure |
|---|
| 20 | from vcpx.tzinfo import UTC |
|---|
| 21 | |
|---|
| 22 | |
|---|
| 23 | MOTD = """\ |
|---|
| 24 | Tailorized equivalent of |
|---|
| 25 | %s |
|---|
| 26 | """ |
|---|
| 27 | |
|---|
| 28 | |
|---|
| 29 | class DarcsTargetWorkingDir(SynchronizableTargetWorkingDir): |
|---|
| 30 | """ |
|---|
| 31 | A target working directory under ``darcs``. |
|---|
| 32 | """ |
|---|
| 33 | |
|---|
| 34 | def importFirstRevision(self, source_repo, changeset, initial): |
|---|
| 35 | from os import walk, sep |
|---|
| 36 | from vcpx.dualwd import IGNORED_METADIRS |
|---|
| 37 | |
|---|
| 38 | if not self.repository.split_initial_import_level: |
|---|
| 39 | super(DarcsTargetWorkingDir, self).importFirstRevision( |
|---|
| 40 | source_repo, changeset, initial) |
|---|
| 41 | else: |
|---|
| 42 | cmd = self.repository.command("add", "--case-ok", "--quiet") |
|---|
| 43 | add = ExternalCommand(cwd=self.repository.basedir, command=cmd) |
|---|
| 44 | cmd = self.repository.command("add", "--case-ok", "--recursive", |
|---|
| 45 | "--quiet") |
|---|
| 46 | addrecurs = ExternalCommand(cwd=self.repository.basedir, command=cmd) |
|---|
| 47 | for root, dirs, files in walk(self.repository.basedir): |
|---|
| 48 | subtree = root[len(self.repository.basedir)+1:] |
|---|
| 49 | if subtree: |
|---|
| 50 | log = "Import of subtree %s" % subtree |
|---|
| 51 | level = len(subtree.split(sep)) |
|---|
| 52 | else: |
|---|
| 53 | log = "Import of first level" |
|---|
| 54 | level = 0 |
|---|
| 55 | for excd in IGNORED_METADIRS: |
|---|
| 56 | if excd in dirs: |
|---|
| 57 | dirs.remove(excd) |
|---|
| 58 | if level>self.repository.split_initial_import_level: |
|---|
| 59 | while dirs: |
|---|
| 60 | d = dirs.pop(0) |
|---|
| 61 | addrecurs.execute(join(subtree, d)) |
|---|
| 62 | filenames = [join(subtree, f) for f in files] |
|---|
| 63 | if filenames: |
|---|
| 64 | add.execute(*filenames) |
|---|
| 65 | else: |
|---|
| 66 | dirnames = [join(subtree, d) for d in dirs] |
|---|
| 67 | if dirnames: |
|---|
| 68 | add.execute(*dirnames) |
|---|
| 69 | filenames = [join(subtree, f) for f in files] |
|---|
| 70 | if filenames: |
|---|
| 71 | add.execute(*filenames) |
|---|
| 72 | self._commit(changeset.date, "tailor", "Initial import", |
|---|
| 73 | log, isinitialcommit=initial) |
|---|
| 74 | |
|---|
| 75 | cmd = self.repository.command("tag", "--author", "tailor") |
|---|
| 76 | ExternalCommand(cwd=self.repository.basedir, command=cmd).execute( |
|---|
| 77 | "Initial import from %s" % source_repo.repository) |
|---|
| 78 | |
|---|
| 79 | def _addPathnames(self, names): |
|---|
| 80 | """ |
|---|
| 81 | Add some new filesystem objects. |
|---|
| 82 | """ |
|---|
| 83 | |
|---|
| 84 | cmd = self.repository.command("add", "--case-ok", "--not-recursive", |
|---|
| 85 | "--quiet") |
|---|
| 86 | ExternalCommand(cwd=self.repository.basedir, command=cmd).execute(names) |
|---|
| 87 | |
|---|
| 88 | def _addSubtree(self, subdir): |
|---|
| 89 | """ |
|---|
| 90 | Use the --recursive variant of ``darcs add`` to add a subtree. |
|---|
| 91 | """ |
|---|
| 92 | |
|---|
| 93 | cmd = self.repository.command("add", "--case-ok", "--recursive", |
|---|
| 94 | "--quiet") |
|---|
| 95 | add = ExternalCommand(cwd=self.repository.basedir, command=cmd, |
|---|
| 96 | ok_status=(0,2)) |
|---|
| 97 | output = add.execute(subdir, stdout=PIPE, stderr=STDOUT)[0] |
|---|
| 98 | if add.exit_status and add.exit_status!=2: |
|---|
| 99 | self.log.warning("%s returned status %d, saying %s", |
|---|
| 100 | str(add), add.exit_status, output.read()) |
|---|
| 101 | |
|---|
| 102 | def _commit(self, date, author, patchname, changelog=None, entries=None, |
|---|
| 103 | tags = [], isinitialcommit = False): |
|---|
| 104 | """ |
|---|
| 105 | Commit the changeset. |
|---|
| 106 | """ |
|---|
| 107 | |
|---|
| 108 | from os import rename, unlink |
|---|
| 109 | |
|---|
| 110 | logmessage = [] |
|---|
| 111 | |
|---|
| 112 | logmessage.append(date.astimezone(UTC).strftime('%Y/%m/%d %H:%M:%S UTC')) |
|---|
| 113 | logmessage.append(author) |
|---|
| 114 | if patchname: |
|---|
| 115 | logmessage.append(patchname) |
|---|
| 116 | else: |
|---|
| 117 | # This is possibile also when REMOVE_FIRST_LOG_LINE is in |
|---|
| 118 | # effect and the changelog starts with newlines: discard |
|---|
| 119 | # those, otherwise darcs will complain about invalid patch |
|---|
| 120 | # name |
|---|
| 121 | if changelog and changelog.startswith('\n'): |
|---|
| 122 | while changelog.startswith('\n'): |
|---|
| 123 | changelog = changelog[1:] |
|---|
| 124 | if changelog: |
|---|
| 125 | logmessage.append(changelog) |
|---|
| 126 | |
|---|
| 127 | if not logmessage: |
|---|
| 128 | logmessage.append('Unnamed patch') |
|---|
| 129 | |
|---|
| 130 | cmd = self.repository.command("record", "--all", "--pipe", "--ignore-times") |
|---|
| 131 | if not entries: |
|---|
| 132 | entries = ['.'] |
|---|
| 133 | |
|---|
| 134 | record = ExternalCommand(cwd=self.repository.basedir, command=cmd) |
|---|
| 135 | output = record.execute(input=self.repository.encode('\n'.join(logmessage)), |
|---|
| 136 | stdout=PIPE, stderr=STDOUT)[0] |
|---|
| 137 | |
|---|
| 138 | if record.exit_status: |
|---|
| 139 | pending = join(self.repository.basedir, '_darcs', 'patches', 'pending') |
|---|
| 140 | if exists(pending): |
|---|
| 141 | wrongpending = pending + '.wrong' |
|---|
| 142 | if exists(wrongpending): |
|---|
| 143 | unlink(wrongpending) |
|---|
| 144 | rename(pending, wrongpending) |
|---|
| 145 | self.log.debug("Pending file renamed to %s", wrongpending) |
|---|
| 146 | raise ChangesetReplayFailure( |
|---|
| 147 | "%s returned status %d, saying: %s" % (str(record), |
|---|
| 148 | record.exit_status, |
|---|
| 149 | output.read())) |
|---|
| 150 | |
|---|
| 151 | def _postCommitCheck(self): |
|---|
| 152 | # If we are using --look-for-adds on commit this is useless |
|---|
| 153 | if not self.repository.use_look_for_adds: |
|---|
| 154 | cmd = self.repository.command("whatsnew", "--summary", "--look-for-add") |
|---|
| 155 | whatsnew = ExternalCommand(cwd=self.repository.basedir, command=cmd, ok_status=(1,)) |
|---|
| 156 | output = whatsnew.execute(stdout=PIPE, stderr=STDOUT)[0] |
|---|
| 157 | if not whatsnew.exit_status: |
|---|
| 158 | raise PostCommitCheckFailure( |
|---|
| 159 | "Changes left in working dir after commit:\n%s" % output.read()) |
|---|
| 160 | |
|---|
| 161 | def _replayChangeset(self, changeset): |
|---|
| 162 | """ |
|---|
| 163 | Instead of using the "darcs mv" command, manually add |
|---|
| 164 | the rename to the pending file: this is a dirty trick, that |
|---|
| 165 | allows darcs to handle the case when the source changeset |
|---|
| 166 | is something like:: |
|---|
| 167 | $ bzr mv A B |
|---|
| 168 | $ touch A |
|---|
| 169 | $ bzr add A |
|---|
| 170 | where A is actually replaced, and old A is now B. Since by the |
|---|
| 171 | time the changeset gets replayed, the source has already replaced |
|---|
| 172 | A with its new content, darcs would move the *wrong* A to B... |
|---|
| 173 | """ |
|---|
| 174 | |
|---|
| 175 | # The "_darcs/patches/pending" file is basically a patch containing |
|---|
| 176 | # only the changes (hunks, adds...) not yet recorded by darcs: it does |
|---|
| 177 | # contain either a single change (that is, exactly one line), or a |
|---|
| 178 | # collection of changes, with opening and closing curl braces. |
|---|
| 179 | # Filenames must begin with "./", and eventual spaces replaced by '\32\'. |
|---|
| 180 | # Order is significant! |
|---|
| 181 | |
|---|
| 182 | pending = join(self.repository.basedir, '_darcs', 'patches', 'pending') |
|---|
| 183 | if exists(pending): |
|---|
| 184 | p = open(pending).readlines() |
|---|
| 185 | if p[0] != '{\n': |
|---|
| 186 | p.insert(0, '{\n') |
|---|
| 187 | p.append('}\n') |
|---|
| 188 | else: |
|---|
| 189 | p = [ '{\n', '}\n' ] |
|---|
| 190 | |
|---|
| 191 | entries = [] |
|---|
| 192 | |
|---|
| 193 | while changeset.entries: |
|---|
| 194 | e = changeset.entries.pop(0) |
|---|
| 195 | if e.action_kind == e.DELETED: |
|---|
| 196 | elide = False |
|---|
| 197 | for j,oe in enumerate(changeset.entries): |
|---|
| 198 | if oe.action_kind == oe.ADDED and e.name == oe.name: |
|---|
| 199 | self.log.debug('Collapsing a %s and a %s on %s, assuming ' |
|---|
| 200 | 'an upstream "replacement"', |
|---|
| 201 | e.action_kind, oe.action_kind, oe.name) |
|---|
| 202 | del changeset.entries[j] |
|---|
| 203 | elide = True |
|---|
| 204 | break |
|---|
| 205 | if not elide: |
|---|
| 206 | entries.append(e) |
|---|
| 207 | elif e.action_kind == e.ADDED: |
|---|
| 208 | elide = False |
|---|
| 209 | for j,oe in enumerate(changeset.entries): |
|---|
| 210 | if oe.action_kind == oe.DELETED and e.name == oe.name: |
|---|
| 211 | self.log.debug('Collapsing a %s and a %s on %s, assuming ' |
|---|
| 212 | 'an upstream "replacement"', |
|---|
| 213 | e.action_kind, oe.action_kind, oe.name) |
|---|
| 214 | del changeset.entries[j] |
|---|
| 215 | elide = True |
|---|
| 216 | break |
|---|
| 217 | if not elide: |
|---|
| 218 | entries.append(e) |
|---|
| 219 | else: |
|---|
| 220 | entries.append(e) |
|---|
| 221 | |
|---|
| 222 | changed = False |
|---|
| 223 | for e in entries: |
|---|
| 224 | if e.action_kind == e.RENAMED: |
|---|
| 225 | self.log.debug('Mimicing "darcs mv %s %s"', |
|---|
| 226 | e.old_name, e.name) |
|---|
| 227 | oname = e.old_name.replace(' ', '\\32\\') |
|---|
| 228 | nname = e.name.replace(' ', '\\32\\') |
|---|
| 229 | p.insert(-1, 'move ./%s ./%s\n' % (oname, nname)) |
|---|
| 230 | changed = True |
|---|
| 231 | elif e.action_kind == e.ADDED: |
|---|
| 232 | self.log.debug('Mimicing "darcs add %s"', e.name) |
|---|
| 233 | name = e.name.replace(' ', '\\32\\') |
|---|
| 234 | if e.is_directory: |
|---|
| 235 | p.insert(-1, 'adddir ./%s\n' % name) |
|---|
| 236 | else: |
|---|
| 237 | p.insert(-1, 'addfile ./%s\n' % name) |
|---|
| 238 | changed = True |
|---|
| 239 | elif e.action_kind == e.DELETED: |
|---|
| 240 | self.log.debug('Mimicing "darcs rm %s"', e.name) |
|---|
| 241 | name = e.name.replace(' ', '\\32\\') |
|---|
| 242 | if e.is_directory: |
|---|
| 243 | p.insert(-1, 'rmdir ./%s\n' % name) |
|---|
| 244 | else: |
|---|
| 245 | p.insert(-1, 'rmfile ./%s\n' % name) |
|---|
| 246 | changed = True |
|---|
| 247 | if changed: |
|---|
| 248 | open(pending, 'w').writelines(p) |
|---|
| 249 | return True |
|---|
| 250 | |
|---|
| 251 | def _prepareTargetRepository(self): |
|---|
| 252 | """ |
|---|
| 253 | Create the base directory if it doesn't exist, and execute |
|---|
| 254 | ``darcs initialize`` if needed. |
|---|
| 255 | """ |
|---|
| 256 | |
|---|
| 257 | metadir = join(self.repository.basedir, '_darcs') |
|---|
| 258 | |
|---|
| 259 | if not exists(metadir): |
|---|
| 260 | self.repository.create() |
|---|
| 261 | |
|---|
| 262 | prefsdir = join(metadir, 'prefs') |
|---|
| 263 | prefsname = join(prefsdir, 'prefs') |
|---|
| 264 | boringname = join(prefsdir, 'boring') |
|---|
| 265 | if exists(prefsname): |
|---|
| 266 | for pref in open(prefsname, 'rU'): |
|---|
| 267 | if pref: |
|---|
| 268 | pname, pvalue = pref.split(' ', 1) |
|---|
| 269 | if pname == 'boringfile': |
|---|
| 270 | boringname = join(self.repository.basedir, pvalue[:-1]) |
|---|
| 271 | |
|---|
| 272 | boring = open(boringname, 'rU') |
|---|
| 273 | ignored = boring.read().rstrip().split('\n') |
|---|
| 274 | boring.close() |
|---|
| 275 | |
|---|
| 276 | # Build a list of compiled regular expressions, that will be |
|---|
| 277 | # used later to filter the entries. |
|---|
| 278 | self.__unwanted_entries = [re.compile(rx) for rx in ignored |
|---|
| 279 | if rx and not rx.startswith('#')] |
|---|
| 280 | |
|---|
| 281 | def _prepareWorkingDirectory(self, source_repo): |
|---|
| 282 | """ |
|---|
| 283 | Tweak the default settings of the repository. |
|---|
| 284 | """ |
|---|
| 285 | |
|---|
| 286 | motd = open(join(self.repository.basedir, '_darcs/prefs/motd'), 'w') |
|---|
| 287 | motd.write(MOTD % str(source_repo)) |
|---|
| 288 | motd.close() |
|---|
| 289 | |
|---|
| 290 | def _adaptEntries(self, changeset): |
|---|
| 291 | """ |
|---|
| 292 | Filter out boring files. |
|---|
| 293 | """ |
|---|
| 294 | |
|---|
| 295 | from copy import copy |
|---|
| 296 | |
|---|
| 297 | adapted = SynchronizableTargetWorkingDir._adaptEntries(self, changeset) |
|---|
| 298 | |
|---|
| 299 | # If there are no entries or no rules, there's nothing to do |
|---|
| 300 | if not adapted or not adapted.entries or not self.__unwanted_entries: |
|---|
| 301 | return adapted |
|---|
| 302 | |
|---|
| 303 | entries = [] |
|---|
| 304 | skipped = False |
|---|
| 305 | for e in adapted.entries: |
|---|
| 306 | skip = False |
|---|
| 307 | for rx in self.__unwanted_entries: |
|---|
| 308 | if rx.search(e.name): |
|---|
| 309 | skip = True |
|---|
| 310 | break |
|---|
| 311 | if skip: |
|---|
| 312 | self.log.info('Entry "%s" skipped per boring rules', e.name) |
|---|
| 313 | skipped = True |
|---|
| 314 | else: |
|---|
| 315 | entries.append(e) |
|---|
| 316 | |
|---|
| 317 | # All entries are gone, don't commit this changeset |
|---|
| 318 | if not entries: |
|---|
| 319 | self.log.info('All entries ignored, skipping whole ' |
|---|
| 320 | 'changeset "%s"', changeset.revision) |
|---|
| 321 | return None |
|---|
| 322 | |
|---|
| 323 | if skipped: |
|---|
| 324 | adapted = copy(adapted) |
|---|
| 325 | adapted.entries = entries |
|---|
| 326 | |
|---|
| 327 | return adapted |
|---|
| 328 | |
|---|
| 329 | def _tag(self, tag, date, author): |
|---|
| 330 | """ |
|---|
| 331 | Apply the given tag to the repository, unless it has already |
|---|
| 332 | been applied to the current state. (If it has been applied to |
|---|
| 333 | an earlier state, do apply it; the later tag overrides the |
|---|
| 334 | earlier one. |
|---|
| 335 | """ |
|---|
| 336 | if tag not in self._currentTags(): |
|---|
| 337 | cmd = self.repository.command("tag", "--author", "Unknown tagger") |
|---|
| 338 | ExternalCommand(cwd=self.repository.basedir, command=cmd).execute(tag) |
|---|
| 339 | |
|---|
| 340 | def _currentTags(self): |
|---|
| 341 | """ |
|---|
| 342 | Return a list of tags that refer to the repository's current |
|---|
| 343 | state. Does not consider tags themselves to be part of the |
|---|
| 344 | state, so if the repo was tagged with T1 and then T2, then |
|---|
| 345 | both T1 and T2 are considered to refer to the current state, |
|---|
| 346 | even though 'darcs get --tag=T1' and 'darcs get --tag=T2' |
|---|
| 347 | would have different results (the latter creates a repo that |
|---|
| 348 | contains tag T2, but the former does not). |
|---|
| 349 | |
|---|
| 350 | This function assumes that a tag depends on all patches that |
|---|
| 351 | precede it in the "darcs changes" list. This assumption is |
|---|
| 352 | valid if tags only come into the repository via tailor; if the |
|---|
| 353 | user applies a tag by hand in the hybrid repository, or pulls |
|---|
| 354 | in a tag from another darcs repository, then the assumption |
|---|
| 355 | could be violated and mistagging could result. |
|---|
| 356 | """ |
|---|
| 357 | |
|---|
| 358 | from vcpx.repository.darcs.source import changesets_from_darcschanges_unsafe |
|---|
| 359 | |
|---|
| 360 | cmd = self.repository.command("changes", |
|---|
| 361 | "--from-match", "not name ^TAG", |
|---|
| 362 | "--xml-output", "--reverse") |
|---|
| 363 | changes = ExternalCommand(cwd=self.repository.basedir, command=cmd) |
|---|
| 364 | output = changes.execute(stdout=PIPE)[0] |
|---|
| 365 | if changes.exit_status: |
|---|
| 366 | raise ChangesetReplayFailure( |
|---|
| 367 | "%s returned status %d saying\n%s" % |
|---|
| 368 | (str(changes), changes.exit_status, output.read())) |
|---|
| 369 | |
|---|
| 370 | tags = [] |
|---|
| 371 | for cs in changesets_from_darcschanges_unsafe(output): |
|---|
| 372 | for tag in cs.tags: |
|---|
| 373 | if tag not in tags: |
|---|
| 374 | tags.append(tag) |
|---|
| 375 | return tags |
|---|