| 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.bzrdir import BzrDir |
|---|
| 24 | from bzrlib.osutils import normpath, pathjoin |
|---|
| 25 | from bzrlib.plugin import load_plugins |
|---|
| 26 | |
|---|
| 27 | from vcpx.repository import Repository |
|---|
| 28 | from vcpx.source import UpdatableSourceWorkingDir, ChangesetApplicationFailure |
|---|
| 29 | from vcpx.target import SynchronizableTargetWorkingDir |
|---|
| 30 | from vcpx.workdir import WorkingDir |
|---|
| 31 | |
|---|
| 32 | |
|---|
| 33 | class BzrRepository(Repository): |
|---|
| 34 | METADIR = '.bzr' |
|---|
| 35 | |
|---|
| 36 | def _load(self, project): |
|---|
| 37 | Repository._load(self, project) |
|---|
| 38 | ppath = project.config.get(self.name, 'python-path') |
|---|
| 39 | if ppath: |
|---|
| 40 | from sys import path |
|---|
| 41 | |
|---|
| 42 | if ppath not in path: |
|---|
| 43 | path.insert(0, ppath) |
|---|
| 44 | |
|---|
| 45 | def create(self): |
|---|
| 46 | """ |
|---|
| 47 | Create a branch with a working tree at the base directory. If the base |
|---|
| 48 | directory is inside a Bazaar style "shared repository", it will use |
|---|
| 49 | that to create a branch and working tree (make sure it allows working |
|---|
| 50 | trees). |
|---|
| 51 | """ |
|---|
| 52 | |
|---|
| 53 | self.log.info('Initializing new repository in %r...', self.basedir) |
|---|
| 54 | try: |
|---|
| 55 | bzrdir = BzrDir.open(self.basedir) |
|---|
| 56 | except errors.NotBranchError: |
|---|
| 57 | # really a NotBzrDir error... |
|---|
| 58 | branch = BzrDir.create_branch_convenience(self.basedir, force_new_tree=True) |
|---|
| 59 | wtree = branch.bzrdir.open_workingtree() |
|---|
| 60 | else: |
|---|
| 61 | bzrdir.create_branch() |
|---|
| 62 | wtree = bzrdir.create_workingtree() |
|---|
| 63 | |
|---|
| 64 | return wtree |
|---|
| 65 | |
|---|
| 66 | |
|---|
| 67 | class BzrWorkingDir(UpdatableSourceWorkingDir, SynchronizableTargetWorkingDir): |
|---|
| 68 | def __init__(self, repository): |
|---|
| 69 | from os.path import split |
|---|
| 70 | from bzrlib import version_info, IGNORE_FILENAME |
|---|
| 71 | |
|---|
| 72 | if version_info > (0,9): |
|---|
| 73 | from bzrlib.ignores import add_runtime_ignores, parse_ignore_file |
|---|
| 74 | else: |
|---|
| 75 | from bzrlib import DEFAULT_IGNORE |
|---|
| 76 | |
|---|
| 77 | WorkingDir.__init__(self, repository) |
|---|
| 78 | # TODO: check if there is a "repository" in the configuration, |
|---|
| 79 | # and use it as a bzr repository |
|---|
| 80 | self.ignored = [] |
|---|
| 81 | self._working_tree = None |
|---|
| 82 | |
|---|
| 83 | # The bzr repository may have some plugins that needs to be activated |
|---|
| 84 | load_plugins() |
|---|
| 85 | |
|---|
| 86 | try: |
|---|
| 87 | bzrdir = BzrDir.open(self.repository.basedir) |
|---|
| 88 | wt = self._working_tree = bzrdir.open_workingtree() |
|---|
| 89 | |
|---|
| 90 | # read .bzrignore for _addSubtree() |
|---|
| 91 | if wt.has_filename(IGNORE_FILENAME): |
|---|
| 92 | f = wt.get_file_byname(IGNORE_FILENAME) |
|---|
| 93 | if version_info > (0,9): |
|---|
| 94 | self.ignored.extend(parse_ignore_file(f)) |
|---|
| 95 | else: |
|---|
| 96 | self.ignored.extend([ line.rstrip("\n\r") for line in f.readlines() ]) |
|---|
| 97 | f.close() |
|---|
| 98 | except errors.NotBranchError, errors.NoWorkingTree: |
|---|
| 99 | pass |
|---|
| 100 | |
|---|
| 101 | # Omit our own log... |
|---|
| 102 | logfile = self.repository.projectref().logfile |
|---|
| 103 | dir, file = split(logfile) |
|---|
| 104 | if dir == self.repository.basedir: |
|---|
| 105 | self.ignored.append(file) |
|---|
| 106 | |
|---|
| 107 | # ... and state file |
|---|
| 108 | sfname = self.repository.projectref().state_file.filename |
|---|
| 109 | dir, file = split(sfname) |
|---|
| 110 | if dir == self.repository.basedir: |
|---|
| 111 | self.ignored.append(file) |
|---|
| 112 | self.ignored.append(file+'.old') |
|---|
| 113 | self.ignored.append(file+'.journal') |
|---|
| 114 | |
|---|
| 115 | if version_info > (0,9): |
|---|
| 116 | add_runtime_ignores(self.ignored) |
|---|
| 117 | else: |
|---|
| 118 | DEFAULT_IGNORE.extend(self.ignored) |
|---|
| 119 | |
|---|
| 120 | |
|---|
| 121 | ############################# |
|---|
| 122 | ## UpdatableSourceWorkingDir |
|---|
| 123 | |
|---|
| 124 | def _changesetFromRevision(self, branch, revision_id): |
|---|
| 125 | """ |
|---|
| 126 | Generate changeset for the given Bzr revision |
|---|
| 127 | """ |
|---|
| 128 | from datetime import datetime |
|---|
| 129 | from vcpx.changes import ChangesetEntry, Changeset |
|---|
| 130 | from vcpx.tzinfo import FixedOffset, UTC |
|---|
| 131 | |
|---|
| 132 | revision = branch.repository.get_revision(revision_id) |
|---|
| 133 | deltatree = branch.get_revision_delta(branch.revision_id_to_revno(revision_id)) |
|---|
| 134 | entries = [] |
|---|
| 135 | |
|---|
| 136 | for delta in deltatree.added: |
|---|
| 137 | e = ChangesetEntry(delta[0]) |
|---|
| 138 | e.action_kind = ChangesetEntry.ADDED |
|---|
| 139 | entries.append(e) |
|---|
| 140 | |
|---|
| 141 | for delta in deltatree.removed: |
|---|
| 142 | e = ChangesetEntry(delta[0]) |
|---|
| 143 | e.action_kind = ChangesetEntry.DELETED |
|---|
| 144 | entries.append(e) |
|---|
| 145 | |
|---|
| 146 | for delta in deltatree.renamed: |
|---|
| 147 | e = ChangesetEntry(delta[1]) |
|---|
| 148 | e.action_kind = ChangesetEntry.RENAMED |
|---|
| 149 | e.old_name = delta[0] |
|---|
| 150 | entries.append(e) |
|---|
| 151 | |
|---|
| 152 | for delta in deltatree.modified: |
|---|
| 153 | e = ChangesetEntry(delta[0]) |
|---|
| 154 | e.action_kind = ChangesetEntry.UPDATED |
|---|
| 155 | entries.append(e) |
|---|
| 156 | |
|---|
| 157 | if revision.timezone is not None: |
|---|
| 158 | timezone = FixedOffset(revision.timezone / 60) |
|---|
| 159 | else: |
|---|
| 160 | timezone = UTC |
|---|
| 161 | |
|---|
| 162 | return Changeset(revision.revision_id, |
|---|
| 163 | datetime.fromtimestamp(revision.timestamp, timezone), |
|---|
| 164 | revision.committer, |
|---|
| 165 | revision.message, |
|---|
| 166 | entries) |
|---|
| 167 | |
|---|
| 168 | def _getUpstreamChangesets(self, sincerev): |
|---|
| 169 | """ |
|---|
| 170 | See what other revisions exist upstream and return them |
|---|
| 171 | """ |
|---|
| 172 | parent_branch = BzrDir.open(self.repository.repository).open_branch() |
|---|
| 173 | branch = self._working_tree.branch |
|---|
| 174 | revisions = branch.missing_revisions(parent_branch) |
|---|
| 175 | branch.fetch(parent_branch) |
|---|
| 176 | |
|---|
| 177 | for revision_id in revisions: |
|---|
| 178 | yield self._changesetFromRevision(parent_branch, revision_id) |
|---|
| 179 | |
|---|
| 180 | def _applyChangeset(self, changeset): |
|---|
| 181 | """ |
|---|
| 182 | Apply the given changeset to the working tree |
|---|
| 183 | """ |
|---|
| 184 | parent_branch = BzrDir.open(self.repository.repository).open_branch() |
|---|
| 185 | self._working_tree.lock_write() |
|---|
| 186 | self.log.info('Updating to %r', changeset.revision) |
|---|
| 187 | try: |
|---|
| 188 | count = self._working_tree.pull(parent_branch, |
|---|
| 189 | stop_revision=changeset.revision) |
|---|
| 190 | conflicts = self._working_tree.update() |
|---|
| 191 | finally: |
|---|
| 192 | self._working_tree.unlock() |
|---|
| 193 | self.log.debug("%s updated to %s", |
|---|
| 194 | ', '.join([e.name for e in changeset.entries]), |
|---|
| 195 | changeset.revision) |
|---|
| 196 | try: |
|---|
| 197 | pulled_revnos = count.new_revno - count.old_revno |
|---|
| 198 | except AttributeError: |
|---|
| 199 | # Prior to 0.15 pull returned a simple integer instead of a result object |
|---|
| 200 | pulled_revnos = count |
|---|
| 201 | if (pulled_revnos != 1) or conflicts: |
|---|
| 202 | raise ChangesetApplicationFailure('unknown reason') |
|---|
| 203 | return [] # No conflict handling yet |
|---|
| 204 | |
|---|
| 205 | def _checkoutUpstreamRevision(self, revision): |
|---|
| 206 | """ |
|---|
| 207 | Initial checkout of upstream branch, equivalent of 'bzr branch -r', |
|---|
| 208 | and return the last changeset. |
|---|
| 209 | """ |
|---|
| 210 | parent_bzrdir = BzrDir.open(self.repository.repository) |
|---|
| 211 | parent_branch = parent_bzrdir.open_branch() |
|---|
| 212 | |
|---|
| 213 | if revision == "INITIAL": |
|---|
| 214 | revid = parent_branch.get_rev_id(1) |
|---|
| 215 | elif revision == "HEAD": |
|---|
| 216 | revid = None |
|---|
| 217 | else: |
|---|
| 218 | revid = revision |
|---|
| 219 | |
|---|
| 220 | self.log.info('Extracting %r out of %r in %r...', |
|---|
| 221 | revid, parent_bzrdir.root_transport.base, self.repository.basedir) |
|---|
| 222 | bzrdir = parent_bzrdir.sprout(self.repository.basedir, revid) |
|---|
| 223 | self._working_tree = bzrdir.open_workingtree() |
|---|
| 224 | |
|---|
| 225 | return self._changesetFromRevision(parent_branch, revid) |
|---|
| 226 | |
|---|
| 227 | ################################# |
|---|
| 228 | ## SynchronizableTargetWorkingDir |
|---|
| 229 | |
|---|
| 230 | def _addPathnames(self, names): |
|---|
| 231 | if len(names): |
|---|
| 232 | names = [ pathjoin(self.repository.basedir, n) for n in names ] |
|---|
| 233 | self._working_tree.smart_add(names, recurse=False) |
|---|
| 234 | |
|---|
| 235 | def _addSubtree(self, subdir): |
|---|
| 236 | subdir = pathjoin(self.repository.basedir, subdir) |
|---|
| 237 | added, ignored = self._working_tree.smart_add([subdir], recurse=True) |
|---|
| 238 | |
|---|
| 239 | from vcpx.dualwd import IGNORED_METADIRS |
|---|
| 240 | |
|---|
| 241 | for meta in IGNORED_METADIRS + self.ignored: |
|---|
| 242 | if ignored.has_key(meta): |
|---|
| 243 | del ignored[meta] |
|---|
| 244 | |
|---|
| 245 | if len(ignored): |
|---|
| 246 | f = [] |
|---|
| 247 | map(f.extend, ignored.values()) |
|---|
| 248 | self._addPathnames(f) |
|---|
| 249 | |
|---|
| 250 | def _commit(self, date, author, patchname, changelog=None, entries=None, |
|---|
| 251 | tags = [], isinitialcommit = False): |
|---|
| 252 | """ |
|---|
| 253 | Commit the changeset. |
|---|
| 254 | """ |
|---|
| 255 | from calendar import timegm # like mktime(), but returns UTC timestamp |
|---|
| 256 | from binascii import hexlify |
|---|
| 257 | from re import search |
|---|
| 258 | |
|---|
| 259 | logmessage = [] |
|---|
| 260 | if patchname: |
|---|
| 261 | logmessage.append(patchname) |
|---|
| 262 | if changelog: |
|---|
| 263 | logmessage.append(changelog) |
|---|
| 264 | if logmessage: |
|---|
| 265 | self.log.info('Committing %r...', logmessage[0]) |
|---|
| 266 | logmessage = '\n'.join(logmessage) |
|---|
| 267 | else: |
|---|
| 268 | self.log.info('Committing...') |
|---|
| 269 | logmessage = "Empty changelog" |
|---|
| 270 | |
|---|
| 271 | timestamp = timegm(date.utctimetuple()) |
|---|
| 272 | timezone = date.utcoffset().seconds + date.utcoffset().days * 24 * 3600 |
|---|
| 273 | |
|---|
| 274 | # Normalize file names |
|---|
| 275 | if entries: |
|---|
| 276 | entries = [normpath(entry) for entry in entries] |
|---|
| 277 | |
|---|
| 278 | self._working_tree.commit(logmessage, committer=author, |
|---|
| 279 | specific_files=entries, |
|---|
| 280 | verbose=self.repository.projectref().verbose, |
|---|
| 281 | timestamp=timestamp, timezone=timezone) |
|---|
| 282 | |
|---|
| 283 | def _removePathnames(self, names): |
|---|
| 284 | """ |
|---|
| 285 | Remove files from the tree. |
|---|
| 286 | """ |
|---|
| 287 | self.log.info('Removing %s...', ', '.join(names)) |
|---|
| 288 | names.sort(reverse=True) # remove files before the dir they're in |
|---|
| 289 | self._working_tree.remove(names) |
|---|
| 290 | |
|---|
| 291 | def _renamePathname(self, oldname, newname): |
|---|
| 292 | """ |
|---|
| 293 | Rename a file from oldname to newname. |
|---|
| 294 | """ |
|---|
| 295 | from os import rename |
|---|
| 296 | from os.path import join, exists |
|---|
| 297 | |
|---|
| 298 | # bzr does the rename itself as well |
|---|
| 299 | unmoved = False |
|---|
| 300 | oldpath = join(self.repository.basedir, oldname) |
|---|
| 301 | newpath = join(self.repository.basedir, newname) |
|---|
| 302 | if not exists(oldpath): |
|---|
| 303 | try: |
|---|
| 304 | rename(newpath, oldpath) |
|---|
| 305 | except OSError: |
|---|
| 306 | self.log.critical('Cannot rename %r back to %r', |
|---|
| 307 | newpath, oldpath) |
|---|
| 308 | raise |
|---|
| 309 | unmoved = True |
|---|
| 310 | |
|---|
| 311 | self.log.info('Renaming %r to %r...', oldname, newname) |
|---|
| 312 | try: |
|---|
| 313 | self._working_tree.rename_one(oldname, newname) |
|---|
| 314 | except: |
|---|
| 315 | if unmoved: |
|---|
| 316 | rename(oldpath, newpath) |
|---|
| 317 | raise |
|---|
| 318 | |
|---|
| 319 | def _prepareTargetRepository(self): |
|---|
| 320 | from bzrlib import version_info |
|---|
| 321 | from vcpx.dualwd import IGNORED_METADIRS |
|---|
| 322 | |
|---|
| 323 | if self._working_tree is None: |
|---|
| 324 | self._working_tree = self.repository.create() |
|---|
| 325 | |
|---|
| 326 | if version_info > (0,9): |
|---|
| 327 | from bzrlib.ignores import add_runtime_ignores |
|---|
| 328 | add_runtime_ignores(IGNORED_METADIRS) |
|---|
| 329 | else: |
|---|
| 330 | from bzrlib import DEFAULT_IGNORE |
|---|
| 331 | DEFAULT_IGNORE.extend(IGNORED_METADIRS) |
|---|