# HG changeset patch
# User Georges Racinet <georges.racinet@cloudcrane.io>
# Date 1736180568 -3600
#      Mon Jan 06 17:22:48 2025 +0100
# Node ID 216426df9b7fbad5a77aff8cf9fb9f2f7b18a664
# Parent  e298d64aee8df4e18938af5a744687e70bb65dd4
HeptapodGitHandler: Git repo move for native Mercurial now a separate method

We will soon make it called only when needed (not as part of the regular
`gitlab-mirror` command).

diff --git a/hgext3rd/heptapod/git.py b/hgext3rd/heptapod/git.py
--- a/hgext3rd/heptapod/git.py
+++ b/hgext3rd/heptapod/git.py
@@ -63,28 +63,41 @@
         if main_repo is None:
             main_repo = self.repo
 
-        old_gitdir = re.sub(br'\.hg$', b'', main_repo.root) + b'.git'
+        self.gitdir = re.sub(br'\.hg$', b'', main_repo.root) + b'.git'
+        if self.repo.ui.configbool(b'heptapod', b'native'):
+            self.native_project_ensure_set_git_dir()
+
+        self.unfiltered_repo = self.repo.unfiltered()
+        self._default_git_ref = None
+
+    def native_project_ensure_set_git_dir(self):
+        """Ensure existence of the auxiliary git repo for native Projects.
+
+        Even native Mercurial projects can have auxiliary Git repositories,
+        for mirroring to Git, although they are not alongside the Mercurial
+        repository and are managed in a different way (see heptapod#1963).
+
+        This makes sure that any Git repository for a native project is moved
+        over to the proper location.
+        """
         storages_root = self.repo.ui.config(b'heptapod', b'repositories-root')
         git_repos_root = os.path.join(storages_root,
                                       b'+hgitaly', b'hg-git')
         os.makedirs(git_repos_root, mode=0o700, exist_ok=True)
-        if self.repo.ui.configbool(b'heptapod', b'native'):
-            rel_path = os.path.relpath(old_gitdir, storages_root)
-            self.gitdir = os.path.join(git_repos_root, rel_path)
-            os.makedirs(os.path.dirname(self.gitdir),
-                        mode=0o700,
-                        exist_ok=True)
-            if (
-                    os.path.exists(old_gitdir)
-                    and not os.path.exists(self.gitdir)
-            ):
-                os.rename(old_gitdir, self.gitdir)
+
+        # After instantiation, `self.gitdir` is the proper one for
+        # legacy (hg-git based) projects
+        old_gitdir = self.gitdir
 
-        else:
-            self.gitdir = old_gitdir
+        rel_path = os.path.relpath(old_gitdir, storages_root)
+        self.gitdir = os.path.join(git_repos_root, rel_path)
 
-        self.unfiltered_repo = self.repo.unfiltered()
-        self._default_git_ref = None
+        os.makedirs(os.path.dirname(self.gitdir),
+                    mode=0o700,
+                    exist_ok=True)
+
+        if os.path.exists(old_gitdir) and not os.path.exists(self.gitdir):
+            os.rename(old_gitdir, self.gitdir)
 
     @property
     def gitlab_refs(self):
# HG changeset patch
# User Georges Racinet <georges.racinet@cloudcrane.io>
# Date 1735770234 -3600
#      Wed Jan 01 23:23:54 2025 +0100
# Node ID 6ae2947cdd94cf337a5bc69ab0222a93ceac67b9
# Parent  216426df9b7fbad5a77aff8cf9fb9f2f7b18a664
Native Mercurial: stop converting to Git by default

The conversion is now handled by the separate `hpd-export-native-to-git`, to
be called ultimately by the Rails app only for Git-push mirroring.

The separate command was already existing, as it was supporting the rollback
of the native migration, we just gave it a new alias.
It takes care of moving the Git repository to
its expected location if it turns out that it escaped the migration doing
so and is still right beside the Mercurial repository.

Releasing this is the first step of heptapod#1963

diff --git a/heptapod/VERSION b/heptapod/VERSION
--- a/heptapod/VERSION
+++ b/heptapod/VERSION
@@ -1,1 +1,1 @@
-4.8.1dev0
+5.0.0dev0
diff --git a/heptapod/testhelpers/git.py b/heptapod/testhelpers/git.py
--- a/heptapod/testhelpers/git.py
+++ b/heptapod/testhelpers/git.py
@@ -52,7 +52,10 @@
     def git(self, *args):
         return subprocess.check_output(('git', ) + args, cwd=str(self.path))
 
