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
91
def _iter_tag_refs(self, refs):
92
raise NotImplementedError(self._iter_tag_refs)
94
def _merge_to_git(self, to_tags, refs, overwrite=False):
95
target_repo = to_tags.repository
97
for k, v in refs.iteritems():
100
if overwrite or not k in self.target.repository.refs:
101
target_repo.refs[k] = v
102
elif target_repo.repository.refs[k] == v:
105
conflicts.append((ref_to_tag_name(k), v, target_repo.refs[k]))
108
def _merge_to_non_git(self, to_tags, refs, overwrite=False):
109
unpeeled_map = defaultdict(set)
111
result = dict(to_tags.get_tag_dict())
112
for n, peeled, unpeeled, bzr_revid in self._iter_tag_refs(refs):
113
if unpeeled is not None:
114
unpeeled_map[peeled].add(unpeeled)
115
if n not in result or overwrite:
116
result[n] = bzr_revid
117
elif result[n] == bzr_revid:
120
conflicts.append((n, result[n], bzr_revid))
121
to_tags._set_tag_dict(result)
122
if len(unpeeled_map) > 0:
123
map_file = UnpeelMap.from_repository(to_tags.branch.repository)
124
map_file.update(unpeeled_map)
125
map_file.save_in_repository(to_tags.branch.repository)
128
def merge_to(self, to_tags, overwrite=False, ignore_master=False,
130
if source_refs is None:
131
source_refs = self.repository.get_refs()
134
if isinstance(to_tags, GitTags):
135
return self._merge_to_git(to_tags, source_refs,
141
master = to_tags.branch.get_master_branch()
142
conflicts = self._merge_to_non_git(to_tags, source_refs,
144
if master is not None:
145
conflicts += self.merge_to(to_tags, overwrite=overwrite,
146
source_refs=source_refs,
147
ignore_master=ignore_master)
150
def get_tag_dict(self):
152
refs = self.repository.get_refs()
153
for (name, peeled, unpeeled, bzr_revid) in self._iter_tag_refs(refs):
154
ret[name] = bzr_revid
158
class LocalGitTagDict(GitTags):
159
"""Dictionary with tags in a local repository."""
161
def __init__(self, branch):
162
super(LocalGitTagDict, self).__init__(branch)
163
self.refs = self.repository._git.refs
165
def _iter_tag_refs(self, refs):
166
"""Iterate over the tag refs.
168
:param refs: Refs dictionary (name -> git sha1)
169
:return: iterator over (name, peeled_sha1, unpeeled_sha1, bzr_revid)
171
for k, (peeled, unpeeled) in extract_tags(refs).iteritems():
173
obj = self.repository._git[peeled]
175
mutter("Tag %s points at unknown object %s, ignoring", peeled,
178
# FIXME: this shouldn't really be necessary, the repository
179
# already should have these unpeeled.
180
while isinstance(obj, Tag):
181
peeled = obj.object[1]
182
obj = self.repository._git[peeled]
183
if not isinstance(obj, Commit):
184
mutter("Tag %s points at object %r that is not a commit, "
187
yield (k, peeled, unpeeled,
188
self.branch.lookup_foreign_revision_id(peeled))
191
def _set_tag_dict(self, to_dict):
192
extra = set(self.repository._git.get_refs().keys())
193
for k, revid in to_dict.iteritems():
194
name = tag_name_to_ref(k)
197
self.set_tag(k, revid)
200
del self.repository._git[name]
202
def set_tag(self, name, revid):
203
self.refs[tag_name_to_ref(name)], _ = \
204
self.branch.lookup_bzr_revision_id(revid)
207
class DictTagDict(LocalGitTagDict):
209
def __init__(self, branch, tags):
210
super(DictTagDict, self).__init__(branch)
213
def get_tag_dict(self):
217
class GitBranchFormat(branch.BranchFormat):
219
def get_format_description(self):
222
def network_name(self):
225
def supports_tags(self):
228
def get_foreign_tests_branch_factory(self):
229
from bzrlib.plugins.git.tests.test_branch import ForeignTestsBranchFactory
230
return ForeignTestsBranchFactory()
232
def make_tags(self, branch):
233
if getattr(branch.repository, "get_refs", None) is not None:
234
from bzrlib.plugins.git.remote import RemoteGitTagDict
235
return RemoteGitTagDict(branch)
237
return LocalGitTagDict(branch)
240
class GitReadLock(object):
242
def __init__(self, unlock):
246
class GitWriteLock(object):
248
def __init__(self, unlock):
252
class GitBranch(ForeignBranch):
253
"""An adapter to git repositories for bzr Branch objects."""
255
def __init__(self, bzrdir, repository, ref, lockfiles, tagsdict=None):
256
self.repository = repository
257
self._format = GitBranchFormat()
258
self.control_files = lockfiles
260
super(GitBranch, self).__init__(repository.get_mapping())
261
if tagsdict is not None:
262
self.tags = DictTagDict(self, tagsdict)
264
self.name = ref_to_branch_name(ref)
266
self.base = bzrdir.root_transport.base
268
def _get_checkout_format(self):
269
"""Return the most suitable metadir for a checkout of this branch.
270
Weaves are used if this branch's repository uses weaves.
272
return bzrdir.format_registry.make_bzrdir("default")
274
def get_child_submit_format(self):
275
"""Return the preferred format of submissions to this branch."""
276
ret = self.get_config().get_user_option("child_submit_format")
281
def _get_nick(self, local=False, possible_master_transports=None):
282
"""Find the nick name for this branch.
286
return self.name or "HEAD"
288
def _set_nick(self, nick):
289
raise NotImplementedError
291
nick = property(_get_nick, _set_nick)
294
return "<%s(%r, %r)>" % (self.__class__.__name__, self.repository.base,
297
def generate_revision_history(self, revid, old_revid=None):
298
# FIXME: Check that old_revid is in the ancestry of revid
299
newhead, self.mapping = self.mapping.revision_id_bzr_to_foreign(revid)
300
self._set_head(newhead)
302
def lock_write(self):
303
self.control_files.lock_write()
304
return GitWriteLock(self.unlock)
306
def get_stacked_on_url(self):
307
# Git doesn't do stacking (yet...)
308
raise errors.UnstackableBranchFormat(self._format, self.base)
310
def get_parent(self):
311
"""See Branch.get_parent()."""
312
# FIXME: Set "origin" url from .git/config ?
315
def set_parent(self, url):
316
# FIXME: Set "origin" url in .git/config ?
320
self.control_files.lock_read()
321
return GitReadLock(self.unlock)
324
return self.control_files.is_locked()
327
self.control_files.unlock()
329
def get_physical_lock_status(self):
333
def last_revision(self):
334
# perhaps should escape this ?
335
if self.head is None:
336
return revision.NULL_REVISION
337
return self.lookup_foreign_revision_id(self.head)
339
def _basic_push(self, target, overwrite=False, stop_revision=None):
340
return branch.InterBranch.get(self, target)._basic_push(
341
overwrite, stop_revision)
343
def lookup_foreign_revision_id(self, foreign_revid):
344
return self.repository.lookup_foreign_revision_id(foreign_revid,
347
def lookup_bzr_revision_id(self, revid):
348
return self.repository.lookup_bzr_revision_id(
349
revid, mapping=self.mapping)
352
class LocalGitBranch(GitBranch):
353
"""A local Git branch."""
355
def __init__(self, bzrdir, repository, name, lockfiles, tagsdict=None):
356
super(LocalGitBranch, self).__init__(bzrdir, repository, name,
358
refs = repository._git.get_refs()
359
if not (name in refs.keys() or "HEAD" in refs.keys()):
360
raise errors.NotBranchError(self.base)
362
def create_checkout(self, to_location, revision_id=None, lightweight=False,
363
accelerator_tree=None, hardlink=False):
365
t = transport.get_transport(to_location)
367
format = self._get_checkout_format()
368
checkout = format.initialize_on_transport(t)
369
from_branch = branch.BranchReferenceFormat().initialize(checkout,
371
tree = checkout.create_workingtree(revision_id,
372
from_branch=from_branch, hardlink=hardlink)
375
return self._create_heavyweight_checkout(to_location, revision_id,
378
def _create_heavyweight_checkout(self, to_location, revision_id=None,
380
"""Create a new heavyweight checkout of this branch.
382
:param to_location: URL of location to create the new checkout in.
383
:param revision_id: Revision that should be the tip of the checkout.
384
:param hardlink: Whether to hardlink
385
:return: WorkingTree object of checkout.
387
checkout_branch = bzrdir.BzrDir.create_branch_convenience(
388
to_location, force_new_tree=False)
389
checkout = checkout_branch.bzrdir
390
checkout_branch.bind(self)
391
# pull up to the specified revision_id to set the initial
392
# branch tip correctly, and seed it with history.
393
checkout_branch.pull(self, stop_revision=revision_id)
394
return checkout.create_workingtree(revision_id, hardlink=hardlink)
396
def _gen_revision_history(self):
397
if self.head is None:
399
ret = list(self.repository.iter_reverse_revision_history(
400
self.last_revision()))
406
return self.repository._git.ref(self.ref or "HEAD")
410
def set_last_revision_info(self, revno, revid):
411
self.set_last_revision(revid)
413
def set_last_revision(self, revid):
414
(newhead, self.mapping) = self.repository.lookup_bzr_revision_id(revid)
417
def _set_head(self, value):
419
self.repository._git.refs[self.ref or "HEAD"] = self._head
420
self._clear_cached_state()
422
head = property(_get_head, _set_head)
424
def get_config(self):
425
return GitBranchConfig(self)
427
def get_push_location(self):
428
"""See Branch.get_push_location."""
429
push_loc = self.get_config().get_user_option('push_location')
432
def set_push_location(self, location):
433
"""See Branch.set_push_location."""
434
self.get_config().set_user_option('push_location', location,
435
store=config.STORE_LOCATION)
437
def supports_tags(self):
441
def _quick_lookup_revno(local_branch, remote_branch, revid):
442
assert isinstance(revid, str), "was %r" % revid
443
# Try in source branch first, it'll be faster
445
return local_branch.revision_id_to_revno(revid)
446
except errors.NoSuchRevision:
447
graph = local_branch.repository.get_graph()
449
return graph.find_distance_to_null(revid)
450
except errors.GhostRevisionsHaveNoRevno:
451
# FIXME: Check using graph.find_distance_to_null() ?
452
return remote_branch.revision_id_to_revno(revid)
455
class GitBranchPullResult(branch.PullResult):
458
super(GitBranchPullResult, self).__init__()
459
self.new_git_head = None
460
self._old_revno = None
461
self._new_revno = None
463
def report(self, to_file):
465
if self.old_revid == self.new_revid:
466
to_file.write('No revisions to pull.\n')
467
elif self.new_git_head is not None:
468
to_file.write('Now on revision %d (git sha: %s).\n' %
469
(self.new_revno, self.new_git_head))
471
to_file.write('Now on revision %d.\n' % (self.new_revno,))
472
self._show_tag_conficts(to_file)
474
def _lookup_revno(self, revid):
475
return _quick_lookup_revno(self.target_branch, self.source_branch, revid)
477
def _get_old_revno(self):
478
if self._old_revno is not None:
479
return self._old_revno
480
return self._lookup_revno(self.old_revid)
482
def _set_old_revno(self, revno):
483
self._old_revno = revno
485
old_revno = property(_get_old_revno, _set_old_revno)
487
def _get_new_revno(self):
488
if self._new_revno is not None:
489
return self._new_revno
490
return self._lookup_revno(self.new_revid)
492
def _set_new_revno(self, revno):
493
self._new_revno = revno
495
new_revno = property(_get_new_revno, _set_new_revno)
498
class GitBranchPushResult(branch.BranchPushResult):
500
def _lookup_revno(self, revid):
501
return _quick_lookup_revno(self.source_branch, self.target_branch, revid)
505
return self._lookup_revno(self.old_revid)
509
new_original_revno = getattr(self, "new_original_revno", None)
510
if new_original_revno:
511
return new_original_revno
512
if getattr(self, "new_original_revid", None) is not None:
513
return self._lookup_revno(self.new_original_revid)
514
return self._lookup_revno(self.new_revid)
517
class InterFromGitBranch(branch.GenericInterBranch):
518
"""InterBranch implementation that pulls from Git into bzr."""
521
def _get_branch_formats_to_test():
525
def _get_interrepo(self, source, target):
526
return repository.InterRepository.get(source.repository,
530
def is_compatible(cls, source, target):
531
return (isinstance(source, GitBranch) and
532
not isinstance(target, GitBranch) and
533
(getattr(cls._get_interrepo(source, target), "fetch_objects", None) is not None))
535
def _update_revisions(self, stop_revision=None, overwrite=False,
536
graph=None, limit=None):
537
"""Like InterBranch.update_revisions(), but with additions.
539
Compared to the `update_revisions()` below, this function takes a
540
`limit` argument that limits how many git commits will be converted
541
and returns the new git head and remote refs.
543
interrepo = self._get_interrepo(self.source, self.target)
544
def determine_wants(heads):
545
if self.source.ref is not None and not self.source.ref in heads:
546
raise NoSuchRef(self.source.ref, heads.keys())
547
if stop_revision is not None:
548
self._last_revid = stop_revision
549
head, mapping = self.source.repository.lookup_bzr_revision_id(
552
if self.source.ref is not None:
553
head = heads[self.source.ref]
556
self._last_revid = self.source.lookup_foreign_revision_id(head)
557
if self.target.repository.has_revision(self._last_revid):
560
pack_hint, head, refs = interrepo.fetch_objects(
561
determine_wants, self.source.mapping, limit=limit)
562
if (pack_hint is not None and
563
self.target.repository._format.pack_compresses):
564
self.target.repository.pack(hint=pack_hint)
566
self._last_revid = self.source.lookup_foreign_revision_id(head)
568
prev_last_revid = None
570
prev_last_revid = self.target.last_revision()
571
self.target.generate_revision_history(self._last_revid,
575
def update_revisions(self, stop_revision=None, overwrite=False,
577
"""See InterBranch.update_revisions()."""
578
self._update_revisions(stop_revision, overwrite, graph)
580
def pull(self, overwrite=False, stop_revision=None,
581
possible_transports=None, _hook_master=None, run_hooks=True,
582
_override_hook_target=None, local=False, limit=None):
585
:param _hook_master: Private parameter - set the branch to
586
be supplied as the master to pull hooks.
587
:param run_hooks: Private parameter - if false, this branch
588
is being called because it's the master of the primary branch,
589
so it should not run its hooks.
590
:param _override_hook_target: Private parameter - set the branch to be
591
supplied as the target_branch to pull hooks.
592
:param limit: Only import this many revisons. `None`, the default,
593
means import all revisions.
595
# This type of branch can't be bound.
597
raise errors.LocalRequiresBoundBranch()
598
result = GitBranchPullResult()
599
result.source_branch = self.source
600
if _override_hook_target is None:
601
result.target_branch = self.target
603
result.target_branch = _override_hook_target
604
self.source.lock_read()
606
# We assume that during 'pull' the target repository is closer than
608
graph = self.target.repository.get_graph(self.source.repository)
609
(result.old_revno, result.old_revid) = \
610
self.target.last_revision_info()
611
result.new_git_head, remote_refs = self._update_revisions(
612
stop_revision, overwrite=overwrite, graph=graph, limit=limit)
613
result.tag_conflicts = self.source.tags.merge_to(self.target.tags,
615
(result.new_revno, result.new_revid) = \
616
self.target.last_revision_info()
618
result.master_branch = _hook_master
619
result.local_branch = result.target_branch
621
result.master_branch = result.target_branch
622
result.local_branch = None
624
for hook in branch.Branch.hooks['post_pull']:
630
def _basic_push(self, overwrite=False, stop_revision=None):
631
result = branch.BranchPushResult()
632
result.source_branch = self.source
633
result.target_branch = self.target
634
graph = self.target.repository.get_graph(self.source.repository)
635
result.old_revno, result.old_revid = self.target.last_revision_info()
636
result.new_git_head, remote_refs = self._update_revisions(
637
stop_revision, overwrite=overwrite, graph=graph)
638
result.tag_conflicts = self.source.tags.merge_to(self.target.tags,
640
result.new_revno, result.new_revid = self.target.last_revision_info()
644
class InterGitBranch(branch.GenericInterBranch):
645
"""InterBranch implementation that pulls between Git branches."""
648
class InterGitLocalRemoteBranch(InterGitBranch):
649
"""InterBranch that copies from a local to a remote git branch."""
652
def _get_branch_formats_to_test():
656
def is_compatible(self, source, target):
657
from bzrlib.plugins.git.remote import RemoteGitBranch
658
return (isinstance(source, LocalGitBranch) and
659
isinstance(target, RemoteGitBranch))
661
def _basic_push(self, overwrite=False, stop_revision=None):
662
from dulwich.protocol import ZERO_SHA
663
result = GitBranchPushResult()
664
result.source_branch = self.source
665
result.target_branch = self.target
666
if stop_revision is None:
667
stop_revision = self.source.last_revision()
668
# FIXME: Check for diverged branches
669
def get_changed_refs(old_refs):
670
result.old_revid = self.target.lookup_foreign_revision_id(old_refs.get(self.target.ref, ZERO_SHA))
671
refs = { self.target.ref: self.source.repository.lookup_bzr_revision_id(stop_revision)[0] }
672
result.new_revid = stop_revision
673
for name, sha in self.source.repository._git.refs.as_dict("refs/tags").iteritems():
674
refs[tag_name_to_ref(name)] = sha
676
self.target.repository.send_pack(get_changed_refs,
677
self.source.repository._git.object_store.generate_pack_contents)
681
class InterGitRemoteLocalBranch(InterGitBranch):
682
"""InterBranch that copies from a remote to a local git branch."""
685
def _get_branch_formats_to_test():
689
def is_compatible(self, source, target):
690
from bzrlib.plugins.git.remote import RemoteGitBranch
691
return (isinstance(source, RemoteGitBranch) and
692
isinstance(target, LocalGitBranch))
694
def _basic_push(self, overwrite=False, stop_revision=None):
695
result = branch.BranchPushResult()
696
result.source_branch = self.source
697
result.target_branch = self.target
698
result.old_revid = self.target.last_revision()
699
refs, stop_revision = self.update_refs(stop_revision)
700
self.target.generate_revision_history(stop_revision, result.old_revid)
701
result.tag_conflicts = self.source.tags.merge_to(self.target.tags,
702
source_refs=refs, overwrite=overwrite)
703
result.new_revid = self.target.last_revision()
706
def update_refs(self, stop_revision=None):
707
interrepo = repository.InterRepository.get(self.source.repository,
708
self.target.repository)
709
if stop_revision is None:
710
refs = interrepo.fetch(branches=["HEAD"])
711
stop_revision = self.target.lookup_foreign_revision_id(refs["HEAD"])
713
refs = interrepo.fetch(revision_id=stop_revision)
714
return refs, stop_revision
716
def pull(self, stop_revision=None, overwrite=False,
717
possible_transports=None, run_hooks=True,local=False):
718
# This type of branch can't be bound.
720
raise errors.LocalRequiresBoundBranch()
721
result = GitPullResult()
722
result.source_branch = self.source
723
result.target_branch = self.target
724
result.old_revid = self.target.last_revision()
725
refs, stop_revision = self.update_refs(stop_revision)
726
self.target.generate_revision_history(stop_revision, result.old_revid)
727
result.tag_conflicts = self.source.tags.merge_to(self.target.tags,
728
overwrite=overwrite, source_refs=refs)
729
result.new_revid = self.target.last_revision()
733
class InterToGitBranch(branch.GenericInterBranch):
734
"""InterBranch implementation that pulls from Git into bzr."""
736
def __init__(self, source, target):
737
super(InterToGitBranch, self).__init__(source, target)
738
self.interrepo = repository.InterRepository.get(source.repository,
742
def _get_branch_formats_to_test():
746
def is_compatible(self, source, target):
747
return (not isinstance(source, GitBranch) and
748
isinstance(target, GitBranch))
750
def update_revisions(self, *args, **kwargs):
751
raise NoPushSupport()
753
def _get_new_refs(self, stop_revision=None):
754
if stop_revision is None:
755
(stop_revno, stop_revision) = self.source.last_revision_info()
756
assert type(stop_revision) is str
757
main_ref = self.target.ref or "refs/heads/master"
758
refs = { main_ref: (None, stop_revision) }
759
for name, revid in self.source.tags.get_tag_dict().iteritems():
760
if self.source.repository.has_revision(revid):
761
refs[tag_name_to_ref(name)] = (None, revid)
762
return refs, main_ref, (stop_revno, stop_revision)
764
def pull(self, overwrite=False, stop_revision=None, local=False,
765
possible_transports=None):
766
from dulwich.protocol import ZERO_SHA
767
result = GitBranchPullResult()
768
result.source_branch = self.source
769
result.target_branch = self.target
770
new_refs, main_ref, stop_revinfo = self._get_new_refs(stop_revision)
771
def update_refs(old_refs):
772
refs = dict(old_refs)
773
# FIXME: Check for diverged branches
774
refs.update(new_refs)
776
old_refs, new_refs = self.interrepo.fetch_refs(update_refs)
777
(result.old_revid, old_sha1) = old_refs.get(main_ref, (ZERO_SHA, NULL_REVISION))
778
if result.old_revid is None:
779
result.old_revid = self.target.lookup_foreign_revision_id(old_sha1)
780
result.new_revid = new_refs[main_ref][1]
783
def push(self, overwrite=False, stop_revision=None,
784
_override_hook_source_branch=None):
785
from dulwich.protocol import ZERO_SHA
786
result = GitBranchPushResult()
787
result.source_branch = self.source
788
result.target_branch = self.target
789
new_refs, main_ref, stop_revinfo = self._get_new_refs(stop_revision)
790
def update_refs(old_refs):
791
refs = dict(old_refs)
792
# FIXME: Check for diverged branches
793
refs.update(new_refs)
795
old_refs, new_refs = self.interrepo.fetch_refs(update_refs)
796
(result.old_revid, old_sha1) = old_refs.get(main_ref, (ZERO_SHA, NULL_REVISION))
797
if result.old_revid is None:
798
result.old_revid = self.target.lookup_foreign_revision_id(old_sha1)
799
result.new_revid = new_refs[main_ref][1]
802
def lossy_push(self, stop_revision=None):
803
result = GitBranchPushResult()
804
result.source_branch = self.source
805
result.target_branch = self.target
806
new_refs, main_ref, stop_revinfo = self._get_new_refs(stop_revision)
807
def update_refs(old_refs):
808
refs = dict(old_refs)
809
# FIXME: Check for diverged branches
810
refs.update(new_refs)
812
result.revidmap, old_refs, new_refs = self.interrepo.dfetch_refs(
814
result.old_revid = old_refs.get(self.target.ref, (None, NULL_REVISION))[1]
815
result.new_revid = new_refs[main_ref][1]
816
(result.new_original_revno, result.new_original_revid) = stop_revinfo
820
branch.InterBranch.register_optimiser(InterGitRemoteLocalBranch)
821
branch.InterBranch.register_optimiser(InterFromGitBranch)
822
branch.InterBranch.register_optimiser(InterToGitBranch)
823
branch.InterBranch.register_optimiser(InterGitLocalRemoteBranch)