# HG changeset patch
# User Georges Racinet on mutations.racinet.fr <georges@racinet.fr>
# Date 1583746536 -3600
#      Mon Mar 09 10:35:36 2020 +0100
# Branch heptapod-0-8
# Node ID 687c6dd6073864f72d371fd22e7dad57e715f62d
# Parent  e79710251b38b6b98feb76def5225363f27541f6
started to rewrap hg-git

This is done by subclassing `GitHandler`, for now
adding `update_exportable_for_named_branches`, almost
identically.

The only functional difference is that named branches and
topics are always exported, i.e., not depending
on the value of `hg-git.export-named-branches`

diff --git a/hgext3rd/heptapod/__init__.py b/hgext3rd/heptapod/__init__.py
--- a/hgext3rd/heptapod/__init__.py
+++ b/hgext3rd/heptapod/__init__.py
@@ -22,6 +22,7 @@
 
 from . import (
     topic as topicmod,
+    git,
 )
 
 eh = exthelper.exthelper()
@@ -111,6 +112,11 @@
                                force=force, **opts)
 
 
+@command('gitlab-mirror')
+def gitlab_mirror(ui, repo):
+    git.gitlab_mirror(ui, repo)
+
+
 def runsystem(orig, ui, cmd, environ, cwd, out):
     heptapod_env = {k: v for k, v in ui.environ.items()
                     if k.startswith('HEPTAPOD_')}