-    def delete(self):
+    def delete(self):  # pragma no cover (see below)
+        # We just removed the unique caller of this method. It was a direct
+        # call from a test, not something deeply buried in fixture code or
+        # similar (coverage drop would have been suspicious in that case).
         if os.path.exists(self.path):
             shutil.rmtree(self.path)
 
diff --git a/heptapod/wsgi.py b/heptapod/wsgi.py
--- a/heptapod/wsgi.py
+++ b/heptapod/wsgi.py
@@ -187,11 +187,6 @@
         if native:
             repo.ui.setconfig(b'heptapod', b'native',
                               pycompat.sysbytes(native))
-        if repo.ui.configbool(b'heptapod', b'native'):
-            no_git = env.get('HTTP_X_HEPTAPOD_NO_GIT')
-            if no_git is not None:
-                repo.ui.setconfig(b'heptapod', b'no-git',
-                                  pycompat.sysbytes(no_git))
         return repo
 
     def _runwsgi(self, env, respond):
diff --git a/hgext3rd/heptapod/__init__.py b/hgext3rd/heptapod/__init__.py
--- a/hgext3rd/heptapod/__init__.py
+++ b/hgext3rd/heptapod/__init__.py
@@ -86,7 +86,6 @@
     configitem(b'heptapod', b'allow-multiple-heads')
     configitem(b'heptapod', b'allow-bookmarks')
     configitem(b'heptapod', b'native', False)
-    configitem(b'heptapod', b'no-git', False)
 
 
 cmdtable = {}
@@ -130,9 +129,6 @@
     env_native = ui.environ.get(b'HEPTAPOD_HG_NATIVE', None)
     if env_native is not None:
         ui.setconfig(b'heptapod', b'native', env_native)
