# HG changeset patch
# User Georges Racinet <georges.racinet@cloudcrane.io>
# Date 1736679537 -3600
#      Sun Jan 12 11:58:57 2025 +0100
# Node ID a1831e878bd49ca41ca0d23bb1f737cbf11fffcd
# Parent  c410f41dbf1d4b7929eb11e3f0a2a9d8731bbd45
Git testhelpers: comparing repos

diff --git a/heptapod/testhelpers/git.py b/heptapod/testhelpers/git.py
--- a/heptapod/testhelpers/git.py
+++ b/heptapod/testhelpers/git.py
@@ -14,6 +14,7 @@
 typically solved by installing Git system-wide.
 """
 import os
+import re
 import shutil
 import subprocess
 from mercurial import pycompat
@@ -35,6 +36,15 @@
     return {ref: info['title'] for ref, info in branches.items()}
 
 
+def git_version():
+    out = subprocess.check_output(('git', 'version')).decode().strip()
+    m = re.match(r'git version (\d+)\.(\d+)\.(\d+)', out)
+    return tuple(int(m.group(i)) for i in range(1, 4))
+
+
+GIT_VERSION = git_version()
+
+
 class GitRepo(object):
     """Represent a Git repository, relying on the Git executable.
 
@@ -77,7 +87,9 @@
         return out.strip().split(b'|')
 
     def get_symref(self, name):
-        return self.path.join(name).read().strip().split(':', 1)[1].strip()
+        dotgit = self.path.join('.git')
+        path = dotgit if dotgit.exists() else self.path
+        return path.join(name).read().strip().split(':', 1)[1].strip()
 
     def set_symref(self, name, target_ref):
         self.path.join(name).write('ref: %s\n' % target_ref)
@@ -110,3 +122,43 @@
         lines = self.git('show-ref').splitlines()
         return {split[1]: split[0]
                 for split in (line.split(b' ', 1) for line in lines)}
+
+    def raw_refs(self, with_root_refs=False):
+        """Use `git for-each-ref` to query all refs, including "root refs"
+
+        The output is the full `stdout` of the Git process, as bytes.
+        Each line has 2 or 3 fields:
+
+        - the target object SHA
+        - the name of the ref (e.g, `refs/heads/main` or `HEAD`)
+        - its target ref *if* it is a root ref (e.g, `HEAD`).
+
+        The root refs are a subset of symrefs. It includes `HEAD` but not
+        arbitrary symrefs that can be created with `git symbolic-ref`.
+
+        Git sorts the output on `refname` (e.g., `refs/heads/main` or `HEAD`)
+        by default, which is fine to compare two repositories.
+        """
+        git_args = ['for-each-ref',
+                    '--format', '%(objectname)s %(refname) %(symref)']
+        if with_root_refs:
+            git_args.append('--include-root-refs')  # pragma no cover
+
+        return self.git(*git_args)
+
+    def __eq__(self, other):
+        """True iff the two repos have the same content.
+
+        THis is done by comparing all refs, including the remotes
+        """
+        # not insisting on the same exact class
+        if not isinstance(other, GitRepo):
+            return False
+
+        opts = dict(with_root_refs=True)
+        if GIT_VERSION < (2, 45, 0):  # pragma no cover
+            opts['with_root_refs'] = False
+            if self.get_symref('HEAD') != other.get_symref('HEAD'):
+                return False
+
+        return self.raw_refs(**opts) == other.raw_refs(**opts)
diff --git a/heptapod/testhelpers/tests/test_git.py b/heptapod/testhelpers/tests/test_git.py
--- a/heptapod/testhelpers/tests/test_git.py
+++ b/heptapod/testhelpers/tests/test_git.py
@@ -8,7 +8,7 @@
 from ..git import GitRepo
 
 
-def test_branches(tmpdir):
+def test_refs(tmpdir):
     server = tmpdir / 'server.git'
     # server_repo is bare, hence we cannot commit directly in there
     server_repo = GitRepo.init(server)
