1
# Copyright (C) 2008 Jelmer Vernooij <jelmer@samba.org>
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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
"""Foreign branch utilities."""
19
from bzrlib import errors, log, osutils, registry
20
from bzrlib.branch import Branch
21
from bzrlib.commands import Command, Option
22
from bzrlib.errors import InvalidRevisionId
23
from bzrlib.repository import Repository
24
from bzrlib.revision import Revision
25
from bzrlib.trace import info
27
class VcsMapping(object):
28
"""Describes the mapping between the semantics of Bazaar and a foreign vcs.
31
# Whether this is an experimental mapping that is still open to changes.
34
# Whether this mapping supports exporting and importing all bzr semantics.
37
# Prefix used when importing native foreign revisions (not roundtripped)
41
def revision_id_bzr_to_foreign(self, bzr_revid):
42
"""Parse a bzr revision id and convert it to a foreign revid.
44
:param bzr_revid: The bzr revision id (a string).
45
:return: A foreign revision id, can be any sort of object.
47
raise NotImplementedError(self.revision_id_bzr_to_foreign)
49
def revision_id_foreign_to_bzr(self, foreign_revid):
50
"""Parse a foreign revision id and convert it to a bzr revid.
52
:param foreign_revid: Foreign revision id, can be any sort of object.
53
:return: A bzr revision id.
55
raise NotImplementedError(self.revision_id_foreign_to_bzr)
57
def show_foreign_revid(self, foreign_revid):
58
"""Prepare a foreign revision id for formatting using bzr log.
60
:param foreign_revid: Foreign revision id.
61
:return: Dictionary mapping string keys to string values.
66
class VcsMappingRegistry(registry.Registry):
67
"""Registry for Bazaar<->foreign VCS mappings.
69
There should be one instance of this registry for every foreign VCS.
72
def register(self, key, factory, help):
73
"""Register a mapping between Bazaar and foreign VCS semantics.
75
The factory must be a callable that takes one parameter: the key.
76
It must produce an instance of VcsMapping when called.
79
raise ValueError("mapping name can not contain colon (:)")
80
registry.Registry.register(self, key, factory, help)
82
def set_default(self, key):
83
"""Set the 'default' key to be a clone of the supplied key.
85
This method must be called once and only once.
87
self._set_default_key(key)
89
def get_default(self):
90
"""Convenience function for obtaining the default mapping to use."""
91
return self.get(self._get_default_key())
93
def revision_id_bzr_to_foreign(self, revid):
94
"""Convert a bzr revision id to a foreign revid."""
95
raise NotImplementedError(self.revision_id_bzr_to_foreign)
98
class ForeignVcs(object):
99
"""A foreign version control system."""
101
def __init__(self, mapping_registry):
102
self.mapping_registry = mapping_registry
105
class ForeignVcsRegistry(registry.Registry):
106
"""Registry for Foreign VCSes.
110
def register(self, key, foreign_vcs, help):
111
"""Register a foreign VCS.
114
if ":" in key or "-" in key:
115
raise ValueError("vcs name can not contain : or -")
116
registry.Registry.register(self, key, foreign_vcs, help)
118
def parse_revision_id(self, revid):
120
raise InvalidRevisionId(revid, None)
122
foreign_vcs = self.get(revid.split("-")[0])
124
raise InvalidRevisionId(revid, None)
125
return foreign_vcs.mapping_registry.revision_id_bzr_to_foreign(revid)
128
class ForeignBranch(Branch):
129
"""Branch that exists in a foreign version control system."""
131
def __init__(self, mapping):
132
self.mapping = mapping
133
super(ForeignBranch, self).__init__()
135
def dpull(self, source, stop_revision=None):
136
"""Pull deltas from another branch.
138
:note: This does not, like pull, retain the revision ids from
141
:param source: Source branch
142
:param stop_revision: Revision to pull, defaults to last revision.
144
raise NotImplementedError(self.pull)
147
class ForeignRepository(Repository):
149
def has_foreign_revision(self, foreign_revid):
150
raise NotImplementedError(self.has_foreign_revision)
152
def all_revision_ids(self, mapping=None):
153
raise NotImplementedError(self.all_revision_ids)
155
def get_mapping(self):
156
raise NotImplementedError(self.get_default_mapping)
158
def get_inventory_xml(self, revision_id):
159
"""See Repository.get_inventory_xml()."""
160
return self.serialise_inventory(self.get_inventory(revision_id))
162
def get_inventory_sha1(self, revision_id):
163
"""Get the sha1 for the XML representation of an inventory.
165
:param revision_id: Revision id of the inventory for which to return
170
return osutils.sha_string(self.get_inventory_xml(revision_id))
172
def get_revision_xml(self, revision_id):
173
"""Return the XML representation of a revision.
175
:param revision_id: Revision for which to return the XML.
178
return self._serializer.write_revision_to_string(self.get_revision(revision_id))
182
class FakeControlFiles(object):
183
"""Dummy implementation of ControlFiles.
185
This is required as some code relies on controlfiles being
187
def get_utf8(self, name):
188
raise errors.NoSuchFile(name)
191
raise errors.NoSuchFile(name)
193
def break_lock(self):
197
class cmd_dpush(Command):
198
"""Push diffs into a foreign version control system without any
199
Bazaar-specific metadata.
201
This will afterwards rebase the local Bazaar branch on the remote
202
branch unless the --no-rebase option is used, in which case
203
the two branches will be out of sync.
205
takes_args = ['location?']
206
takes_options = ['remember', Option('directory',
207
help='Branch to push from, '
208
'rather than the one containing the working directory.',
212
Option('no-rebase', help="Don't rebase after push")]
214
def run(self, location=None, remember=False, directory=None,
216
from bzrlib import urlutils
217
from bzrlib.bzrdir import BzrDir
218
from bzrlib.errors import BzrCommandError, NoWorkingTree
219
from bzrlib.workingtree import WorkingTree
221
if directory is None:
224
source_wt = WorkingTree.open_containing(directory)[0]
225
source_branch = source_wt.branch
226
except NoWorkingTree:
227
source_branch = Branch.open_containing(directory)[0]
229
stored_loc = source_branch.get_push_location()
231
if stored_loc is None:
232
raise BzrCommandError("No push location known or specified.")
234
display_url = urlutils.unescape_for_display(stored_loc,
236
self.outf.write("Using saved location: %s\n" % display_url)
237
location = stored_loc
239
bzrdir = BzrDir.open(location)
240
target_branch = bzrdir.open_branch()
241
target_branch.lock_write()
242
if not isinstance(target_branch, ForeignBranch):
243
info("target branch is not a foreign branch, using regular push.")
244
target_branch.pull(source_branch)
247
revid_map = target_branch.dpull(source_branch)
248
# We successfully created the target, remember it
249
if source_branch.get_push_location() is None or remember:
250
source_branch.set_push_location(target_branch.base)
252
_, old_last_revid = source_branch.last_revision_info()
253
new_last_revid = revid_map[old_last_revid]
254
if source_wt is not None:
255
source_wt.pull(target_branch, overwrite=True,
256
stop_revision=new_last_revid)
258
source_branch.pull(target_branch, overwrite=True,
259
stop_revision=new_last_revid)
262
from unittest import TestSuite
263
from bzrlib.tests import TestUtil
264
loader = TestUtil.TestLoader()
266
testmod_names = ['test_versionedfiles', ]
267
suite.addTest(loader.loadTestsFromModuleNames(testmod_names))
271
def escape_commit_message(message):
272
"""Replace xml-incompatible control characters."""
276
# FIXME: RBC 20060419 this should be done by the revision
277
# serialiser not by commit. Then we can also add an unescaper
278
# in the deserializer and start roundtripping revision messages
279
# precisely. See repository_implementations/test_repository.py
281
# Python strings can include characters that can't be
282
# represented in well-formed XML; escape characters that
283
# aren't listed in the XML specification
284
# (http://www.w3.org/TR/REC-xml/#NT-Char).
285
message, _ = re.subn(
286
u'[^\x09\x0A\x0D\u0020-\uD7FF\uE000-\uFFFD]+',
287
lambda match: match.group(0).encode('unicode_escape'),
292
class ForeignRevision(Revision):
293
"""A Revision from a Foreign repository. Remembers
294
information about foreign revision id and mapping.
298
def __init__(self, foreign_revid, mapping, *args, **kwargs):
299
if not "inventory_sha1" in kwargs:
300
kwargs["inventory_sha1"] = ""
301
super(ForeignRevision, self).__init__(*args, **kwargs)
302
self.foreign_revid = foreign_revid
303
self.mapping = mapping
306
def show_foreign_properties(rev):
307
"""Custom log displayer for foreign revision identifiers.
309
:param rev: Revision object.
311
# Revision comes directly from a foreign repository
312
if isinstance(rev, ForeignRevision):
313
return rev.mapping.show_foreign_revid(rev.foreign_revid)
315
# Revision was once imported from a foreign repository
317
foreign_revid, mapping = foreign_vcs_registry.parse_revision_id(rev.revision_id)
318
except InvalidRevisionId:
321
return mapping.show_foreign_revid(foreign_revid)
323
if not "foreign" in log.properties_handler_registry:
324
log.properties_handler_registry.register("foreign",
325
show_foreign_properties,
326
"Show foreign VCS properties")
328
foreign_vcs_registry = ForeignVcsRegistry()