| 1 | # -*- mode: python; coding: utf-8 -*- |
|---|
| 2 | # :Progetto: vcpx -- Bazaar support using the bzrlib instead of the frontend |
|---|
| 3 | # :Creato: Fri Aug 19 01:06:08 CEST 2005 |
|---|
| 4 | # :Autore: Johan Rydberg <jrydberg@gnu.org> |
|---|
| 5 | # Jelmer Vernooij <jelmer@samba.org> |
|---|
| 6 | # Lalo Martins <lalo.martins@gmail.com> |
|---|
| 7 | # Olaf Conradi <olaf@conradi.org> |
|---|
| 8 | # :Licenza: GNU General Public License |
|---|
| 9 | # |
|---|
| 10 | |
|---|
| 11 | """ |
|---|
| 12 | This module implements the backends for Bazaar. |
|---|
| 13 | """ |
|---|
| 14 | |
|---|
| 15 | __docformat__ = 'reStructuredText' |
|---|
| 16 | |
|---|
| 17 | |
|---|
| 18 | from sys import version_info |
|---|
| 19 | assert version_info >= (2,4), "Bazaar backend requires Python 2.4" |
|---|
| 20 | del version_info |
|---|
| 21 | |
|---|
| 22 | from bzrlib import errors |
|---|
| 23 | from bzrlib.branch import Branch |
|---|
| 24 | from bzrlib.bzrdir import BzrDir |
|---|
| 25 | from bzrlib.errors import NoSuchRevision, TagsNotSupported |
|---|
| 26 | from bzrlib.missing import find_unmerged |
|---|
| 27 | from bzrlib.osutils import normpath, pathjoin |
|---|
| 28 | from bzrlib.plugin import load_plugins |
|---|
| 29 | |
|---|
| 30 | from vcpx.changes import Changeset, ChangesetEntry |
|---|
| 31 | from vcpx.repository import Repository |
|---|
| 32 | from vcpx.source import UpdatableSourceWorkingDir, ChangesetApplicationFailure |
|---|
| 33 | from vcpx.target import SynchronizableTargetWorkingDir |
|---|
| 34 | from vcpx.workdir import WorkingDir |
|---|
| 35 | |
|---|
| 36 | |
|---|
| 37 | class BzrChangeset(Changeset): |
|---|
| 38 | """ |
|---|
| 39 | Manage the particular reordering of the entries. |
|---|
| 40 | |
|---|
| 41 | Apparently TreeDelta doesn't expose the entries in a sensible order, |
|---|
| 42 | they are grouped by kind. |
|---|
| 43 | """ |
|---|
| 44 | |
|---|
| 45 | def __init__(self, revision, date, author, log, entries=None, **other): |
|---|
| 46 | """ |
|---|
| 47 | Initialize a new BzrChangeset, inserting the entries in a sensible order. |
|---|
| 48 | """ |
|---|
| 49 | |
|---|
| 50 | from os.path import split, join |
|---|
| 51 | |
|---|
| 52 | super(BzrChangeset, self).__init__(revision, date, author, log, entries=None, **other) |
|---|
| 53 | if entries is not None: |
|---|
| 54 | for e in entries: |
|---|
| 55 | self.addEntry(e, revision) |
|---|
| 56 | |
|---|
| 57 | # Adjust old_name on renamed entries: bzr tell us the *original* |
|---|
| 58 | # name of the rename... |
|---|
| 59 | # Consider this: |
|---|
| 60 | # |
|---|
| 61 | # $ bzr mv newnamedir/subdir/a newnamedir/subdir/b |
|---|
| 62 | # newnamedir/subdir/a => newnamedir/subdir/b |
|---|
| 63 | # $ bzr mv newnamedir/subdir newnamedir/newsubdir |
|---|
| 64 | # newnamedir/subdir => newnamedir/newsubdir |
|---|
| 65 | # $ bzr mv newnamedir dir |
|---|
| 66 | # newnamedir => dir |
|---|
| 67 | # $ bzr st |
|---|
| 68 | # renamed: |
|---|
| 69 | # newnamedir => dir |
|---|
| 70 | # newnamedir/subdir => dir/newsubdir |
|---|
| 71 | # newnamedir/subdir/a => dir/newsubdir/b |
|---|
| 72 | |
|---|
| 73 | renames = {} |
|---|
| 74 | for e in self.entries: |
|---|
| 75 | if e.action_kind == e.RENAMED: |
|---|
| 76 | renames[e.old_name] = e.name |
|---|
| 77 | d,f = split(e.old_name) |
|---|
| 78 | while d: |
|---|
| 79 | if d in renames: |
|---|
| 80 | e.old_name = join(renames[d], e.old_name[len(d)+1:]) |
|---|
| 81 | break |
|---|
| 82 | d,f = split(d) |
|---|
| 83 | |
|---|
| 84 | def addEntry(self, entry, revision): |
|---|
| 85 | """ |
|---|
| 86 | Fixup the ordering of the entries, by giving precedence to directories |
|---|
| 87 | """ |
|---|
| 88 | |
|---|
| 89 | if entry.action_kind in (entry.ADDED, entry.RENAMED) and entry.is_directory: |
|---|
| 90 | dirname = entry.name + '/' # does bzr on windows use this too? |
|---|
| 91 | for i,e in enumerate(self.entries): |
|---|
| 92 | if e.name.startswith(dirname): |
|---|
| 93 | self.entries.insert(i, entry) |
|---|
| 94 | return |
|---|
| 95 | elif entry.action_kind == entry.DELETED: |
|---|
| 96 | for i,e in enumerate(self.entries): |
|---|
| 97 | if e.action_kind == e.RENAMED and e.name == entry.name: |
|---|
| 98 | # This is the following case: |
|---|
| 99 | # $ bzr rm A |
|---|
| 100 | # $ bzr mv B A |
|---|
| 101 | self.entries.insert(i, entry) |
|---|
| 102 | return |
|---|
| 103 | elif (e.action_kind == e.DELETED |
|---|
| 104 | and e.is_directory |
|---|
| 105 | and entry.name.startswith(e.name)): |
|---|
| 106 | # Remove dir contents before dir itself |
|---|
| 107 | self.entries.insert(i, entry) |
|---|
| 108 | return |
|---|
| 109 | elif (e.action_kind == e.ADDED and e.name == entry.name): |
|---|
| 110 | # put replacement (rm+add) in the right order |
|---|
| 111 | self.entries.insert(i, entry) |
|---|
| 112 | return |
|---|
| 113 | |
|---|
| 114 | self.entries.append(entry) |
|---|
| 115 | |
|---|
| 116 | |
|---|
| 117 | class BzrRepository(Repository): |
|---|
| 118 | METADIR = '.bzr' |
|---|
| 119 | |
|---|
| 120 | def _load(self, project): |
|---|
| 121 | Repository._load(self, project) |
|---|
| 122 | ppath = project.config.get(self.name, 'python-path') |
|---|
| 123 | if ppath: |
|---|
| 124 | from sys import path |
|---|
| 125 | |
|---|
| 126 | if ppath not in path: |
|---|
| 127 | path.insert(0, ppath) |
|---|
| 128 | |
|---|
| 129 | def create(self): |
|---|
| 130 | """ |
|---|
| 131 | Create a branch with a working tree at the base directory. If the base |
|---|
| 132 | directory is inside a Bazaar style "shared repository", it will use |
|---|
| 133 | that to create a branch and working tree (make sure it allows working |
|---|
| 134 | trees). |
|---|
| 135 | """ |
|---|
| 136 | |
|---|
| 137 | self.log.info('Initializing new repository in %r...', self.basedir) |
|---|
| 138 | try: |
|---|
| 139 | bzrdir = BzrDir.open(self.basedir) |
|---|
| 140 | except errors.NotBranchError: |
|---|
| 141 | # really a NotBzrDir error... |
|---|
| 142 | branch = BzrDir.create_branch_convenience(self.basedir, force_new_tree=True) |
|---|
| 143 | wtree = branch.bzrdir.open_workingtree() |
|---|
| 144 | else: |
|---|
| 145 | bzrdir.create_branch() |
|---|
| 146 | wtree = bzrdir.create_workingtree() |
|---|
| 147 | |
|---|
| 148 | return wtree |
|---|
| 149 | |
|---|
| 150 | |
|---|
| 151 | class BzrWorkingDir(UpdatableSourceWorkingDir, SynchronizableTargetWorkingDir): |
|---|
| 152 | def __init__(self, repository): |
|---|
| 153 | from os.path import split |
|---|
| 154 | from bzrlib import version_info, IGNORE_FILENAME |
|---|
| 155 | |
|---|
| 156 | if version_info > (0,9): |
|---|
| 157 | from bzrlib.ignores import add_runtime_ignores, parse_ignore_file |
|---|
| 158 | else: |
|---|
| 159 | from bzrlib import DEFAULT_IGNORE |
|---|
| 160 | |
|---|
| 161 | WorkingDir.__init__(self, repository) |
|---|
| 162 | # TODO: check if there is a "repository" in the configuration, |
|---|
| 163 | # and use it as a bzr repository |
|---|
| 164 | self.ignored = [] |
|---|
| 165 | self._working_tree = None |
|---|
| 166 | |
|---|
| 167 | # The bzr repository may have some plugins that needs to be activated |
|---|
| 168 | load_plugins() |
|---|
| 169 | |
|---|
| 170 | try: |
|---|
| 171 | bzrdir = BzrDir.open(self.repository.basedir) |
|---|
| 172 | wt = self._working_tree = bzrdir.open_workingtree() |
|---|
| 173 | |
|---|
| 174 | # read .bzrignore for _addSubtree() |
|---|
| 175 | if wt.has_filename(IGNORE_FILENAME): |
|---|
| 176 | f = wt.get_file_byname(IGNORE_FILENAME) |
|---|
| 177 | if version_info > (0,9): |
|---|
| 178 | self.ignored.extend(parse_ignore_file(f)) |
|---|
| 179 | else: |
|---|
| 180 | self.ignored.extend([ line.rstrip("\n\r") for line in f.readlines() ]) |
|---|
| 181 | f.close() |
|---|
| 182 | except (errors.NotBranchError, errors.NoWorkingTree): |
|---|
| 183 | pass |
|---|
| 184 | |
|---|
| 185 | # Omit our own log... |
|---|
| 186 | logfile = self.repository.projectref().logfile |
|---|
| 187 | dir, file = split(logfile) |
|---|
| 188 | if dir == self.repository.basedir: |
|---|
| 189 | self.ignored.append(file) |
|---|
| 190 | |
|---|
| 191 | # ... and state file |
|---|
| 192 | sfname = self.repository.projectref().state_file.filename |
|---|
| 193 | dir, file = split(sfname) |
|---|
| 194 | if dir == self.repository.basedir: |
|---|
| 195 | self.ignored.append(file) |
|---|
| 196 | self.ignored.append(file+'.old') |
|---|
| 197 | self.ignored.append(file+'.journal') |
|---|
| 198 | |
|---|
| 199 | if version_info > (0,9): |
|---|
| 200 | add_runtime_ignores(self.ignored) |
|---|
| 201 | else: |
|---|
| 202 | DEFAULT_IGNORE.extend(self.ignored) |
|---|
| 203 | |
|---|
| 204 | |
|---|
| 205 | ############################# |
|---|
| 206 | ## UpdatableSourceWorkingDir |
|---|
| 207 | |
|---|
| 208 | def _changesetFromRevision(self, branch, revision_id): |
|---|
| 209 | """ |
|---|
| 210 | Generate changeset for the given Bzr revision |
|---|
| 211 | """ |
|---|
| 212 | from datetime import datetime |
|---|
| 213 | from vcpx.tzinfo import FixedOffset, UTC |
|---|
| 214 | |
|---|
| 215 | revision = branch.repository.get_revision(revision_id) |
|---|
| 216 | deltatree = branch.get_revision_delta(branch.revision_id_to_revno(revision_id)) |
|---|
| 217 | entries = [] |
|---|
| 218 | |
|---|
| 219 | for delta in deltatree.renamed: |
|---|
| 220 | e = ChangesetEntry(delta[1]) |
|---|
| 221 | e.action_kind = ChangesetEntry.RENAMED |
|---|
| 222 | e.old_name = delta[0] |
|---|
| 223 | e.is_directory = delta[3] == 'directory' |
|---|
| 224 | entries.append(e) |
|---|
| 225 | |
|---|
| 226 | for delta in deltatree.added: |
|---|
| 227 | e = ChangesetEntry(delta[0]) |
|---|
| 228 | e.action_kind = ChangesetEntry.ADDED |
|---|
| 229 | e.is_directory = delta[2] == 'directory' |
|---|
| 230 | entries.append(e) |
|---|
| 231 | |
|---|
| 232 | for delta in deltatree.removed: |
|---|
| 233 | e = ChangesetEntry(delta[0]) |
|---|
| 234 | e.action_kind = ChangesetEntry.DELETED |
|---|
| 235 | e.is_directory = delta[2] == 'directory' |
|---|
| 236 | entries.append(e) |
|---|
| 237 | |
|---|
| 238 | for delta in deltatree.modified: |
|---|
| 239 | e = ChangesetEntry(delta[0]) |
|---|
| 240 | e.action_kind = ChangesetEntry.UPDATED |
|---|
| 241 | entries.append(e) |
|---|
| 242 | |
|---|
| 243 | if revision.timezone is not None: |
|---|
| 244 | timezone = FixedOffset(revision.timezone / 60) |
|---|
| 245 | else: |
|---|
| 246 | timezone = UTC |
|---|
| 247 | |
|---|
| 248 | return BzrChangeset(revision.revision_id, |
|---|
| 249 | datetime.fromtimestamp(revision.timestamp, timezone), |
|---|
| 250 | revision.committer, |
|---|
| 251 | revision.message, |
|---|
| 252 | entries) |
|---|
| 253 | |
|---|
| 254 | def _getUpstreamChangesets(self, sincerev): |
|---|
| 255 | """ |
|---|
| 256 | See what other revisions exist upstream and return them |
|---|
| 257 | """ |
|---|
| 258 | |
|---|
| 259 | from bzrlib import version_info |
|---|
| 260 | |
|---|
| 261 | parent_branch = Branch.open(self.repository.repository) |
|---|
| 262 | |
|---|
| 263 | branch = self._working_tree.branch |
|---|
| 264 | branch.lock_read() |
|---|
| 265 | try: |
|---|
| 266 | parent_branch.lock_read() |
|---|
| 267 | try: |
|---|
| 268 | if version_info > (1, 6): |
|---|
| 269 | revisions = find_unmerged(branch, parent_branch, 'remote')[1] |
|---|
| 270 | else: |
|---|
| 271 | revisions = find_unmerged(branch, parent_branch)[1] |
|---|
| 272 | |
|---|
| 273 | self.log.info("Collecting %d missing changesets", len(revisions)) |
|---|
| 274 | |
|---|
| 275 | for id, revision in revisions: |
|---|
| 276 | yield self._changesetFromRevision(parent_branch, revision) |
|---|
| 277 | except: |
|---|
| 278 | parent_branch.unlock() |
|---|
| 279 | raise |
|---|
| 280 | parent_branch.unlock() |
|---|
| 281 | except: |
|---|
| 282 | branch.unlock() |
|---|
| 283 | raise |
|---|
| 284 | branch.unlock() |
|---|
| 285 | |
|---|
| 286 | self.log.info("Fetching concrete changesets") |
|---|
| 287 | branch.lock_write() |
|---|
| 288 | try: |
|---|
| 289 | branch.fetch(parent_branch) |
|---|
| 290 | finally: |
|---|
| 291 | branch.unlock() |
|---|
| 292 | |
|---|
| 293 | def _applyChangeset(self, changeset): |
|---|
| 294 | """ |
|---|
| 295 | Apply the given changeset to the working tree |
|---|
| 296 | """ |
|---|
| 297 | parent_branch = BzrDir.open(self.repository.repository).open_branch() |
|---|
| 298 | self._working_tree.lock_write() |
|---|
| 299 | try: |
|---|
| 300 | count = self._working_tree.pull(parent_branch, |
|---|
| 301 | stop_revision=changeset.revision) |
|---|
| 302 | # XXX: this does not seem to return a true value on conflicts! |
|---|
| 303 | conflicts = self._working_tree.update() |
|---|
| 304 | finally: |
|---|
| 305 | self._working_tree.unlock() |
|---|
| 306 | try: |
|---|
| 307 | pulled_revnos = count.new_revno - count.old_revno |
|---|
| 308 | except AttributeError: |
|---|
| 309 | # Prior to 0.15 pull returned a simple integer instead of a result object |
|---|
| 310 | pulled_revnos = count |
|---|
| 311 | self.log.info('Updated to %r, applied %d changesets', changeset.revision, count) |
|---|
| 312 | if conflicts: |
|---|
| 313 | # No conflict handling yet |
|---|
| 314 | raise ChangesetApplicationFailure('Unsupported: conflicts') |
|---|
| 315 | return [] |
|---|
| 316 | |
|---|
| 317 | def _checkoutUpstreamRevision(self, revision): |
|---|
| 318 | """ |
|---|
| 319 | Initial checkout of upstream branch, equivalent of 'bzr branch -r', |
|---|
| 320 | and return the last changeset. |
|---|
| 321 | """ |
|---|
| 322 | parent_bzrdir = BzrDir.open(self.repository.repository) |
|---|
| 323 | parent_branch = parent_bzrdir.open_branch() |
|---|
| 324 | |
|---|
| 325 | if revision == "INITIAL": |
|---|
| 326 | try: |
|---|
| 327 | revid = parent_branch.get_rev_id(1) |
|---|
| 328 | except NoSuchRevision: |
|---|
| 329 | return None |
|---|
| 330 | elif revision == "HEAD": |
|---|
| 331 | revid = None |
|---|
| 332 | else: |
|---|
| 333 | revid = revision |
|---|
| 334 | |
|---|
| 335 | self.log.info('Extracting %r out of %r in %r...', |
|---|
| 336 | revid, parent_bzrdir.root_transport.base, self.repository.basedir) |
|---|
| 337 | bzrdir = parent_bzrdir.sprout(self.repository.basedir, revid) |
|---|
| 338 | self._working_tree = bzrdir.open_workingtree() |
|---|
| 339 | |
|---|
| 340 | return self._changesetFromRevision(parent_branch, revid) |
|---|
| 341 | |
|---|
| 342 | ################################# |
|---|
| 343 | ## SynchronizableTargetWorkingDir |
|---|
| 344 | |
|---|
| 345 | def _addPathnames(self, names): |
|---|
| 346 | if len(names): |
|---|
| 347 | names = [ pathjoin(self.repository.basedir, n) for n in names ] |
|---|
| 348 | self._working_tree.smart_add(names, recurse=False) |
|---|
| 349 | |
|---|
| 350 | def _addSubtree(self, subdir): |
|---|
| 351 | subdir = pathjoin(self.repository.basedir, subdir) |
|---|
| 352 | added, ignored = self._working_tree.smart_add([subdir], recurse=True) |
|---|
| 353 | |
|---|
| 354 | from vcpx.dualwd import IGNORED_METADIRS |
|---|
| 355 | |
|---|
| 356 | for meta in IGNORED_METADIRS + self.ignored: |
|---|
| 357 | if ignored.has_key(meta): |
|---|
| 358 | del ignored[meta] |
|---|
| 359 | |
|---|
| 360 | if len(ignored): |
|---|
| 361 | f = [] |
|---|
| 362 | map(f.extend, ignored.values()) |
|---|
| 363 | self._addPathnames(f) |
|---|
| 364 | |
|---|
| 365 | def _commit(self, date, author, patchname, changelog=None, entries=None, |
|---|
| 366 | tags = [], isinitialcommit = False): |
|---|
| 367 | """ |
|---|
| 368 | Commit the changeset. |
|---|
| 369 | """ |
|---|
| 370 | from calendar import timegm # like mktime(), but returns UTC timestamp |
|---|
| 371 | from binascii import hexlify |
|---|
| 372 | from re import search |
|---|
| 373 | |
|---|
| 374 | logmessage = [] |
|---|
| 375 | if patchname: |
|---|
| 376 | logmessage.append(patchname) |
|---|
| 377 | if changelog: |
|---|
| 378 | logmessage.append(changelog) |
|---|
| 379 | if logmessage: |
|---|
| 380 | self.log.info('Committing %s...', logmessage[0]) |
|---|
| 381 | logmessage = '\n'.join(logmessage) |
|---|
| 382 | else: |
|---|
| 383 | self.log.info('Committing...') |
|---|
| 384 | logmessage = "Empty changelog" |
|---|
| 385 | |
|---|
| 386 | timestamp = timegm(date.utctimetuple()) |
|---|
| 387 | timezone = date.utcoffset().seconds + date.utcoffset().days * 24 * 3600 |
|---|
| 388 | |
|---|
| 389 | # Normalize file names |
|---|
| 390 | if entries: |
|---|
| 391 | entries = [normpath(entry) for entry in entries] |
|---|
| 392 | |
|---|
| 393 | self._working_tree.commit(logmessage, committer=author, |
|---|
| 394 | specific_files=entries, |
|---|
| 395 | verbose=self.repository.projectref().verbose, |
|---|
| 396 | timestamp=timestamp, timezone=timezone) |
|---|
| 397 | |
|---|
| 398 | def _tag(self, tagname, date, author): |
|---|
| 399 | """ |
|---|
| 400 | Tag the current version, if supported. |
|---|
| 401 | """ |
|---|
| 402 | branch = self._working_tree.branch |
|---|
| 403 | try: |
|---|
| 404 | branch.tags.set_tag(tagname, branch.last_revision()) |
|---|
| 405 | except TagsNotSupported: |
|---|
| 406 | pass |
|---|
| 407 | |
|---|
| 408 | def _removePathnames(self, names): |
|---|
| 409 | """ |
|---|
| 410 | Remove files from the tree. |
|---|
| 411 | """ |
|---|
| 412 | self.log.info('Removing %s...', ', '.join(names)) |
|---|
| 413 | names.sort(reverse=True) # remove files before the dir they're in |
|---|
| 414 | self._working_tree.remove(names) |
|---|
| 415 | |
|---|
| 416 | def _renamePathname(self, oldname, newname): |
|---|
| 417 | """ |
|---|
| 418 | Rename a file from oldname to newname. |
|---|
| 419 | """ |
|---|
| 420 | self.log.info('Renaming %r to %r...', oldname, newname) |
|---|
| 421 | self._working_tree.rename_one(oldname, newname) |
|---|
| 422 | |
|---|
| 423 | def _prepareTargetRepository(self): |
|---|
| 424 | from bzrlib import version_info |
|---|
| 425 | from vcpx.dualwd import IGNORED_METADIRS |
|---|
| 426 | |
|---|
| 427 | if self._working_tree is None: |
|---|
| 428 | self._working_tree = self.repository.create() |
|---|
| 429 | |
|---|
| 430 | if version_info > (0,9): |
|---|
| 431 | from bzrlib.ignores import add_runtime_ignores |
|---|
| 432 | add_runtime_ignores(IGNORED_METADIRS) |
|---|
| 433 | else: |
|---|
| 434 | from bzrlib import DEFAULT_IGNORE |
|---|
| 435 | DEFAULT_IGNORE.extend(IGNORED_METADIRS) |
|---|