@@ -41,3 +41,18 @@
     assert server_repo.commit_hash_title('master') == [sha.encode(),
                                                        b'init commit']
     assert server_repo.get_branch_sha(b'master') == sha
+
+    client_repo = GitRepo(client)
+    assert server_repo != client  # wrong class
+    assert server_repo != client_repo  # the client has remote refs
+    client_repo.git('update-ref', '-d', 'refs/remotes/origin/master')
+    assert server_repo == client_repo
+
+    # (for Git < 2.45 coverage): same HEAD but an additional branch
+    (client / 'foo').write('bar')
+    subprocess.call(('git', 'checkout', '-b', 'other'), cwd=str(client))
+    subprocess.call(('git', 'add', 'foo'), cwd=str(client))
+
+    subprocess.call(('git', 'commit', '-m', 'other branch', '--no-gpg-sign'),
+                    cwd=str(client))
+    assert server_repo != client_repo
# HG changeset patch
# User Georges Racinet <georges.racinet@cloudcrane.io>
# Date 1736685358 -3600
#      Sun Jan 12 13:35:58 2025 +0100
# Node ID a5fd743bb61503bb8e380a38448fe58c98a21b36
# Parent  a1831e878bd49ca41ca0d23bb1f737cbf11fffcd
test helpers: separate functions to activate state maintaining

Two helpers for in-RAM activation on the Mercurial `localrepo` object
and in config dicts of our testhelpers for persistence in `.hg/hgrc`.

This makes it easier to use in HGitaly test fixtures, reducing hardcoding
and duplication.

diff --git a/heptapod/testhelpers/gitlab.py b/heptapod/testhelpers/gitlab.py
--- a/heptapod/testhelpers/gitlab.py
+++ b/heptapod/testhelpers/gitlab.py
@@ -24,6 +24,11 @@
 from .git import GitRepo
 logger = logging.getLogger(__name__)
 
+STATE_MAINTAINER_ACTIVATION_CONFIG = (
+    b'hooks', b'pretxnclose.testcase',
+    b'python:heptapod.hooks.gitlab_mirror.mirror',
+)
+
 
 def patch_gitlab_hooks(monkeypatch, records, action=None):
 
@@ -43,6 +48,26 @@
     monkeypatch.setattr(hooks.PostReceive, '__call__', call)
 
 
+def activate_gitlab_state_maintainer(hg_repo_wrapper):
+    """Make state maintaining or mirroring to Git repo automatic.
+
+    This is essential to get the state maintaining happen in-transaction.
+    If the repository is configured with `native=False` (legacy hg-git based
+    projects), this triggers the mirroring to Git.
+    """
+    hg_repo_wrapper.repo.ui.setconfig(*STATE_MAINTAINER_ACTIVATION_CONFIG)
+
+
+def activate_gitlab_state_maintainer_in_dict(config):  # pragma no cover
+    """Activation in config dicts used before persisting configuration.
+
+    This is used in HGitaly tests, as py-heptapod tests tend to do it
+    after repo creation, in non-persistent form.
+    """
+    section, key, value = STATE_MAINTAINER_ACTIVATION_CONFIG
+    config[section] = {key: value}
+
+
 @attr.s
 class GitLabStateMaintainerFixture:
     """Helper class to create fixtures for GitLab state maintainers.
@@ -97,13 +122,7 @@
         del self.gitlab_notifs[:]
 
     def activate_mirror(self):
-        """Make mirroring from Mercurial to Git repo automatic.
-
-        This is essential to get the mirroring code to run in-transaction.
-        """
-        self.hg_repo_wrapper.repo.ui.setconfig(
-            b'hooks', b'pretxnclose.testcase',
-            b'python:heptapod.hooks.gitlab_mirror.mirror')
+        activate_gitlab_state_maintainer(self.hg_repo_wrapper)
 
     def delete(self):
         hg_path = self.hg_repo_wrapper.repo.root