-        env_no_git = ui.environ.get(b'HEPTAPOD_NO_GIT', None)
-        if env_no_git is not None and ui.configbool(b'heptapod', b'native'):
-            ui.setconfig(b'heptapod', b'no-git', env_no_git)
 
 
 @command(
@@ -171,7 +167,7 @@
 @command(b'gitlab-mirror')
 def gitlab_mirror(ui, repo):
     """Export changesets as Git commits in the GitLab repository."""
-    if ui.configbool(b'heptapod', b'no-git'):
+    if ui.configbool(b'heptapod', b'native'):
         repo.ui.note(b'GitLab sync without Git export')
         no_git.NoGitStateMaintainer(ui, repo).sync()
     else:
@@ -182,15 +178,12 @@
 @command(b'move-hg-git-repo-out-of-gitaly-reach')
 def move_hg_git_repo_out_of_gitaly_reach(ui, repo):
     """Export changesets as Git commits in the GitLab repository."""
-    if ui.configbool(b'heptapod', b'no-git'):
-        raise error.Abort(b"This should not be run on a native repository "
-                          b"without Git conversion")
-
     if not ui.configbool(b'heptapod', b'native'):
         raise error.Abort(b"This should not be run on a repository based "
                           b"on hg-git: the point is to expose them to Gitaly")
 
-    git.HeptapodGitHandler(repo, repo.ui)  # instantiation is enough
+    handler = git.HeptapodGitHandler(repo, repo.ui)
+    handler.native_project_ensure_set_git_dir()
     return 0
 
 
@@ -393,7 +386,7 @@
             tarf.extractall(os.fsdecode(repo.path))
 
 
-@command(b'hpd-git-resync', [], )
+@command(b'hpd-git-resync|hpd-export-native-to-git', [], )
 def git_resync(ui, repo):
     """Silently resynchronize the Git repo.
 
@@ -408,16 +401,14 @@
     No permissions are involved: all the refs changes are already presented
     by the Mercurial repository.
 
-    The main use case is to restart the Git sysnchronization if it has been
-    interrupted (e.g to try running without it) and it turns out to be
-    necessary, either to push to external Git services (use-case of
-    heptapod#125) or to workaround bugs while the mode without Git is
-    experimental. Except for the direct references to SHAs in the database
-    (and the time to convert to Git), in theory, a project could go all the
-    way back from being native without Git to being hg-git based.
+    The main use case used to be to switch back a Project to hg-git after it
+    has been migrated to native.
+    Nowadays, it is to update converted Git repositories that are being used
+    for remote pushes.
     """
     handler = git.HeptapodGitHandler(repo, repo.ui)
     with repo.wlock(), repo.lock():
+        handler.native_project_ensure_set_git_dir()
         handler.export_git_objects()
         handler.save_map(handler.map_file)
         handler.force_git_refs_from_gitlab_files()
diff --git a/hgext3rd/heptapod/git.py b/hgext3rd/heptapod/git.py
--- a/hgext3rd/heptapod/git.py
+++ b/hgext3rd/heptapod/git.py
@@ -64,9 +64,6 @@
             main_repo = self.repo
 
         self.gitdir = re.sub(br'\.hg$', b'', main_repo.root) + b'.git'
-        if self.repo.ui.configbool(b'heptapod', b'native'):
-            self.native_project_ensure_set_git_dir()
-
         self.unfiltered_repo = self.repo.unfiltered()
         self._default_git_ref = None
 
diff --git a/hgext3rd/heptapod/state_maintainer.py b/hgext3rd/heptapod/state_maintainer.py
--- a/hgext3rd/heptapod/state_maintainer.py
+++ b/hgext3rd/heptapod/state_maintainer.py
@@ -815,7 +815,6 @@
         prune_default_branch = to_prune.pop(default_git_branch, None)
         if (
                 prune_default_branch
-                and ui.configbool(b'heptapod', b'native')
                 and default_git_branch.startswith(b'branch/')
         ):
             msg = (b"These changes close or prune the Heptapod default "
diff --git a/hgext3rd/heptapod/tests/git/test_inner.py b/hgext3rd/heptapod/tests/git/test_inner.py
--- a/hgext3rd/heptapod/tests/git/test_inner.py
+++ b/hgext3rd/heptapod/tests/git/test_inner.py
@@ -372,7 +372,7 @@
     patch_gitlab_hooks(monkeypatch, notifs)
 
     config = common_config(tmpdir)
-    config['heptapod']['native'] = True
+    config['heptapod']['native'] = False
     wrapper = RepoWrapper.init(tmpdir.join('repo'), config=config)
 
     wrapper.write_commit('foo')
diff --git a/hgext3rd/heptapod/tests/git/test_integration.py b/hgext3rd/heptapod/tests/git/test_integration.py
--- a/hgext3rd/heptapod/tests/git/test_integration.py
+++ b/hgext3rd/heptapod/tests/git/test_integration.py
@@ -169,6 +169,7 @@
                         content='foo2',
                         message="default2")
     wrapper.command('gitlab-mirror')
+    wrapper.command('hpd-export-native-to-git')
     assert fixture.git_repo.branch_titles() == {b'branch/default': b'default2'}
 
 
@@ -870,22 +871,24 @@
     set_prune_closed_branches(wrapper, True)
     wrapper.command('commit', message=b"closing default!", close_branch=True)
 
-    # On non-native repos, this is ignored
-    wrapper.repo.ui.setconfig(b'heptapod', b'native', False)
-    wrapper.command('gitlab-mirror')
+    # On non-native repos, this is a user error
+    wrapper.repo.ui.setconfig(b'heptapod', b'native', False)  # making sure
+    with pytest.raises(error.Abort) as exc_info:
+        wrapper.command('gitlab-mirror')
+
+    assert re.search(br'prune.*default branch', exc_info.value.args[0])
+
     assert git_repo.branch_titles() == {b'branch/default': b'default1'}
 
-    # On native repos, this is a user error
+    # On native repos, this is a user error as well, but still it will move
+    # the repo out of Gitaly reach (this sounds weird in this "git" test, but
+    # we need the Git repo to have been created and this test just did it.
     wrapper.repo.ui.setconfig(b'heptapod', b'native', True)
-    # But still it will move the repo out of Gitaly reach
     fixture.reload_git_repo()
     git_repo = fixture.git_repo
 
     fixture.clear_gitlab_notifs()
-    with pytest.raises(error.Abort) as exc_info:
-        wrapper.command('gitlab-mirror')
-
-    assert re.search(br'prune.*default branch', exc_info.value.args[0])
+    wrapper.command('hpd-export-native-to-git')
 
     assert not notifs
     assert git_repo.branch_titles() == {b'branch/default': b'default1'}
diff --git a/hgext3rd/heptapod/tests/no_git/test_integration.py b/hgext3rd/heptapod/tests/no_git/test_integration.py
--- a/hgext3rd/heptapod/tests/no_git/test_integration.py
+++ b/hgext3rd/heptapod/tests/no_git/test_integration.py
@@ -77,7 +77,6 @@
             tmpdir, monkeypatch,
             hg_config=common_config(tmpdir)) as fixture:
         fixture.hg_repo_wrapper.set_config('heptapod.native', True)
