| 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 |
|---|
| 23 | from bzrlib.bzrdir import BzrDir |
|---|
| 24 | from bzrlib.delta import compare_trees |
|---|
| 25 | from bzrlib import errors |
|---|
| 26 | |
|---|
| 27 | from vcpx.repository import Repository |
|---|
| 28 | from vcpx.workdir import WorkingDir |
|---|
| 29 | from vcpx.source import UpdatableSourceWorkingDir, ChangesetApplicationFailure |
|---|
| 30 | from vcpx.target import SynchronizableTargetWorkingDir |
|---|
| 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 | |
|---|
| 46 | class BzrWorkingDir(UpdatableSourceWorkingDir, SynchronizableTargetWorkingDir): |
|---|
| 47 | def __init__(self, repository): |
|---|
| 48 | WorkingDir.__init__(self, repository) |
|---|
| 49 | # TODO: check if there is a "repository" in the configuration, |
|---|
| 50 | # and use it as a bzr repository |
|---|
| 51 | self._working_tree = None |
|---|
| 52 | try: |
|---|
| 53 | bzrdir = BzrDir.open(self.basedir) |
|---|
| 54 | self._working_tree = bzrdir.open_workingtree() |
|---|
| 55 | except errors.NotBranchError, errors.NoWorkingTree: |
|---|
| 56 | pass |
|---|
| 57 | |
|---|
| 58 | ############################# |
|---|
| 59 | ## UpdatableSourceWorkingDir |
|---|
| 60 | |
|---|
| 61 | def _changesetFromRevision(self, branch, revision_id): |
|---|
| 62 | """ |
|---|
| 63 | Generate changeset for the given Bzr revision |
|---|
| 64 | """ |
|---|
| 65 | from datetime import datetime |
|---|
| 66 | from vcpx.changes import ChangesetEntry, Changeset |
|---|
| 67 | |
|---|
| 68 | revision = branch.repository.get_revision(revision_id) |
|---|
| 69 | deltatree = branch.get_revision_delta(branch.revision_id_to_revno(revision_id)) |
|---|
| 70 | entries = [] |
|---|
| 71 | |
|---|
| 72 | for delta in deltatree.added: |
|---|
| 73 | e = ChangesetEntry(delta[0]) |
|---|
| 74 | e.action_kind = ChangesetEntry.ADDED |
|---|
| 75 | entries.append(e) |
|---|
| 76 | |
|---|
| 77 | for delta in deltatree.removed: |
|---|
| 78 | e = ChangesetEntry(delta[0]) |
|---|
| 79 | e.action_kind = ChangesetEntry.DELETED |
|---|
| 80 | entries.append(e) |
|---|
| 81 | |
|---|
| 82 | for delta in deltatree.renamed: |
|---|
| 83 | e = ChangesetEntry(delta[1]) |
|---|
| 84 | e.action_kind = ChangesetEntry.RENAMED |
|---|
| 85 | e.old_name = delta[0] |
|---|
| 86 | entries.append(e) |
|---|
| 87 | |
|---|
| 88 | for delta in deltatree.modified: |
|---|
| 89 | e = ChangesetEntry(delta[0]) |
|---|
| 90 | e.action_kind = ChangesetEntry.UPDATED |
|---|
| 91 | entries.append(e) |
|---|
| 92 | |
|---|
| 93 | return Changeset(revision.revision_id, |
|---|
| 94 | datetime.fromtimestamp(revision.timestamp), |
|---|
| 95 | revision.committer, |
|---|
| 96 | revision.message, |
|---|
| 97 | entries) |
|---|
| 98 | |
|---|
| 99 | def _getUpstreamChangesets(self, sincerev): |
|---|
| 100 | """ |
|---|
| 101 | See what other revisions exist upstream and return them |
|---|
| 102 | """ |
|---|
| 103 | parent_branch = BzrDir.open(self.repository.repository).open_branch() |
|---|
| 104 | branch = self._working_tree.branch |
|---|
| 105 | revisions = branch.missing_revisions(parent_branch) |
|---|
| 106 | branch.fetch(parent_branch) |
|---|
| 107 | |
|---|
| 108 | for revision_id in revisions: |
|---|
| 109 | yield self._changesetFromRevision(parent_branch, revision_id) |
|---|
| 110 | |
|---|
| 111 | def _applyChangeset(self, changeset): |
|---|
| 112 | """ |
|---|
| 113 | Apply the given changeset to the working tree |
|---|
| 114 | """ |
|---|
| 115 | parent_branch = BzrDir.open(self.repository.repository).open_branch() |
|---|
| 116 | self._working_tree.lock_write() |
|---|
| 117 | self.log.info('Updating to %r', changeset.revision) |
|---|
| 118 | try: |
|---|
| 119 | count = self._working_tree.pull(parent_branch, |
|---|
| 120 | stop_revision=changeset.revision) |
|---|
| 121 | conflicts = self._working_tree.update() |
|---|
| 122 | finally: |
|---|
| 123 | self._working_tree.unlock() |
|---|
| 124 | self.log.debug("%s updated to %s", |
|---|
| 125 | ', '.join([e.name for e in changeset.entries]), |
|---|
| 126 | changeset.revision) |
|---|
| 127 | if (count != 1) or conflicts: |
|---|
| 128 | raise ChangesetApplicationFailure('unknown reason') |
|---|
| 129 | return [] # No conflict handling yet |
|---|
| 130 | |
|---|
| 131 | def _checkoutUpstreamRevision(self, revision): |
|---|
| 132 | """ |
|---|
| 133 | Initial checkout of upstream branch, equivalent of 'bzr branch -r', |
|---|
| 134 | and return the last changeset. |
|---|
| 135 | """ |
|---|
| 136 | parent_bzrdir = BzrDir.open(self.repository.repository) |
|---|
| 137 | parent_branch = parent_bzrdir.open_branch() |
|---|
| 138 | |
|---|
| 139 | if revision == "INITIAL": |
|---|
| 140 | revid = parent_branch.get_rev_id(1) |
|---|
| 141 | elif revision == "HEAD": |
|---|
| 142 | revid = None |
|---|
| 143 | else: |
|---|
| 144 | revid = revision |
|---|
| 145 | |
|---|
| 146 | self.log.info('Extracting %r out of %r in %r...', |
|---|
| 147 | revid, parent_bzrdir.root_transport.base, self.basedir) |
|---|
| 148 | bzrdir = parent_bzrdir.sprout(self.basedir, revid) |
|---|
| 149 | self._working_tree = bzrdir.open_workingtree() |
|---|
| 150 | |
|---|
| 151 | return self._changesetFromRevision(parent_branch, revid) |
|---|
| 152 | |
|---|
| 153 | ################################# |
|---|
| 154 | ## SynchronizableTargetWorkingDir |
|---|
| 155 | |
|---|
| 156 | def _addPathnames(self, names): |
|---|
| 157 | """ |
|---|
| 158 | Add new files to working tree. |
|---|
| 159 | |
|---|
| 160 | This method may get invoked several times with the same files. |
|---|
| 161 | Bzrlib complains if you try to add a file which is already |
|---|
| 162 | versioned. This method filters these out. A file might already been |
|---|
| 163 | marked to be added in this changeset, or might be a target in a rename |
|---|
| 164 | operation. Remove those too. |
|---|
| 165 | |
|---|
| 166 | This method does not catch any errors from the adding through bzrlib, |
|---|
| 167 | since they are **real** errors. |
|---|
| 168 | """ |
|---|
| 169 | last_revision = self._working_tree.branch.last_revision() |
|---|
| 170 | if last_revision is None: |
|---|
| 171 | # initial revision |
|---|
| 172 | fnames = names |
|---|
| 173 | else: |
|---|
| 174 | fnames = [] |
|---|
| 175 | basis_tree = self._working_tree.branch.basis_tree() |
|---|
| 176 | inv = basis_tree.inventory |
|---|
| 177 | diff = compare_trees(basis_tree, self._working_tree) |
|---|
| 178 | added = ([new[0] for new in diff.added] + |
|---|
| 179 | [renamed[1] for renamed in diff.renamed]) |
|---|
| 180 | |
|---|
| 181 | def parent_was_copied(n): |
|---|
| 182 | for p in added: |
|---|
| 183 | if n.startswith(p+'/'): |
|---|
| 184 | return True |
|---|
| 185 | return False |
|---|
| 186 | |
|---|
| 187 | for fn in names: |
|---|
| 188 | normfn = normpath(fn) |
|---|
| 189 | if (not inv.has_filename(fn) |
|---|
| 190 | and not normfn in added |
|---|
| 191 | and not parent_was_copied(normfn)): |
|---|
| 192 | fnames.append(fn) |
|---|
| 193 | else: |
|---|
| 194 | self.log.debug('"%s" already in inventory, skipping', fn) |
|---|
| 195 | |
|---|
| 196 | if len(fnames): |
|---|
| 197 | self.log.info('Adding %s...', ', '.join(fnames)) |
|---|
| 198 | self._working_tree.add(fnames) |
|---|
| 199 | |
|---|
| 200 | def _commit(self, date, author, patchname, changelog=None, entries=None): |
|---|
| 201 | """ |
|---|
| 202 | Commit the changeset. |
|---|
| 203 | """ |
|---|
| 204 | from time import mktime |
|---|
| 205 | from binascii import hexlify |
|---|
| 206 | from re import search |
|---|
| 207 | from bzrlib.osutils import compact_date, rand_bytes |
|---|
| 208 | |
|---|
| 209 | logmessage = [] |
|---|
| 210 | if patchname: |
|---|
| 211 | logmessage.append(patchname) |
|---|
| 212 | if changelog: |
|---|
| 213 | logmessage.append(changelog) |
|---|
| 214 | if logmessage: |
|---|
| 215 | self.log.info('Committing %r...', logmessage[0]) |
|---|
| 216 | logmessage = '\n'.join(logmessage) |
|---|
| 217 | else: |
|---|
| 218 | self.log.info('Committing...') |
|---|
| 219 | logmessage = "Empty changelog" |
|---|
| 220 | timestamp = int(mktime(date.timetuple())) |
|---|
| 221 | |
|---|
| 222 | # Guess sane email address |
|---|
| 223 | email = search("<(.*@.*)>", author) |
|---|
| 224 | if email: |
|---|
| 225 | email = email.group(1) |
|---|
| 226 | else: |
|---|
| 227 | email = author |
|---|
| 228 | # Remove whitespace |
|---|
| 229 | email = ''.join(email.split()) |
|---|
| 230 | |
|---|
| 231 | # Normalize file names |
|---|
| 232 | if entries: |
|---|
| 233 | entries = [normpath(entry) for entry in entries] |
|---|
| 234 | |
|---|
| 235 | revision_id = "%s-%s-%s" % (email, compact_date(timestamp), |
|---|
| 236 | hexlify(rand_bytes(8))) |
|---|
| 237 | self._working_tree.commit(logmessage, committer=author, |
|---|
| 238 | specific_files=entries, rev_id=revision_id, |
|---|
| 239 | verbose=self.repository.projectref().verbose, |
|---|
| 240 | timestamp=timestamp) |
|---|
| 241 | |
|---|
| 242 | def _removePathnames(self, names): |
|---|
| 243 | """ |
|---|
| 244 | Remove files from the tree. |
|---|
| 245 | """ |
|---|
| 246 | self.log.info('Removing %s...', ', '.join(names)) |
|---|
| 247 | names.sort(reverse=True) # remove files before the dir they're in |
|---|
| 248 | self._working_tree.remove(names) |
|---|
| 249 | |
|---|
| 250 | def _renamePathname(self, oldname, newname): |
|---|
| 251 | """ |
|---|
| 252 | Rename a file from oldname to newname. |
|---|
| 253 | """ |
|---|
| 254 | from os import rename |
|---|
| 255 | from os.path import join, exists |
|---|
| 256 | |
|---|
| 257 | # bzr does the rename itself as well |
|---|
| 258 | unmoved = False |
|---|
| 259 | oldpath = join(self.basedir, oldname) |
|---|
| 260 | newpath = join(self.basedir, newname) |
|---|
| 261 | if not exists(oldpath): |
|---|
| 262 | try: |
|---|
| 263 | rename(newpath, oldpath) |
|---|
| 264 | except OSError: |
|---|
| 265 | self.log.critical('Cannot rename %r back to %r', |
|---|
| 266 | newpath, oldpath) |
|---|
| 267 | raise |
|---|
| 268 | unmoved = True |
|---|
| 269 | |
|---|
| 270 | self.log.info('Renaming %r to %r...', oldname, newname) |
|---|
| 271 | try: |
|---|
| 272 | self._working_tree.rename_one(oldname, newname) |
|---|
| 273 | except: |
|---|
| 274 | if unmoved: |
|---|
| 275 | rename(oldpath, newpath) |
|---|
| 276 | raise |
|---|
| 277 | |
|---|
| 278 | def _prepareTargetRepository(self): |
|---|
| 279 | """ |
|---|
| 280 | Create a branch with a working tree at the base directory. If the base |
|---|
| 281 | directory is inside a Bazaar-NG style "shared repository", it will use |
|---|
| 282 | that to create a branch and working tree (make sure it allows working |
|---|
| 283 | trees). |
|---|
| 284 | """ |
|---|
| 285 | from os.path import join, split |
|---|
| 286 | from bzrlib import IGNORE_FILENAME |
|---|
| 287 | |
|---|
| 288 | if self._working_tree is None: |
|---|
| 289 | ignored = [] |
|---|
| 290 | |
|---|
| 291 | # Omit our own log... |
|---|
| 292 | logfile = self.repository.projectref().logfile |
|---|
| 293 | dir, file = split(logfile) |
|---|
| 294 | if dir == self.basedir: |
|---|
| 295 | ignored.append(file) |
|---|
| 296 | |
|---|
| 297 | # ... and state file |
|---|
| 298 | sfname = self.repository.projectref().state_file.filename |
|---|
| 299 | dir, file = split(sfname) |
|---|
| 300 | if dir == self.basedir: |
|---|
| 301 | ignored.append(file) |
|---|
| 302 | ignored.append(file+'.old') |
|---|
| 303 | ignored.append(file+'.journal') |
|---|
| 304 | |
|---|
| 305 | if ignored: |
|---|
| 306 | bzrignore = open(join(self.basedir, IGNORE_FILENAME), 'wU') |
|---|
| 307 | bzrignore.write('\n'.join(ignored)) |
|---|
| 308 | |
|---|
| 309 | self.log.info('Initializing new repository in %r...', self.basedir) |
|---|
| 310 | try: |
|---|
| 311 | bzrdir = BzrDir.open(self.basedir) |
|---|
| 312 | except errors.NotBranchError: |
|---|
| 313 | # really a NotBzrDir error... |
|---|
| 314 | branch = BzrDir.create_branch_convenience(self.basedir, |
|---|
| 315 | force_new_tree=True) |
|---|
| 316 | self._working_tree = branch.bzrdir.open_workingtree() |
|---|
| 317 | else: |
|---|
| 318 | bzrdir.create_branch() |
|---|
| 319 | self._working_tree = bzrdir.create_workingtree() |
|---|