1
# Copyright (C) 2008 Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18
"""Foreign branch utilities."""
21
from bzrlib.branch import (
25
from bzrlib.commands import Command, Option
26
from bzrlib.repository import Repository
27
from bzrlib.revision import Revision
28
from bzrlib.lazy_import import lazy_import
29
lazy_import(globals(), """
38
class VcsMapping(object):
39
"""Describes the mapping between the semantics of Bazaar and a foreign vcs.
42
# Whether this is an experimental mapping that is still open to changes.
45
# Whether this mapping supports exporting and importing all bzr semantics.
48
# Prefix used when importing native foreign revisions (not roundtripped)
52
def __init__(self, vcs):
53
"""Create a new VcsMapping.
55
:param vcs: VCS that this mapping maps to Bazaar
59
def revision_id_bzr_to_foreign(self, bzr_revid):
60
"""Parse a bzr revision id and convert it to a foreign revid.
62
:param bzr_revid: The bzr revision id (a string).
63
:return: A foreign revision id, can be any sort of object.
65
raise NotImplementedError(self.revision_id_bzr_to_foreign)
67
def revision_id_foreign_to_bzr(self, foreign_revid):
68
"""Parse a foreign revision id and convert it to a bzr revid.
70
:param foreign_revid: Foreign revision id, can be any sort of object.
71
:return: A bzr revision id.
73
raise NotImplementedError(self.revision_id_foreign_to_bzr)
76
class VcsMappingRegistry(registry.Registry):
77
"""Registry for Bazaar<->foreign VCS mappings.
79
There should be one instance of this registry for every foreign VCS.
82
def register(self, key, factory, help):
83
"""Register a mapping between Bazaar and foreign VCS semantics.
85
The factory must be a callable that takes one parameter: the key.
86
It must produce an instance of VcsMapping when called.
89
raise ValueError("mapping name can not contain colon (:)")
90
registry.Registry.register(self, key, factory, help)
92
def set_default(self, key):
93
"""Set the 'default' key to be a clone of the supplied key.
95
This method must be called once and only once.
97
self._set_default_key(key)
99
def get_default(self):
100
"""Convenience function for obtaining the default mapping to use."""
101
return self.get(self._get_default_key())
103
def revision_id_bzr_to_foreign(self, revid):
104
"""Convert a bzr revision id to a foreign revid."""
105
raise NotImplementedError(self.revision_id_bzr_to_foreign)
108
class ForeignRevision(Revision):
109
"""A Revision from a Foreign repository. Remembers
110
information about foreign revision id and mapping.
114
def __init__(self, foreign_revid, mapping, *args, **kwargs):
115
if not "inventory_sha1" in kwargs:
116
kwargs["inventory_sha1"] = ""
117
super(ForeignRevision, self).__init__(*args, **kwargs)
118
self.foreign_revid = foreign_revid
119
self.mapping = mapping
122
def show_foreign_properties(rev):
123
"""Custom log displayer for foreign revision identifiers.
125
:param rev: Revision object.
127
# Revision comes directly from a foreign repository
128
if isinstance(rev, ForeignRevision):
129
return rev.mapping.vcs.show_foreign_revid(rev.foreign_revid)
131
# Revision was once imported from a foreign repository
133
foreign_revid, mapping = \
134
foreign_vcs_registry.parse_revision_id(rev.revision_id)
135
except errors.InvalidRevisionId:
138
return mapping.vcs.show_foreign_revid(foreign_revid)
141
class ForeignVcs(object):
142
"""A foreign version control system."""
144
def __init__(self, mapping_registry):
145
self.mapping_registry = mapping_registry
147
def show_foreign_revid(self, foreign_revid):
148
"""Prepare a foreign revision id for formatting using bzr log.
150
:param foreign_revid: Foreign revision id.
151
:return: Dictionary mapping string keys to string values.
156
class ForeignVcsRegistry(registry.Registry):
157
"""Registry for Foreign VCSes.
159
There should be one entry per foreign VCS. Example entries would be
160
"git", "svn", "hg", "darcs", etc.
164
def register(self, key, foreign_vcs, help):
165
"""Register a foreign VCS.
167
:param key: Prefix of the foreign VCS in revision ids
168
:param foreign_vcs: ForeignVCS instance
169
:param help: Description of the foreign VCS
171
if ":" in key or "-" in key:
172
raise ValueError("vcs name can not contain : or -")
173
registry.Registry.register(self, key, foreign_vcs, help)
175
def parse_revision_id(self, revid):
176
"""Parse a bzr revision and return the matching mapping and foreign
179
:param revid: The bzr revision id
180
:return: tuple with foreign revid and vcs mapping
183
raise errors.InvalidRevisionId(revid, None)
185
foreign_vcs = self.get(revid.split("-")[0])
187
raise errors.InvalidRevisionId(revid, None)
188
return foreign_vcs.mapping_registry.revision_id_bzr_to_foreign(revid)
191
foreign_vcs_registry = ForeignVcsRegistry()
194
class ForeignRepository(Repository):
195
"""A Repository that exists in a foreign version control system.
197
The data in this repository can not be represented natively using
198
Bazaars internal datastructures, but have to converted using a VcsMapping.
201
# This repository's native version control system
204
def has_foreign_revision(self, foreign_revid):
205
"""Check whether the specified foreign revision is present.
207
:param foreign_revid: A foreign revision id, in the format used
208
by this Repository's VCS.
210
raise NotImplementedError(self.has_foreign_revision)
212
def lookup_bzr_revision_id(self, revid):
213
"""Lookup a mapped or roundtripped revision by revision id.
215
:param revid: Bazaar revision id
216
:return: Tuple with foreign revision id and mapping.
218
raise NotImplementedError(self.lookup_revision_id)
220
def all_revision_ids(self, mapping=None):
221
"""See Repository.all_revision_ids()."""
222
raise NotImplementedError(self.all_revision_ids)
224
def get_default_mapping(self):
225
"""Get the default mapping for this repository."""
226
raise NotImplementedError(self.get_default_mapping)
228
def get_inventory_xml(self, revision_id):
229
"""See Repository.get_inventory_xml()."""
230
return self.serialise_inventory(self.get_inventory(revision_id))
232
def get_inventory_sha1(self, revision_id):
233
"""Get the sha1 for the XML representation of an inventory.
235
:param revision_id: Revision id of the inventory for which to return
240
return osutils.sha_string(self.get_inventory_xml(revision_id))
242
def get_revision_xml(self, revision_id):
243
"""Return the XML representation of a revision.
245
:param revision_id: Revision for which to return the XML.
248
return self._serializer.write_revision_to_string(
249
self.get_revision(revision_id))
252
class ForeignBranch(Branch):
253
"""Branch that exists in a foreign version control system."""
255
def __init__(self, mapping):
256
self.mapping = mapping
257
super(ForeignBranch, self).__init__()
260
def update_workingtree_fileids(wt, target_tree):
261
"""Update the file ids in a working tree based on another tree.
263
:param wt: Working tree in which to update file ids
264
:param target_tree: Tree to retrieve new file ids from, based on path
266
tt = transform.TreeTransform(wt)
268
for f, p, c, v, d, n, k, e in target_tree.iter_changes(wt):
269
if v == (True, False):
270
trans_id = tt.trans_id_tree_path(p[0])
271
tt.unversion_file(trans_id)
272
elif v == (False, True):
273
trans_id = tt.trans_id_tree_path(p[1])
274
tt.version_file(f, trans_id)
278
if len(wt.get_parent_ids()) == 1:
279
wt.set_parent_trees([(target_tree.get_revision_id(), target_tree)])
281
wt.set_last_revision(target_tree.get_revision_id())
284
class cmd_dpush(Command):
285
"""Push into a different VCS without any custom bzr metadata.
287
This will afterwards rebase the local branch on the remote
288
branch unless the --no-rebase option is used, in which case
289
the two branches will be out of sync after the push.
292
takes_args = ['location?']
293
takes_options = ['remember', Option('directory',
294
help='Branch to push from, '
295
'rather than the one containing the working directory.',
299
Option('no-rebase', help="Do not rebase after push.")]
301
def run(self, location=None, remember=False, directory=None,
303
from bzrlib import urlutils
304
from bzrlib.bzrdir import BzrDir
305
from bzrlib.errors import BzrCommandError, NoWorkingTree
306
from bzrlib.trace import info
307
from bzrlib.workingtree import WorkingTree
309
if directory is None:
312
source_wt = WorkingTree.open_containing(directory)[0]
313
source_branch = source_wt.branch
314
except NoWorkingTree:
315
source_branch = Branch.open(directory)
317
stored_loc = source_branch.get_push_location()
319
if stored_loc is None:
320
raise BzrCommandError("No push location known or specified.")
322
display_url = urlutils.unescape_for_display(stored_loc,
324
self.outf.write("Using saved location: %s\n" % display_url)
325
location = stored_loc
327
bzrdir = BzrDir.open(location)
328
target_branch = bzrdir.open_branch()
329
target_branch.lock_write()
332
push_result = source_branch.lossy_push(target_branch)
333
except errors.LossyPushToSameVCS:
334
raise BzrCommandError("%r and %r are in the same VCS, lossy "
335
"push not necessary. Please use regular push." %
336
(source_branch, target_branch))
337
# We successfully created the target, remember it
338
if source_branch.get_push_location() is None or remember:
339
source_branch.set_push_location(target_branch.base)
341
old_last_revid = source_branch.last_revision()
342
source_branch.pull(target_branch, overwrite=True)
343
new_last_revid = source_branch.last_revision()
344
if source_wt is not None and old_last_revid != new_last_revid:
345
source_wt.lock_write()
347
target = source_wt.branch.repository.revision_tree(
349
update_workingtree_fileids(source_wt, target)
352
push_result.report(self.outf)
354
target_branch.unlock()
357
class InterToForeignBranch(InterBranch):
359
def lossy_push(self, stop_revision=None):
360
"""Push deltas into another branch.
362
:note: This does not, like push, retain the revision ids from
363
the source branch and will, rather than adding bzr-specific
364
metadata, push only those semantics of the revision that can be
365
natively represented by this branch' VCS.
367
:param target: Target branch
368
:param stop_revision: Revision to push, defaults to last revision.
369
:return: BranchPushResult with an extra member revidmap:
370
A dictionary mapping revision ids from the target branch
371
to new revision ids in the target branch, for each
372
revision that was pushed.
374
raise NotImplementedError(self.lossy_push)