1
# Copyright (C) 2007 Canonical Ltd
2
# Copyright (C) 2009-2010 Jelmer Vernooij <jelmer@samba.org>
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
# GNU General Public License for more details.
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18
"""An adapter between a Git Branch and a Bazaar Branch"""
20
from collections import defaultdict
22
from dulwich.objects import (
37
from bzrlib.decorators import (
40
from bzrlib.revision import (
43
from bzrlib.trace import (
48
from bzrlib.plugins.git.config import (
51
from bzrlib.plugins.git.errors import (
55
from bzrlib.plugins.git.refs import (
64
from bzrlib.foreign import ForeignBranch
67
class GitPullResult(branch.PullResult):
68
"""Result of a pull from a Git branch."""
70
def _lookup_revno(self, revid):
71
assert isinstance(revid, str), "was %r" % revid
72
# Try in source branch first, it'll be faster
73
return self.target_branch.revision_id_to_revno(revid)
77
return self._lookup_revno(self.old_revid)
81
return self._lookup_revno(self.new_revid)
84
class GitTags(tag.BasicTags):
85
"""Ref-based tag dictionary."""
87
def __init__(self, branch):
89
self.repository = branch.repository
92
raise NotImplementedError(self.get_refs)
94
def _iter_tag_refs(self, refs):
95
raise NotImplementedError(self._iter_tag_refs)
97
def _merge_to_git(self, to_tags, refs, overwrite=False):
98
target_repo = to_tags.repository
100
for k, v in refs.iteritems():
103
if overwrite or not k in self.target.repository.refs:
104
target_repo.refs[k] = v
105
elif target_repo.repository.refs[k] == v:
108
conflicts.append((ref_to_tag_name(k), v, target_repo.refs[k]))
111
def _merge_to_non_git(self, to_tags, refs, overwrite=False):
112
unpeeled_map = defaultdict(set)
114
result = dict(to_tags.get_tag_dict())
115
for n, peeled, unpeeled, bzr_revid in self._iter_tag_refs(refs):
116
if unpeeled is not None:
117
unpeeled_map[peeled].add(unpeeled)
118
if n not in result or overwrite:
119
result[n] = bzr_revid
120
elif result[n] == bzr_revid:
123
conflicts.append((n, result[n], bzr_revid))
124
to_tags._set_tag_dict(result)
125
if len(unpeeled_map) > 0:
126
map_file = UnpeelMap.from_repository(to_tags.branch.repository)
127
map_file.update(unpeeled_map)
128
map_file.save_in_repository(to_tags.branch.repository)
131
def merge_to(self, to_tags, overwrite=False, ignore_master=False,
133
if source_refs is None:
134
source_refs = self.get_refs()
137
if isinstance(to_tags, GitTags):
138
return self._merge_to_git(to_tags, source_refs,
144
master = to_tags.branch.get_master_branch()
145
conflicts = self._merge_to_non_git(to_tags, source_refs,
147
if master is not None:
148
conflicts += self.merge_to(to_tags, overwrite=overwrite,
149
source_refs=source_refs,
150
ignore_master=ignore_master)
153
def get_tag_dict(self):
155
refs = self.get_refs()
156
for (name, peeled, unpeeled, bzr_revid) in self._iter_tag_refs(refs):
157
ret[name] = bzr_revid
161
class LocalGitTagDict(GitTags):
162
"""Dictionary with tags in a local repository."""
164
def __init__(self, branch):
165
super(LocalGitTagDict, self).__init__(branch)
166
self.refs = self.repository._git.refs
169
return self.repository._git.get_refs()
171
def _iter_tag_refs(self, refs):
172
"""Iterate over the tag refs.
174
:param refs: Refs dictionary (name -> git sha1)
175
:return: iterator over (name, peeled_sha1, unpeeled_sha1, bzr_revid)
177
for k, (peeled, unpeeled) in extract_tags(refs).iteritems():
179
obj = self.repository._git[peeled]
181
mutter("Tag %s points at unknown object %s, ignoring", peeled,
184
# FIXME: this shouldn't really be necessary, the repository
185
# already should have these unpeeled.
186
while isinstance(obj, Tag):
187
peeled = obj.object[1]
188
obj = self.repository._git[peeled]
189
if not isinstance(obj, Commit):
190
mutter("Tag %s points at object %r that is not a commit, "
193
yield (k, peeled, unpeeled,
194
self.branch.lookup_foreign_revision_id(peeled))
196
def _set_tag_dict(self, to_dict):
197
extra = set(self.get_refs().keys())
198
for k, revid in to_dict.iteritems():
199
name = tag_name_to_ref(k)
202
self.set_tag(k, revid)
205
del self.repository._git[name]
207
def set_tag(self, name, revid):
208
self.refs[tag_name_to_ref(name)], _ = \
209
self.branch.lookup_bzr_revision_id(revid)
212
class DictTagDict(tag.BasicTags):
214
def __init__(self, branch, tags):
215
super(DictTagDict, self).__init__(branch)
218
def get_tag_dict(self):
222
class GitBranchFormat(branch.BranchFormat):
224
def get_format_description(self):
227
def network_name(self):
230
def supports_tags(self):
233
def get_foreign_tests_branch_factory(self):
234
from bzrlib.plugins.git.tests.test_branch import ForeignTestsBranchFactory
235
return ForeignTestsBranchFactory()
237
def make_tags(self, branch):
238
if getattr(branch.repository, "get_refs", None) is not None:
239
from bzrlib.plugins.git.remote import RemoteGitTagDict
240
return RemoteGitTagDict(branch)
242
return LocalGitTagDict(branch)
245
class GitReadLock(object):
247
def __init__(self, unlock):
251
class GitWriteLock(object):
253
def __init__(self, unlock):
257
class GitBranch(ForeignBranch):
258
"""An adapter to git repositories for bzr Branch objects."""
260
def __init__(self, bzrdir, repository, ref, lockfiles, tagsdict=None):
261
self.repository = repository
262
self._format = GitBranchFormat()
263
self.control_files = lockfiles
265
super(GitBranch, self).__init__(repository.get_mapping())
266
if tagsdict is not None:
267
self.tags = DictTagDict(self, tagsdict)
269
self.name = ref_to_branch_name(ref)
271
self.base = bzrdir.root_transport.base
273
def _get_checkout_format(self):
274
"""Return the most suitable metadir for a checkout of this branch.
275
Weaves are used if this branch's repository uses weaves.
277
return bzrdir.format_registry.make_bzrdir("default")
279
def get_child_submit_format(self):
280
"""Return the preferred format of submissions to this branch."""
281
ret = self.get_config().get_user_option("child_submit_format")
286
def _get_nick(self, local=False, possible_master_transports=None):
287
"""Find the nick name for this branch.
291
return self.name or "HEAD"
293
def _set_nick(self, nick):
294
raise NotImplementedError
296
nick = property(_get_nick, _set_nick)
299
return "<%s(%r, %r)>" % (self.__class__.__name__, self.repository.base,
302
def generate_revision_history(self, revid, old_revid=None):
303
# FIXME: Check that old_revid is in the ancestry of revid
304
newhead, self.mapping = self.mapping.revision_id_bzr_to_foreign(revid)
305
self._set_head(newhead)
307
def lock_write(self):
308
self.control_files.lock_write()
309
return GitWriteLock(self.unlock)
311
def get_stacked_on_url(self):
312
# Git doesn't do stacking (yet...)
313
raise errors.UnstackableBranchFormat(self._format, self.base)
315
def get_parent(self):
316
"""See Branch.get_parent()."""
317
# FIXME: Set "origin" url from .git/config ?
320
def set_parent(self, url):
321
# FIXME: Set "origin" url in .git/config ?
325
self.control_files.lock_read()
326
return GitReadLock(self.unlock)
329
return self.control_files.is_locked()
332
self.control_files.unlock()
334
def get_physical_lock_status(self):
338
def last_revision(self):
339
# perhaps should escape this ?
340
if self.head is None:
341
return revision.NULL_REVISION
342
return self.lookup_foreign_revision_id(self.head)
344
def _basic_push(self, target, overwrite=False, stop_revision=None):
345
return branch.InterBranch.get(self, target)._basic_push(
346
overwrite, stop_revision)
348
def lookup_foreign_revision_id(self, foreign_revid):
349
return self.repository.lookup_foreign_revision_id(foreign_revid,
352
def lookup_bzr_revision_id(self, revid):
353
return self.repository.lookup_bzr_revision_id(
354
revid, mapping=self.mapping)
357
class LocalGitBranch(GitBranch):
358
"""A local Git branch."""
360
def __init__(self, bzrdir, repository, name, lockfiles, tagsdict=None):
361
super(LocalGitBranch, self).__init__(bzrdir, repository, name,
363
refs = repository._git.get_refs()
364
if not (name in refs.keys() or "HEAD" in refs.keys()):
365
raise errors.NotBranchError(self.base)
367
def create_checkout(self, to_location, revision_id=None, lightweight=False,
368
accelerator_tree=None, hardlink=False):
370
t = transport.get_transport(to_location)
372
format = self._get_checkout_format()
373
checkout = format.initialize_on_transport(t)
374
from_branch = branch.BranchReferenceFormat().initialize(checkout,
376
tree = checkout.create_workingtree(revision_id,
377
from_branch=from_branch, hardlink=hardlink)
380
return self._create_heavyweight_checkout(to_location, revision_id,
383
def _create_heavyweight_checkout(self, to_location, revision_id=None,
385
"""Create a new heavyweight checkout of this branch.
387
:param to_location: URL of location to create the new checkout in.
388
:param revision_id: Revision that should be the tip of the checkout.
389
:param hardlink: Whether to hardlink
390
:return: WorkingTree object of checkout.
392
checkout_branch = bzrdir.BzrDir.create_branch_convenience(
393
to_location, force_new_tree=False)
394
checkout = checkout_branch.bzrdir
395
checkout_branch.bind(self)
396
# pull up to the specified revision_id to set the initial
397
# branch tip correctly, and seed it with history.
398
checkout_branch.pull(self, stop_revision=revision_id)
399
return checkout.create_workingtree(revision_id, hardlink=hardlink)
401
def _gen_revision_history(self):
402
if self.head is None:
404
ret = list(self.repository.iter_reverse_revision_history(
405
self.last_revision()))
411
return self.repository._git.ref(self.ref or "HEAD")
415
def set_last_revision_info(self, revno, revid):
416
self.set_last_revision(revid)
418
def set_last_revision(self, revid):
419
(newhead, self.mapping) = self.repository.lookup_bzr_revision_id(revid)
422
def _set_head(self, value):
424
self.repository._git.refs[self.ref or "HEAD"] = self._head
425
self._clear_cached_state()
427
head = property(_get_head, _set_head)
429
def get_config(self):
430
return GitBranchConfig(self)
432
def get_push_location(self):
433
"""See Branch.get_push_location."""
434
push_loc = self.get_config().get_user_option('push_location')
437
def set_push_location(self, location):
438
"""See Branch.set_push_location."""
439
self.get_config().set_user_option('push_location', location,
440
store=config.STORE_LOCATION)
442
def supports_tags(self):
446
def _quick_lookup_revno(local_branch, remote_branch, revid):
447
assert isinstance(revid, str), "was %r" % revid
448
# Try in source branch first, it'll be faster
450
return local_branch.revision_id_to_revno(revid)
451
except errors.NoSuchRevision:
452
graph = local_branch.repository.get_graph()
454
return graph.find_distance_to_null(revid)
455
except errors.GhostRevisionsHaveNoRevno:
456
# FIXME: Check using graph.find_distance_to_null() ?
457
return remote_branch.revision_id_to_revno(revid)
460
class GitBranchPullResult(branch.PullResult):
463
super(GitBranchPullResult, self).__init__()
464
self.new_git_head = None
465
self._old_revno = None
466
self._new_revno = None
468
def report(self, to_file):
470
if self.old_revid == self.new_revid:
471
to_file.write('No revisions to pull.\n')
472
elif self.new_git_head is not None:
473
to_file.write('Now on revision %d (git sha: %s).\n' %
474
(self.new_revno, self.new_git_head))
476
to_file.write('Now on revision %d.\n' % (self.new_revno,))
477
self._show_tag_conficts(to_file)
479
def _lookup_revno(self, revid):
480
return _quick_lookup_revno(self.target_branch, self.source_branch, revid)
482
def _get_old_revno(self):
483
if self._old_revno is not None:
484
return self._old_revno
485
return self._lookup_revno(self.old_revid)
487
def _set_old_revno(self, revno):
488
self._old_revno = revno
490
old_revno = property(_get_old_revno, _set_old_revno)
492
def _get_new_revno(self):
493
if self._new_revno is not None:
494
return self._new_revno
495
return self._lookup_revno(self.new_revid)
497
def _set_new_revno(self, revno):
498
self._new_revno = revno
500
new_revno = property(_get_new_revno, _set_new_revno)
503
class GitBranchPushResult(branch.BranchPushResult):
505
def _lookup_revno(self, revid):
506
return _quick_lookup_revno(self.source_branch, self.target_branch, revid)
510
return self._lookup_revno(self.old_revid)
514
new_original_revno = getattr(self, "new_original_revno", None)
515
if new_original_revno:
516
return new_original_revno
517
if getattr(self, "new_original_revid", None) is not None:
518
return self._lookup_revno(self.new_original_revid)
519
return self._lookup_revno(self.new_revid)
522
class InterFromGitBranch(branch.GenericInterBranch):
523
"""InterBranch implementation that pulls from Git into bzr."""
526
def _get_branch_formats_to_test():
530
def _get_interrepo(self, source, target):
531
return repository.InterRepository.get(source.repository,
535
def is_compatible(cls, source, target):
536
return (isinstance(source, GitBranch) and
537
not isinstance(target, GitBranch) and
538
(getattr(cls._get_interrepo(source, target), "fetch_objects", None) is not None))
540
def _update_revisions(self, stop_revision=None, overwrite=False,
541
graph=None, limit=None):
542
"""Like InterBranch.update_revisions(), but with additions.
544
Compared to the `update_revisions()` below, this function takes a
545
`limit` argument that limits how many git commits will be converted
546
and returns the new git head and remote refs.
548
interrepo = self._get_interrepo(self.source, self.target)
549
def determine_wants(heads):
550
if self.source.ref is not None and not self.source.ref in heads:
551
raise NoSuchRef(self.source.ref, heads.keys())
552
if stop_revision is not None:
553
self._last_revid = stop_revision
554
head, mapping = self.source.repository.lookup_bzr_revision_id(
557
if self.source.ref is not None:
558
head = heads[self.source.ref]
561
self._last_revid = self.source.lookup_foreign_revision_id(head)
562
if self.target.repository.has_revision(self._last_revid):
565
pack_hint, head, refs = interrepo.fetch_objects(
566
determine_wants, self.source.mapping, limit=limit)
567
if (pack_hint is not None and
568
self.target.repository._format.pack_compresses):
569
self.target.repository.pack(hint=pack_hint)
571
self._last_revid = self.source.lookup_foreign_revision_id(head)
573
prev_last_revid = None
575
prev_last_revid = self.target.last_revision()
576
self.target.generate_revision_history(self._last_revid,
580
def update_revisions(self, stop_revision=None, overwrite=False,
582
"""See InterBranch.update_revisions()."""
583
self._update_revisions(stop_revision, overwrite, graph)
585
def pull(self, overwrite=False, stop_revision=None,
586
possible_transports=None, _hook_master=None, run_hooks=True,
587
_override_hook_target=None, local=False, limit=None):
590
:param _hook_master: Private parameter - set the branch to
591
be supplied as the master to pull hooks.
592
:param run_hooks: Private parameter - if false, this branch
593
is being called because it's the master of the primary branch,
594
so it should not run its hooks.
595
:param _override_hook_target: Private parameter - set the branch to be
596
supplied as the target_branch to pull hooks.
597
:param limit: Only import this many revisons. `None`, the default,
598
means import all revisions.
600
# This type of branch can't be bound.
602
raise errors.LocalRequiresBoundBranch()
603
result = GitBranchPullResult()
604
result.source_branch = self.source
605
if _override_hook_target is None:
606
result.target_branch = self.target
608
result.target_branch = _override_hook_target
609
self.source.lock_read()
611
# We assume that during 'pull' the target repository is closer than
613
graph = self.target.repository.get_graph(self.source.repository)
614
(result.old_revno, result.old_revid) = \
615
self.target.last_revision_info()
616
result.new_git_head, remote_refs = self._update_revisions(
617
stop_revision, overwrite=overwrite, graph=graph, limit=limit)
618
result.tag_conflicts = self.source.tags.merge_to(self.target.tags,
620
(result.new_revno, result.new_revid) = \
621
self.target.last_revision_info()
623
result.master_branch = _hook_master
624
result.local_branch = result.target_branch
626
result.master_branch = result.target_branch
627
result.local_branch = None
629
for hook in branch.Branch.hooks['post_pull']:
635
def _basic_push(self, overwrite=False, stop_revision=None):
636
result = branch.BranchPushResult()
637
result.source_branch = self.source
638
result.target_branch = self.target
639
graph = self.target.repository.get_graph(self.source.repository)
640
result.old_revno, result.old_revid = self.target.last_revision_info()
641
result.new_git_head, remote_refs = self._update_revisions(
642
stop_revision, overwrite=overwrite, graph=graph)
643
result.tag_conflicts = self.source.tags.merge_to(self.target.tags,
645
result.new_revno, result.new_revid = self.target.last_revision_info()
649
class InterGitBranch(branch.GenericInterBranch):
650
"""InterBranch implementation that pulls between Git branches."""
653
class InterGitLocalRemoteBranch(InterGitBranch):
654
"""InterBranch that copies from a local to a remote git branch."""
657
def _get_branch_formats_to_test():
661
def is_compatible(self, source, target):
662
from bzrlib.plugins.git.remote import RemoteGitBranch
663
return (isinstance(source, LocalGitBranch) and
664
isinstance(target, RemoteGitBranch))
666
def _basic_push(self, overwrite=False, stop_revision=None):
667
from dulwich.protocol import ZERO_SHA
668
result = GitBranchPushResult()
669
result.source_branch = self.source
670
result.target_branch = self.target
671
if stop_revision is None:
672
stop_revision = self.source.last_revision()
673
# FIXME: Check for diverged branches
674
def get_changed_refs(old_refs):
675
result.old_revid = self.target.lookup_foreign_revision_id(old_refs.get(self.target.ref, ZERO_SHA))
676
refs = { self.target.ref: self.source.repository.lookup_bzr_revision_id(stop_revision)[0] }
677
result.new_revid = stop_revision
678
for name, sha in self.source.repository._git.refs.as_dict("refs/tags").iteritems():
679
refs[tag_name_to_ref(name)] = sha
681
self.target.repository.send_pack(get_changed_refs,
682
self.source.repository._git.object_store.generate_pack_contents)
686
class InterGitRemoteLocalBranch(InterGitBranch):
687
"""InterBranch that copies from a remote to a local git branch."""
690
def _get_branch_formats_to_test():
694
def is_compatible(self, source, target):
695
from bzrlib.plugins.git.remote import RemoteGitBranch
696
return (isinstance(source, RemoteGitBranch) and
697
isinstance(target, LocalGitBranch))
699
def _basic_push(self, overwrite=False, stop_revision=None):
700
result = branch.BranchPushResult()
701
result.source_branch = self.source
702
result.target_branch = self.target
703
result.old_revid = self.target.last_revision()
704
refs, stop_revision = self.update_refs(stop_revision)
705
self.target.generate_revision_history(stop_revision, result.old_revid)
706
result.tag_conflicts = self.source.tags.merge_to(self.target.tags,
707
source_refs=refs, overwrite=overwrite)
708
result.new_revid = self.target.last_revision()
711
def update_refs(self, stop_revision=None):
712
interrepo = repository.InterRepository.get(self.source.repository,
713
self.target.repository)
714
if stop_revision is None:
715
refs = interrepo.fetch(branches=["HEAD"])
716
stop_revision = self.target.lookup_foreign_revision_id(refs["HEAD"])
718
refs = interrepo.fetch(revision_id=stop_revision)
719
return refs, stop_revision
721
def pull(self, stop_revision=None, overwrite=False,
722
possible_transports=None, run_hooks=True,local=False):
723
# This type of branch can't be bound.
725
raise errors.LocalRequiresBoundBranch()
726
result = GitPullResult()
727
result.source_branch = self.source
728
result.target_branch = self.target
729
result.old_revid = self.target.last_revision()
730
refs, stop_revision = self.update_refs(stop_revision)
731
self.target.generate_revision_history(stop_revision, result.old_revid)
732
result.tag_conflicts = self.source.tags.merge_to(self.target.tags,
733
overwrite=overwrite, source_refs=refs)
734
result.new_revid = self.target.last_revision()
738
class InterToGitBranch(branch.GenericInterBranch):
739
"""InterBranch implementation that pulls from Git into bzr."""
741
def __init__(self, source, target):
742
super(InterToGitBranch, self).__init__(source, target)
743
self.interrepo = repository.InterRepository.get(source.repository,
747
def _get_branch_formats_to_test():
751
def is_compatible(self, source, target):
752
return (not isinstance(source, GitBranch) and
753
isinstance(target, GitBranch))
755
def update_revisions(self, *args, **kwargs):
756
raise NoPushSupport()
758
def _get_new_refs(self, stop_revision=None):
759
if stop_revision is None:
760
(stop_revno, stop_revision) = self.source.last_revision_info()
761
assert type(stop_revision) is str
762
main_ref = self.target.ref or "refs/heads/master"
763
refs = { main_ref: (None, stop_revision) }
764
for name, revid in self.source.tags.get_tag_dict().iteritems():
765
if self.source.repository.has_revision(revid):
766
refs[tag_name_to_ref(name)] = (None, revid)
767
return refs, main_ref, (stop_revno, stop_revision)
769
def pull(self, overwrite=False, stop_revision=None, local=False,
770
possible_transports=None):
771
from dulwich.protocol import ZERO_SHA
772
result = GitBranchPullResult()
773
result.source_branch = self.source
774
result.target_branch = self.target
775
new_refs, main_ref, stop_revinfo = self._get_new_refs(stop_revision)
776
def update_refs(old_refs):
777
refs = dict(old_refs)
778
# FIXME: Check for diverged branches
779
refs.update(new_refs)
781
old_refs, new_refs = self.interrepo.fetch_refs(update_refs)
782
(result.old_revid, old_sha1) = old_refs.get(main_ref, (ZERO_SHA, NULL_REVISION))
783
if result.old_revid is None:
784
result.old_revid = self.target.lookup_foreign_revision_id(old_sha1)
785
result.new_revid = new_refs[main_ref][1]
788
def push(self, overwrite=False, stop_revision=None,
789
_override_hook_source_branch=None):
790
from dulwich.protocol import ZERO_SHA
791
result = GitBranchPushResult()
792
result.source_branch = self.source
793
result.target_branch = self.target
794
new_refs, main_ref, stop_revinfo = self._get_new_refs(stop_revision)
795
def update_refs(old_refs):
796
refs = dict(old_refs)
797
# FIXME: Check for diverged branches
798
refs.update(new_refs)
800
old_refs, new_refs = self.interrepo.fetch_refs(update_refs)
801
(result.old_revid, old_sha1) = old_refs.get(main_ref, (ZERO_SHA, NULL_REVISION))
802
if result.old_revid is None:
803
result.old_revid = self.target.lookup_foreign_revision_id(old_sha1)
804
result.new_revid = new_refs[main_ref][1]
807
def lossy_push(self, stop_revision=None):
808
result = GitBranchPushResult()
809
result.source_branch = self.source
810
result.target_branch = self.target
811
new_refs, main_ref, stop_revinfo = self._get_new_refs(stop_revision)
812
def update_refs(old_refs):
813
refs = dict(old_refs)
814
# FIXME: Check for diverged branches
815
refs.update(new_refs)
817
result.revidmap, old_refs, new_refs = self.interrepo.dfetch_refs(
819
result.old_revid = old_refs.get(self.target.ref, (None, NULL_REVISION))[1]
820
result.new_revid = new_refs[main_ref][1]
821
(result.new_original_revno, result.new_original_revid) = stop_revinfo
825
branch.InterBranch.register_optimiser(InterGitRemoteLocalBranch)
826
branch.InterBranch.register_optimiser(InterFromGitBranch)
827
branch.InterBranch.register_optimiser(InterToGitBranch)
828
branch.InterBranch.register_optimiser(InterGitLocalRemoteBranch)