-        fixture.hg_repo_wrapper.set_config('heptapod.no-git', True)
         yield fixture
 
 
@@ -1197,8 +1196,8 @@
 # instead of about branches/refs being out of sync
 
 
-def test_heptapod_notify_gitlab(tmpdir, monkeypatch):
-    wrapper = RepoWrapper.init(tmpdir.join('repo'))
+def test_heptapod_notify_gitlab(empty_fixture, monkeypatch):
+    wrapper = empty_fixture.hg_repo_wrapper
     handler = NoGitStateMaintainer(wrapper.repo.ui, wrapper.repo)
 
     notifs = []
diff --git a/hgext3rd/heptapod/tests/test_commands_misc.py b/hgext3rd/heptapod/tests/test_commands_misc.py
--- a/hgext3rd/heptapod/tests/test_commands_misc.py
+++ b/hgext3rd/heptapod/tests/test_commands_misc.py
@@ -20,7 +20,9 @@
 )
 from heptapod.testhelpers.gitlab import (
     GitLabMirrorFixture,
+    GitLabStateMaintainerFixture,
 )
+from heptapod.testhelpers.git import GitRepo
 from .utils import common_config
 
 from .. import (
@@ -163,40 +165,48 @@
         yield fixture
 
 
-def test_git_resync(git_mirror_fixture):
+def test_git_resync_existing_git_repo(git_mirror_fixture):
     fixture = git_mirror_fixture
     wrapper = fixture.hg_repo_wrapper
     hg_repo = wrapper.repo
-    git_repo = fixture.git_repo
 
     base_ctx = wrapper.commit_file('foo', message='Commit 0')
 
     # Adding various repo content
     wrapper.command('tag', b'v1.2.3', rev=base_ctx.hex())
     wrapper.commit_file('foo', message='Commit 1')
-    # TODO special ref
-    # TODO keep around
+    # special refs and keep-arounds are added later in this test
     wrapper.command('gitlab-mirror')
+    git_repo = fixture.git_repo
 
-    git_repo.delete()
-    (wrapper.path / '.hg' / 'git-mapfile').remove()
+    wrapper.set_config('heptapod', 'native', 'yes')
 
+    def assert_initial_git_state():
+        assert git_repo.branch_titles() == {b'branch/default': b'Commit 1'}
+        assert git_repo.tags() == {b'v1.2.3'}
+        # checking ref targets, we don't care about the hashes
+        base_git_sha, base_git_title = git_repo.commit_hash_title('v1.2.3')
+        assert base_git_title == b'Commit 0'
+        return base_git_sha
+
+    base_git_sha = assert_initial_git_state()
+
+    # first call does not break anything, and moves Git repo to the dedicated
+    # location
     wrapper.command('hpd-git-resync')
-    assert git_repo.branch_titles() == {b'branch/default': b'Commit 1'}
-    assert git_repo.tags() == {b'v1.2.3'}
-    # checking ref targets, we don't care about the hashes
-    base_git_sha, base_git_title = git_repo.commit_hash_title('v1.2.3')
-    assert base_git_title == b'Commit 0'
+    fixture.reload_git_repo()
+    assert fixture.git_repo.path != git_repo.path
+    git_repo = fixture.git_repo
+    assert_initial_git_state()
 
     # now adding a new commit to check basic incrementality
     # (adding another branch just to spice it a bit)
     wrapper.commit_file('foo', message='Commit 2')
     other_ctx = wrapper.commit_file('bar', parent=base_ctx, branch='other',
                                     message='Commit 3')
-    wrapper.set_config('heptapod', 'no-git', 'on')
     wrapper.command('gitlab-mirror')
 
-    # Disabling of mirrorring to Git did work (validity of test hypothesis)
+    # Mirroring to Git did not happen (validity of test hypothesis)
     assert git_repo.branch_titles() == {b'branch/default': b'Commit 1'}
 
     wrapper.command('hpd-git-resync')
@@ -214,6 +224,40 @@
     assert git_repo.commit_hash_title(git_keep_around)[1] == b'Commit 0'
 
 
+@pytest.fixture()
+def native_fixture(tmpdir, monkeypatch):
+    with GitLabStateMaintainerFixture.init(
+            tmpdir, monkeypatch,
+            hg_config=common_config(tmpdir)) as fixture:
+        fixture.hg_repo_wrapper.set_config('heptapod.native', True)
+        yield fixture
+
+
+def test_git_resync_creates_git_repo(native_fixture):
+    fixture = native_fixture
+    wrapper = fixture.hg_repo_wrapper
+
+    base_ctx = wrapper.commit_file('foo', message='Commit 0')
+
+    # Adding various repo content
+    wrapper.command('tag', b'v1.2.3', rev=base_ctx.hex())
+    wrapper.commit_file('foo', message='Commit 1')
+    # TODO special ref
+    # TODO keep around
+    wrapper.command('gitlab-mirror')
+    wrapper.command('hpd-export-native-to-git')
+    git_repo = GitRepo(
+        fixture.base_path / '+hgitaly/hg-git'
+        / wrapper.path.basename.replace('.hg', '.git')
+    )
+    assert git_repo.branch_titles() == {b'branch/default': b'Commit 1'}
+    assert git_repo.tags() == {b'v1.2.3'}
+    # checking ref targets, we don't care about the hashes
+    base_git_sha, base_git_title = git_repo.commit_hash_title('v1.2.3')
+    assert base_git_title == b'Commit 0'
+    return base_git_sha
+
+
 def test_move_out_of_gitaly_reach(git_mirror_fixture):
     fixture = git_mirror_fixture
     wrapper = fixture.hg_repo_wrapper
@@ -232,7 +276,3 @@
     assert os.path.exists(git_repo.path)
     # would not work if it were not a Git repo
     assert git_repo.branches() == {}
-
-    wrapper.set_config('heptapod', 'no-git', True)
-    with pytest.raises(error.Abort):
-        wrapper.command('move-hg-git-repo-out-of-gitaly-reach')
diff --git a/hgext3rd/heptapod/tests/test_config.py b/hgext3rd/heptapod/tests/test_config.py
--- a/hgext3rd/heptapod/tests/test_config.py
+++ b/hgext3rd/heptapod/tests/test_config.py
@@ -75,27 +75,18 @@
                          pycompat.sysbytes(item)) == value
 
 
