| 1 | # -*- mode: python; coding: utf-8 -*- |
|---|
| 2 | # :Progetto: vcpx -- bazaar-ng 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-NG. |
|---|
| 13 | """ |
|---|
| 14 | |
|---|
| 15 | __docformat__ = 'reStructuredText' |
|---|
| 16 | |
|---|
| 17 | |
|---|
| 18 | from sys import version_info |
|---|
| 19 | assert version_info >= (2,4), "Bazaar-NG backend requires Python 2.4" |
|---|
| 20 | del version_info |
|---|
| 21 | |
|---|
| 22 | from bzrlib.osutils import normpath, pathjoin |
|---|
| 23 | from bzrlib.bzrdir import BzrDir |
|---|
| 24 | from bzrlib.delta import compare_trees |
|---|
| 25 | from bzrlib.add import smart_add_tree |
|---|
| 26 | from bzrlib import errors |
|---|
| 27 | |
|---|
| 28 | from vcpx.repository import Repository |
|---|
| 29 | from vcpx.workdir import WorkingDir |
|---|
| 30 | from vcpx.source import UpdatableSourceWorkingDir, ChangesetApplicationFailure |
|---|
| 31 | from vcpx.target import SynchronizableTargetWorkingDir |
|---|
| 32 | |
|---|
| 33 | |
|---|
| 34 | class BzrRepository(Repository): |
|---|
| 35 | METADIR = '.bzr' |
|---|
| 36 | |
|---|
| 37 | def _load(self, project): |
|---|
| 38 | Repository._load(self, project) |
|---|
| 39 | ppath = project.config.get(self.name, 'python-path') |
|---|
| 40 | if ppath: |
|---|
| 41 | from sys import path |
|---|
| 42 | |
|---|
| 43 | if ppath not in path: |
|---|
| 44 | path.insert(0, ppath) |
|---|
| 45 | |
|---|
| 46 | |
|---|
| 47 | class BzrWorkingDir(UpdatableSourceWorkingDir, SynchronizableTargetWorkingDir): |
|---|
| 48 | def __init__(self, repository): |
|---|
| 49 | WorkingDir.__init__(self, repository) |
|---|
| 50 | # TODO: check if there is a "repository" in the configuration, |
|---|
| 51 | # and use it as a bzr repository |
|---|
| 52 | self._working_tree = None |
|---|
| 53 | try: |
|---|
| 54 | bzrdir = BzrDir.open(self.basedir) |
|---|
| 55 | self._working_tree = bzrdir.open_workingtree() |
|---|
| 56 | except errors.NotBranchError, errors.NoWorkingTree: |
|---|
| 57 | pass |
|---|
| 58 | |
|---|
| 59 | ############################# |
|---|
| 60 | ## UpdatableSourceWorkingDir |
|---|
| 61 | |
|---|
| 62 | def _changesetFromRevision(self, branch, revision_id): |
|---|
| 63 | """ |
|---|
| 64 | Generate changeset for the given Bzr revision |
|---|
| 65 | """ |
|---|
| 66 | from datetime import datetime |
|---|
| 67 | from vcpx.changes import ChangesetEntry, Changeset |
|---|
| 68 | |
|---|
| 69 | revision = branch.repository.get_revision(revision_id) |
|---|
| 70 | deltatree = branch.get_revision_delta(branch.revision_id_to_revno(revision_id)) |
|---|
| 71 | entries = [] |
|---|
| 72 | |
|---|
| 73 | for delta in deltatree.added: |
|---|
| 74 | e = ChangesetEntry(delta[0]) |
|---|
| 75 | e.action_kind = ChangesetEntry.ADDED |
|---|
| 76 | entries.append(e) |
|---|
| 77 | |
|---|
| 78 | for delta in deltatree.removed: |
|---|
| 79 | e = ChangesetEntry(delta[0]) |
|---|
| 80 | e.action_kind = ChangesetEntry.DELETED |
|---|
| 81 | entries.append(e) |
|---|
| 82 | |
|---|
| 83 | for delta in deltatree.renamed: |
|---|
| 84 | e = ChangesetEntry(delta[1]) |
|---|
| 85 | e.action_kind = ChangesetEntry.RENAMED |
|---|
| 86 | e.old_name = delta[0] |
|---|
| 87 | entries.append(e) |
|---|
| 88 | |
|---|
| 89 | for delta in deltatree.modified: |
|---|
| 90 | e = ChangesetEntry(delta[0]) |
|---|
| 91 | e.action_kind = ChangesetEntry.UPDATED |
|---|
| 92 | entries.append(e) |
|---|
| 93 | |
|---|
| 94 | return Changeset(revision.revision_id, |
|---|
| 95 | datetime.fromtimestamp(revision.timestamp), |
|---|
| 96 | revision.committer, |
|---|
| 97 | revision.message, |
|---|
| 98 | entries) |
|---|
| 99 | |
|---|
| 100 | def _getUpstreamChangesets(self, sincerev): |
|---|
| 101 | """ |
|---|
| 102 | See what other revisions exist upstream and return them |
|---|
| 103 | """ |
|---|
| 104 | parent_branch = BzrDir.open(self.repository.repository).open_branch() |
|---|
| 105 | branch = self._working_tree.branch |
|---|
| 106 | revisions = branch.missing_revisions(parent_branch) |
|---|
| 107 | branch.fetch(parent_branch) |
|---|
| 108 | |
|---|
| 109 | for revision_id in revisions: |
|---|
| 110 | yield self._changesetFromRevision(parent_branch, revision_id) |
|---|
| 111 | |
|---|
| 112 | def _applyChangeset(self, changeset): |
|---|
| 113 | """ |
|---|
| 114 | Apply the given changeset to the working tree |
|---|
| 115 | """ |
|---|
| 116 | parent_branch = BzrDir.open(self.repository.repository).open_branch() |
|---|
| 117 | self._working_tree.lock_write() |
|---|
| 118 | self.log.info('Updating to %r', changeset.revision) |
|---|
| 119 | try: |
|---|
| 120 | count = self._working_tree.pull(parent_branch, |
|---|
| 121 | stop_revision=changeset.revision) |
|---|
| 122 | conflicts = self._working_tree.update() |
|---|
| 123 | finally: |
|---|
| 124 | self._working_tree.unlock() |
|---|
| 125 | self.log.debug("%s updated to %s", |
|---|
| 126 | ', '.join([e.name for e in changeset.entries]), |
|---|
| 127 | changeset.revision) |
|---|
| 128 | if (count != 1) or conflicts: |
|---|
| 129 | raise ChangesetApplicationFailure('unknown reason') |
|---|
| 130 | return [] # No conflict handling yet |
|---|
| 131 | |
|---|
| 132 | def _checkoutUpstreamRevision(self, revision): |
|---|
| 133 | """ |
|---|
| 134 | Initial checkout of upstream branch, equivalent of 'bzr branch -r', |
|---|
| 135 | and return the last changeset. |
|---|
| 136 | """ |
|---|
| 137 | parent_bzrdir = BzrDir.open(self.repository.repository) |
|---|
| 138 | parent_branch = parent_bzrdir.open_branch() |
|---|
| 139 | |
|---|
| 140 | if revision == "INITIAL": |
|---|
| 141 | revid = parent_branch.get_rev_id(1) |
|---|
| 142 | elif revision == "HEAD": |
|---|
| 143 | revid = None |
|---|
| 144 | else: |
|---|
| 145 | revid = revision |
|---|
| 146 | |
|---|
| 147 | self.log.info('Extracting %r out of %r in %r...', |
|---|
| 148 | revid, parent_bzrdir.root_transport.base, self.basedir) |
|---|
| 149 | bzrdir = parent_bzrdir.sprout(self.basedir, revid) |
|---|
| 150 | self._working_tree = bzrdir.open_workingtree() |
|---|
| 151 | |
|---|
| 152 | return self._changesetFromRevision(parent_branch, revid) |
|---|
| 153 | |
|---|
| 154 | ################################# |
|---|
| 155 | ## SynchronizableTargetWorkingDir |
|---|
| 156 | |
|---|
| 157 | def _addPathnames(self, names): |
|---|
| 158 | if len(names): |
|---|
| 159 | names = [ pathjoin(self.basedir, n) for n in names ] |
|---|
| 160 | smart_add_tree(self._working_tree, names, recurse=False) |
|---|
| 161 | |
|---|
| 162 | def _addSubtree(self, subdir): |
|---|
| 163 | subdir = pathjoin(self.basedir, subdir) |
|---|
| 164 | added, ignored = smart_add_tree(self._working_tree, [subdir], recurse=True) |
|---|
| 165 | |
|---|
| 166 | if len(ignored): |
|---|
| 167 | f = [] |
|---|
| 168 | map(f.extend, ignored.values()) |
|---|
| 169 | self._addPathnames(f) |
|---|
| 170 | |
|---|
| 171 | def _commit(self, date, author, patchname, changelog=None, entries=None): |
|---|
| 172 | """ |
|---|
| 173 | Commit the changeset. |
|---|
| 174 | """ |
|---|
| 175 | from time import mktime |
|---|
| 176 | from binascii import hexlify |
|---|
| 177 | from re import search |
|---|
| 178 | from bzrlib.osutils import compact_date, rand_bytes |
|---|
| 179 | |
|---|
| 180 | logmessage = [] |
|---|
| 181 | if patchname: |
|---|
| 182 | logmessage.append(patchname) |
|---|
| 183 | if changelog: |
|---|
| 184 | logmessage.append(changelog) |
|---|
| 185 | if logmessage: |
|---|
| 186 | self.log.info('Committing %r...', logmessage[0]) |
|---|
| 187 | logmessage = '\n'.join(logmessage) |
|---|
| 188 | else: |
|---|
| 189 | self.log.info('Committing...') |
|---|
| 190 | logmessage = "Empty changelog" |
|---|
| 191 | timestamp = int(mktime(date.timetuple())) |
|---|
| 192 | |
|---|
| 193 | # Guess sane email address |
|---|
| 194 | email = search("<(.*@.*)>", author) |
|---|
| 195 | if email: |
|---|
| 196 | email = email.group(1) |
|---|
| 197 | else: |
|---|
| 198 | email = author |
|---|
| 199 | # Remove whitespace |
|---|
| 200 | email = ''.join(email.split()) |
|---|
| 201 | |
|---|
| 202 | # Normalize file names |
|---|
| 203 | if entries: |
|---|
| 204 | entries = [normpath(entry) for entry in entries] |
|---|
| 205 | |
|---|
| 206 | revision_id = "%s-%s-%s" % (email, compact_date(timestamp), |
|---|
| 207 | hexlify(rand_bytes(8))) |
|---|
| 208 | self._working_tree.commit(logmessage, committer=author, |
|---|
| 209 | specific_files=entries, rev_id=revision_id, |
|---|
| 210 | verbose=self.repository.projectref().verbose, |
|---|
| 211 | timestamp=timestamp) |
|---|
| 212 | |
|---|
| 213 | def _removePathnames(self, names): |
|---|
| 214 | """ |
|---|
| 215 | Remove files from the tree. |
|---|
| 216 | """ |
|---|
| 217 | self.log.info('Removing %s...', ', '.join(names)) |
|---|
| 218 | names.sort(reverse=True) # remove files before the dir they're in |
|---|
| 219 | self._working_tree.remove(names) |
|---|
| 220 | |
|---|
| 221 | def _renamePathname(self, oldname, newname): |
|---|
| 222 | """ |
|---|
| 223 | Rename a file from oldname to newname. |
|---|
| 224 | """ |
|---|
| 225 | from os import rename |
|---|
| 226 | from os.path import join, exists |
|---|
| 227 | |
|---|
| 228 | # bzr does the rename itself as well |
|---|
| 229 | unmoved = False |
|---|
| 230 | oldpath = join(self.basedir, oldname) |
|---|
| 231 | newpath = join(self.basedir, newname) |
|---|
| 232 | if not exists(oldpath): |
|---|
| 233 | try: |
|---|
| 234 | rename(newpath, oldpath) |
|---|
| 235 | except OSError: |
|---|
| 236 | self.log.critical('Cannot rename %r back to %r', |
|---|
| 237 | newpath, oldpath) |
|---|
| 238 | raise |
|---|
| 239 | unmoved = True |
|---|
| 240 | |
|---|
| 241 | self.log.info('Renaming %r to %r...', oldname, newname) |
|---|
| 242 | try: |
|---|
| 243 | self._working_tree.rename_one(oldname, newname) |
|---|
| 244 | except: |
|---|
| 245 | if unmoved: |
|---|
| 246 | rename(oldpath, newpath) |
|---|
| 247 | raise |
|---|
| 248 | |
|---|
| 249 | def _prepareTargetRepository(self): |
|---|
| 250 | """ |
|---|
| 251 | Create a branch with a working tree at the base directory. If the base |
|---|
| 252 | directory is inside a Bazaar-NG style "shared repository", it will use |
|---|
| 253 | that to create a branch and working tree (make sure it allows working |
|---|
| 254 | trees). |
|---|
| 255 | """ |
|---|
| 256 | from os.path import join, split |
|---|
| 257 | from bzrlib import IGNORE_FILENAME |
|---|
| 258 | |
|---|
| 259 | if self._working_tree is None: |
|---|
| 260 | ignored = [] |
|---|
| 261 | |
|---|
| 262 | # Omit our own log... |
|---|
| 263 | logfile = self.repository.projectref().logfile |
|---|
| 264 | dir, file = split(logfile) |
|---|
| 265 | if dir == self.basedir: |
|---|
| 266 | ignored.append(file) |
|---|
| 267 | |
|---|
| 268 | # ... and state file |
|---|
| 269 | sfname = self.repository.projectref().state_file.filename |
|---|
| 270 | dir, file = split(sfname) |
|---|
| 271 | if dir == self.basedir: |
|---|
| 272 | ignored.append(file) |
|---|
| 273 | ignored.append(file+'.old') |
|---|
| 274 | ignored.append(file+'.journal') |
|---|
| 275 | |
|---|
| 276 | if ignored: |
|---|
| 277 | bzrignore = open(join(self.basedir, IGNORE_FILENAME), 'wU') |
|---|
| 278 | bzrignore.write('\n'.join(ignored)) |
|---|
| 279 | |
|---|
| 280 | self.log.info('Initializing new repository in %r...', self.basedir) |
|---|
| 281 | try: |
|---|
| 282 | bzrdir = BzrDir.open(self.basedir) |
|---|
| 283 | except errors.NotBranchError: |
|---|
| 284 | # really a NotBzrDir error... |
|---|
| 285 | branch = BzrDir.create_branch_convenience(self.basedir, |
|---|
| 286 | force_new_tree=True) |
|---|
| 287 | self._working_tree = branch.bzrdir.open_workingtree() |
|---|
| 288 | else: |
|---|
| 289 | bzrdir.create_branch() |
|---|
| 290 | self._working_tree = bzrdir.create_workingtree() |
|---|