diff --git a/hgext3rd/heptapod/git.py b/hgext3rd/heptapod/git.py
new file mode 100644
--- /dev/null
+++ b/hgext3rd/heptapod/git.py
@@ -0,0 +1,126 @@
+"""Interaction with Git repos.
+
+It is not expected that the rise of the HGitaly project would remove
+all interesting things to do with Git.
+"""
+from dulwich.protocol import ZERO_SHA
+from hggit.git_handler import GitHandler
+from mercurial.i18n import _
+from mercurial.node import hex
+from mercurial import (
+    error,
+)
+import re
+
+
+class HeptapodGitHandler(GitHandler):
+
+    def get_exportable(self):
+        """Heptapod version, including named branches and topics.
+
+        This rewraps :meth:`GitHandler.get_exportable` to add named branches
+        and topics to the returned Git refs
+        """
+        git_refs = super(HeptapodGitHandler, self).get_exportable()
+        logprefix = 'HeptapodGitHandler.get_exportable '
+        repo = self.repo.filtered('served')
+        ui = repo.ui
+
+        # "exporting" the ZERO_SHA will mean by convention a request to prune
+        # the corresponding heads -- only a request, we can't really decide
+        # at this stage.
+        # The value is a dict mapping refs to reasons (just strings)
+        to_prune = git_refs[ZERO_SHA] = {}
+
+        txn = repo.currenttransaction()
+        allow_bookmarks = ui.configbool('experimental',
+                                        'hg-git.bookmarks-on-named-branches')
+        bm_changes = txn.changes.get('bookmarks') if txn is not None else None
+        if bm_changes:
+            ui.note(
+                "HeptapodGitHandler bookmark changes=%r" % bm_changes)
+            new_bookmarks = []
+            for name, change in bm_changes.items():
+                if change[0] is None:
+                    new_bookmarks.append(name)
+                elif change[1] is None:
+                    # prune Git branch for removed bookmark
+                    to_prune['refs/heads/' + name] = 'deleted-bookmark'
+
+            if new_bookmarks and not allow_bookmarks:
+                raise error.Abort(_(
+                    "Creating bookmarks is forbidden by default in Heptapod "
+                    "(got these new ones: %r). "
+                    "See https://heptapod.net/pages/faq.html#bookmarks to "
+                    "learn why and how to partially lift "
+                    "that restriction" % new_bookmarks))
+            # TODO py3 compat (iteritems)
+            for new_bm_name, (_ign, new_bm_node) in bm_changes.iteritems():
+                new_bm_ctx = repo[new_bm_node]
+                new_bm_topic = new_bm_ctx.topic()
+                if new_bm_topic:
+                    raise error.Abort(_(
+                        "Creating or updating bookmarks on topic "
+                        "changesets is forbidden"),
+                        hint="topic %r and bookmark %r for changeset %s" % (
+                            new_bm_topic, new_bm_name, new_bm_ctx.hex()))
+
+        all_bookmarks = self.repo._bookmarks
+        # currently, self_filter_for_bookmarks() only does some renaming,
+        # and doesn't discard any, so this is actually just equivalent to
+        # hgshas of all bookmarks, but that may change in future hg-git
+        hgshas_with_bookmark_git_ref = {
+            hex(all_bookmarks[bm])
+            for _, bm in self._filter_for_bookmarks(all_bookmarks)}
+        for branch, hg_nodes in repo.branchmap().iteritems():
+            gb = self.git_branch_for_branchmap_branch(branch)
+
+            if ui.configbool('experimental',
+                             'hg-git.prune-newly-closed-branches'):
+                hg_nodes = [n for n in hg_nodes if not repo[n].closesbranch()]
+                if not hg_nodes:
+                    to_prune['refs/heads/' + gb] = 'closed'
+            hg_shas = {hex(n) for n in hg_nodes}
+
+            # We ignore bookmarked changesets because:
+            # - we don't want them in the 'wild' namespace
+            # - no other Git ref would be relevant for them
+            hg_shas.difference_update(hgshas_with_bookmark_git_ref)
+            if not hg_shas:
+                if hg_nodes and allow_bookmarks:
+                    # after removal of bookmarks, but not before,
+                    # there is no hg head on this named branch:
+                    # schedule potential removal. We don't want to do
+                    # this if bookmarks aren't explicitely allowed because
+                    # this must be a side effect, potentially disturbing, of
+                    # an implicit bookmark move
+                    to_prune['refs/heads/' + gb] = 'only-bookmarks'
+                continue
+
+            if 1 < len(hg_shas):
+                ui.note(logprefix, "Multiple heads for branch %r: %r" % (
+                    branch, hg_shas))
+                for hg_sha in hg_shas:
+                    git_refs[hg_sha].heads.add('refs/heads/wild/' + hg_sha)
+                gca = self.multiple_heads_choose(hg_shas, branch)
+                if gca is None:
+                    # giving up in order to avoid confusing situations
+                    continue
+                gca_sha = self.repo[gca].hex()
+                ui.note(logprefix,
+                        "Chose %r out of multiple heads %r "
+                        "for forwarding branch %r" % (
+                            gca_sha, hg_shas, branch))
+                hg_shas = [gca_sha]
+            hg_sha = hg_shas.pop()
+            gb = self.git_branch_for_branchmap_branch(branch)
+            git_refs[hg_sha].heads.add('refs/heads/' + gb)
+        return git_refs
+
+
+def gitlab_mirror(ui, repo):
+    """Export changesets as Git commits in the repo expected by GitLab."""
+    handler = HeptapodGitHandler(repo, repo.ui)
+    hgdir = re.sub(r'\.hg$', '', repo.root)
+    handler.gitdir = hgdir + '.git'
+    handler.export_commits()
diff --git a/hgext3rd/heptapod/tests/test_hg_git.py b/hgext3rd/heptapod/tests/test_hg_git.py
new file mode 100644
--- /dev/null
+++ b/hgext3rd/heptapod/tests/test_hg_git.py
@@ -0,0 +1,52 @@
+from __future__ import absolute_import
+
+import subprocess
+
+from heptapod.testhelpers import (
+    LocalRepoWrapper,
+    )
+from .utils import common_config
+
+
+class GitRepo(object):
+
+    def __init__(self, path):
+        self.path = path
+
+    @classmethod
+    def init(cls, path):
+        subprocess.call(('git', 'init', str(path)))
+        return cls(path)
+
+    def branch_titles(self):
+        out = subprocess.check_output(('git', 'branch', '-v'),
+                                      cwd=str(self.path))
+        split_lines = (l.split(None, 2) for l in out.splitlines())
+        return {sp[0]: sp[2] for sp in split_lines}
+
+
+def make_main_repo(path):
+    """Make repo with 2 public revs; return wrapper, ctx of rev 0
+
+    The returned ctx is for the first changeset because we'll use it as
+    a branching point, hence more often than the second.
+    """
+    config = common_config()
+    config['extensions']['hggit'] = ''
+    config['phases'] = dict(publish=False)
+
+    wrapper = LocalRepoWrapper.init(path, config=config)
+    ctx = wrapper.write_commit('foo', content='foo0', message="default0",
+                               return_ctx=True)
+    wrapper.write_commit('foo', content='foo1', message="default1")
+    wrapper.set_phase('public', ['.'])
+    return wrapper, ctx
+
+
+def test_basic(tmpdir):
+    repo_path = tmpdir.join('repo.hg')
+    repo, base_ctx = make_main_repo(repo_path)
+    git_repo = GitRepo.init(tmpdir.join('repo.git'))
+    repo.command('gitlab-mirror')
+
+    assert git_repo.branch_titles() == {'branch/default': 'default1'}
diff --git a/install-requirements.txt b/install-requirements.txt
--- a/install-requirements.txt
+++ b/install-requirements.txt
@@ -1,2 +1,3 @@
 Mercurial>=5.2
 hg-evolve>=9.2.1
+hg-git>=0.8.13