-@parametrize('env_value,no_git_value,expected,expected_no_git',
-             ((None, None, False, False),
-              (None, 'yes', False, False),
-              (None, 'no', False, False),
-              ('no', 'yes', False, False),
-              ('no', 'no', False, False),
-              ('yes', None, True, False),
-              ('yes', 'no', True, False),
-              ('yes', 'yes', True, True),
+@parametrize('env_value,expected',
+             ((None, False),
+              ('no', False),
+              ('yes', True),
               ))
-def test_native_from_env(env_value, no_git_value, expected, expected_no_git,
+def test_native_from_env(env_value, expected,
                          tmpdir, monkeypatch):
     env = dict(encoding.environ)
     env_key = b'HEPTAPOD_HG_NATIVE'
-    no_git_key = b'HEPTAPOD_NO_GIT'
     if env_value is not None:
         env_value = env_value.encode()
         env[env_key] = env_value
-    if no_git_value is not None:
-        no_git_value = no_git_value.encode()
-        env[no_git_key] = no_git_value
 
     monkeypatch.setattr(encoding, 'environ', env)
 
@@ -105,4 +96,3 @@
     assert ui.environ.get(env_key) == env_value
     # actual test assertions
     assert ui.configbool(b'heptapod', b'native') is expected
-    assert ui.configbool(b'heptapod', b'no-git') is expected_no_git