Skip to content

codingame_tools.contribution_manager.git_repo

git_repo

Low-level git plumbing wrapper backing the git-based contribution repo (main/server/ version-data branches, in a decoupled --git-dir/--work-tree layout--see codingame_tools.contribution_manager.manager for how these are used, and layout.py for the branch/tag naming constants).

The only module in this package that shells out to git (subprocess, not GitPython--matches the plain subprocess.run pattern already used elsewhere in this codebase for external tools, e.g. the old tree_diff.compute_diff3_merge/merge_tools.launch_merge_tool, both superseded by this). Every invocation passes --git-dir/--work-tree explicitly (and sets cwd to the work tree too, for git versions/commands sensitive to it)--cwd-based discovery is never relied on, since the whole point of this layout is that data/ itself carries no .git marker a human's plain git command could ever find (see cg contribution git, in the CLI, for how a human reaches this repo directly).

FALLBACK_IDENTITY_EMAIL module-attribute

FALLBACK_IDENTITY_EMAIL = 'codingame-tools@localhost'

Author/committer identity used only when git can't resolve one of its own.

.meta/'s repository is local, gitignored scaffolding whose commits are never pushed anywhere, so the identity on them carries no meaning--but git commit still refuses to run without one. Git normally auto-detects user@host when nothing is configured, which is why this rarely bites interactively; it fails on a machine whose hostname has no domain (fv-az123.(none) on a GitHub Actions runner) or wherever user.useConfigOnly is set. Without this fallback, cg contribution simply doesn't work for anyone who has never run git config --global user.email.

CgGitError

CgGitError(argv, returncode, stderr)

Bases: Exception

Raised when a git invocation fails (non-zero exit)--wraps the exact argv and stderr for introspection, structured enough to be useful without needing a library for it.

Source code in codingame_tools/contribution_manager/git_repo.py
35
36
37
38
39
def __init__(self, argv: list[str], returncode: int, stderr: str) -> None:
    self.argv = argv
    self.returncode = returncode
    self.stderr = stderr
    super().__init__(f"{' '.join(argv)!r} failed (exit {returncode}): {stderr.strip()}")

CgGitRepo

CgGitRepo(git_dir, work_tree)

One git repository, addressed by its (possibly work-tree-external) git_dir/work_tree pair. work_tree is always the contribution's data/ directory in practice--the checkout of the main branch. server/version-data are never checked out; every method that writes to them does so via plumbing (a scratch index, or a single-blob tree for version-data), never touching HEAD, the real index, or anything under work_tree.

Source code in codingame_tools/contribution_manager/git_repo.py
100
101
102
103
def __init__(self, git_dir: Path, work_tree: Path) -> None:
    self.git_dir = git_dir
    self.work_tree = work_tree
    self._identity_args: list[str] | None = None

set_head

set_head(branch)

Point HEAD at refs/heads/<branch>--used once, right after init_repo(), so a fresh repo's "currently checked out branch" is unambiguously MAIN_BRANCH_NAME regardless of git's own init.defaultBranch configuration.

Source code in codingame_tools/contribution_manager/git_repo.py
165
166
167
168
169
def set_head(self, branch: str) -> None:
    """Point `HEAD` at `refs/heads/<branch>`--used once, right after `init_repo()`, so a fresh
       repo's "currently checked out branch" is unambiguously `MAIN_BRANCH_NAME` regardless of
       git's own `init.defaultBranch` configuration."""
    self._run(["symbolic-ref", "HEAD", f"refs/heads/{branch}"])

rev_parse

rev_parse(*args, check=True)

git rev-parse <args>, stripped. Returns None (rather than raising) if check=False and the command fails--used for existence checks like rev_parse("--verify", "MERGE_HEAD", check=False).

Source code in codingame_tools/contribution_manager/git_repo.py
171
172
173
174
175
176
177
178
def rev_parse(self, *args: str, check: bool = True) -> str | None:
    """`git rev-parse <args>`, stripped. Returns None (rather than raising) if `check=False`
       and the command fails--used for existence checks like `rev_parse("--verify",
       "MERGE_HEAD", check=False)`."""
    result = self._run(["rev-parse", *args], check=check)
    if result.returncode != 0:
        return None
    return result.stdout.decode("utf-8").strip()

resolve_ref

resolve_ref(ref)

The commit SHA ref (a branch/tag name) currently points at, or None if it doesn't exist.

Source code in codingame_tools/contribution_manager/git_repo.py
180
181
182
183
def resolve_ref(self, ref: str) -> str | None:
    """The commit SHA `ref` (a branch/tag name) currently points at, or None if it doesn't
       exist."""
    return self.rev_parse(ref, check=False)

delete_ref

delete_ref(ref)

Delete ref (e.g. refs/heads/server) if it exists--a no-op if it doesn't. The underlying commit objects aren't pruned (no gc/prune is ever run by this class), so anything only reachable through the deleted ref remains locally inspectable by SHA (or via tags--delete_ref never touches those) until a real git gc eventually collects it.

Source code in codingame_tools/contribution_manager/git_repo.py
188
189
190
191
192
193
194
195
196
def delete_ref(self, ref: str) -> None:
    """Delete `ref` (e.g. `refs/heads/server`) if it exists--a no-op if it doesn't. The
       underlying commit objects aren't pruned (no `gc`/`prune` is ever run by this class), so
       anything only reachable through the deleted ref remains locally inspectable by SHA
       (or via tags--`delete_ref` never touches those) until a real `git gc` eventually
       collects it."""
    if self.resolve_ref(ref) is None:
        return
    self._run(["update-ref", "-d", ref])

reset_index_to

reset_index_to(sha)

Move the branch HEAD currently points at (always main, by construction) to sha, and reset the real index to match its tree--leaving the working tree untouched (git reset <sha>, a "mixed" reset). Used after directly building a commit whose tree already matches what's on disk (import_()/push(), both of which build their tree via a scratch index--see write_tree_from_dir--that never touches the real index at all). Without this, the real index is left stale (empty, for a fresh repo) relative to main's new tip; that doesn't matter for anything routed through checkout_all() (read-tree --reset -u resyncs it), but a later real git merge reads the index directly and misbehaves against a stale one--confirmed by direct testing.

Source code in codingame_tools/contribution_manager/git_repo.py
198
199
200
201
202
203
204
205
206
207
208
def reset_index_to(self, sha: str) -> None:
    """Move the branch `HEAD` currently points at (always `main`, by construction) to `sha`,
       and reset the real index to match its tree--leaving the working tree untouched (`git
       reset <sha>`, a "mixed" reset). Used after directly building a commit whose tree
       already matches what's on disk (`import_()`/`push()`, both of which build their tree
       via a scratch index--see `write_tree_from_dir`--that never touches the real index at
       all). Without this, the real index is left stale (empty, for a fresh repo) relative to
       `main`'s new tip; that doesn't matter for anything routed through `checkout_all()`
       (`read-tree --reset -u` resyncs it), but a later real `git merge` reads the index
       directly and misbehaves against a stale one--confirmed by direct testing."""
    self._run(["reset", sha])

tag

tag(name, target)

Create (or overwrite) a lightweight tag--used for server.<version>/ version-data.<version>, both purely informational/addressing conveniences, not something that needs annotation.

Source code in codingame_tools/contribution_manager/git_repo.py
210
211
212
213
214
def tag(self, name: str, target: str) -> None:
    """Create (or overwrite) a lightweight tag--used for `server.<version>`/
       `version-data.<version>`, both purely informational/addressing conveniences, not
       something that needs annotation."""
    self._run(["tag", "-f", name, target])

merge_base

merge_base(a, b)

The best common ancestor of a and b, or None if they share no history (shouldn't happen for main/server, which always share import_()'s initial commit).

Source code in codingame_tools/contribution_manager/git_repo.py
216
217
218
219
220
221
222
def merge_base(self, a: str, b: str) -> str | None:
    """The best common ancestor of `a` and `b`, or None if they share no history (shouldn't
       happen for `main`/`server`, which always share `import_()`'s initial commit)."""
    result = self._run(["merge-base", a, b], check=False)
    if result.returncode != 0:
        return None
    return result.stdout.decode("utf-8").strip()

write_tree_from_dir

write_tree_from_dir(source_dir)

Snapshot whatever's currently on disk at source_dir into a tree object, via a scratch index--never touches the real index, HEAD, or (unless source_dir is self.work_tree) the real working tree. Used both to build server's tree from main's live content after a successful push (source_dir=self.work_tree, i.e. write_tree_from_worktree()) and to build it from freshly-fetched content that was never materialized into the real working tree at all (source_dir is a throwaway temp dir--see fetch()).

Source code in codingame_tools/contribution_manager/git_repo.py
230
231
232
233
234
235
236
237
238
239
240
241
def write_tree_from_dir(self, source_dir: Path) -> str:
    """Snapshot whatever's currently on disk at `source_dir` into a tree object, via a scratch
       index--never touches the real index, `HEAD`, or (unless `source_dir is self.work_tree`)
       the real working tree. Used both to build `server`'s tree from `main`'s live content
       after a successful push (`source_dir=self.work_tree`, i.e. `write_tree_from_worktree()`)
       and to build it from freshly-*fetched* content that was never materialized into the
       real working tree at all (`source_dir` is a throwaway temp dir--see `fetch()`)."""
    with tempfile.TemporaryDirectory() as tmp:
        index_file = Path(tmp) / "index"
        self._run(["add", "-A"], index_file=index_file, work_tree=source_dir)
        result = self._run(["write-tree"], index_file=index_file, work_tree=source_dir)
        return result.stdout.decode("utf-8").strip()

write_tree_from_worktree

write_tree_from_worktree()

write_tree_from_dir(self.work_tree)--see there.

Source code in codingame_tools/contribution_manager/git_repo.py
243
244
245
def write_tree_from_worktree(self) -> str:
    """`write_tree_from_dir(self.work_tree)`--see there."""
    return self.write_tree_from_dir(self.work_tree)

write_tree_single_file

write_tree_single_file(filename, blob_sha)

Build a tree containing exactly one file (filename -> blob_sha)--used for version-data's commits, which are never more than contribution-version-data.json.

Source code in codingame_tools/contribution_manager/git_repo.py
247
248
249
250
251
def write_tree_single_file(self, filename: str, blob_sha: str) -> str:
    """Build a tree containing exactly one file (`filename` -> `blob_sha`)--used for
       `version-data`'s commits, which are never more than `contribution-version-data.json`."""
    result = self._run(["mktree"], input=f"100644 blob {blob_sha}\t{filename}\n".encode())
    return result.stdout.decode("utf-8").strip()

read_file_at

read_file_at(ref, path)

The content of path as it exists in ref's tree (e.g. read_file_at("server", "cover.png")), or None if path doesn't exist there. Used to reuse a previously-fetched cover image's bytes straight from the object database, without needing a live cached file anywhere on disk (see fetch()'s cover-reuse logic).

Source code in codingame_tools/contribution_manager/git_repo.py
266
267
268
269
270
271
272
273
274
def read_file_at(self, ref: str, path: str) -> bytes | None:
    """The content of `path` as it exists in `ref`'s tree (e.g. `read_file_at("server",
       "cover.png")`), or None if `path` doesn't exist there. Used to reuse a previously-fetched
       cover image's bytes straight from the object database, without needing a live cached
       file anywhere on disk (see `fetch()`'s cover-reuse logic)."""
    result = self._run(["cat-file", "-e", f"{ref}:{path}"], check=False)
    if result.returncode != 0:
        return None
    return self._run(["show", f"{ref}:{path}"]).stdout

read_trailers

read_trailers(commit)

The git trailers (Key: Value lines) on commit's message--via git interpret- trailers --parse, robust to trailer-format edge cases (folding, repeated keys, etc.) we don't want to hand-parse ourselves.

Source code in codingame_tools/contribution_manager/git_repo.py
276
277
278
279
280
281
282
283
284
285
286
287
def read_trailers(self, commit: str) -> dict[str, str]:
    """The git trailers (`Key: Value` lines) on `commit`'s message--via `git interpret-
       trailers --parse`, robust to trailer-format edge cases (folding, repeated keys, etc.)
       we don't want to hand-parse ourselves."""
    message = self._run(["log", "-1", "--format=%B", commit]).stdout
    parsed = self._run(["interpret-trailers", "--parse"], input=message).stdout.decode("utf-8")
    trailers: dict[str, str] = {}
    for line in parsed.splitlines():
        if ":" in line:
            key, _, value = line.partition(":")
            trailers[key.strip()] = value.strip()
    return trailers

commit_worktree

commit_worktree(message)

add -A && commit -m <message> against the real working tree--the only method here that touches the real index/HEAD, always on whatever branch is currently checked out (always main, by construction--server/version-data are never checked out).

Source code in codingame_tools/contribution_manager/git_repo.py
291
292
293
294
295
296
297
298
def commit_worktree(self, message: str) -> str:
    """`add -A && commit -m <message>` against the real working tree--the only method here
       that touches the real index/`HEAD`, always on whatever branch is currently checked out
       (always `main`, by construction--`server`/`version-data` are never checked out)."""
    self._run(["add", "-A"])
    self._run(["commit", "-m", message, "--allow-empty"])
    result = self._run(["rev-parse", "HEAD"])
    return result.stdout.decode("utf-8").strip()

restage_and_amend_if_dirty

restage_and_amend_if_dirty()

If the real working tree has any uncommitted changes relative to HEAD (staged or not), stage all of them and fold them into HEAD's existing commit (add -A && commit --amend --no-edit)--parents (so a merge commit's two parents survive intact) and message are both left untouched, only the tree changes. Used right after a merge commit (auto-committed by a clean git merge, or just made by merge_continue()) to fold in a content-preserving renormalize_test_case_dirs() cleanup, so it doesn't need its own separate commit. A no-op (returns False) if nothing changed.

Returns:

  • bool

    Whether anything was actually amended.

Source code in codingame_tools/contribution_manager/git_repo.py
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
def restage_and_amend_if_dirty(self) -> bool:
    """If the real working tree has any uncommitted changes relative to `HEAD` (staged or
       not), stage all of them and fold them into `HEAD`'s existing commit (`add -A && commit
       --amend --no-edit`)--parents (so a merge commit's two parents survive intact) and
       message are both left untouched, only the tree changes. Used right after a merge
       commit (auto-committed by a clean `git merge`, or just made by `merge_continue()`) to
       fold in a content-preserving `renormalize_test_case_dirs()` cleanup, so it doesn't need
       its own separate commit. A no-op (returns False) if nothing changed.

    Returns:
        Whether anything was actually amended.
    """
    status = self._run(["status", "--porcelain"]).stdout
    if not status.strip():
        return False
    self._run(["add", "-A"])
    self._run(["commit", "--amend", "--no-edit"])
    return True

checkout_all

checkout_all(ref)

Reset the index, the working tree, and remove untracked files/directories, so the working tree ends up matching ref's content exactly--without moving HEAD or creating a commit. Used by discard_local()/rebase()'s fast-forward/ merge_discard_local().

Two things confirmed necessary by direct testing, not just one: read-tree --reset -u (rather than checkout <ref> -- ., which only adds/updates paths present in ref, and never removes a tracked path that's absent there) handles files that are tracked but shouldn't be anymore--but it leaves untracked files (never git add-ed at all) alone entirely, since that's simply outside its scope. clean -fd handles that remaining case. Together, nothing extra survives, tracked or not.

Source code in codingame_tools/contribution_manager/git_repo.py
319
320
321
322
323
324
325
326
327
328
329
330
331
332
def checkout_all(self, ref: str) -> None:
    """Reset the index, the working tree, *and* remove untracked files/directories, so the
       working tree ends up matching `ref`'s content *exactly*--without moving `HEAD` or
       creating a commit. Used by `discard_local()`/`rebase()`'s fast-forward/
       `merge_discard_local()`.

       Two things confirmed necessary by direct testing, not just one: `read-tree --reset -u`
       (rather than `checkout <ref> -- .`, which only adds/updates paths present in `ref`, and
       never removes a *tracked* path that's absent there) handles files that are tracked but
       shouldn't be anymore--but it leaves untracked files (never `git add`-ed at all) alone
       entirely, since that's simply outside its scope. `clean -fd` handles that remaining
       case. Together, nothing extra survives, tracked or not."""
    self._run(["read-tree", "--reset", "-u", ref])
    self._run(["clean", "-fd"])

merge_branch

merge_branch(branch)

git merge <branch> against the real working tree.

Returns:

  • bool

    True if the merge completed cleanly (a new merge commit was made, or it was already

  • bool

    up to date/fast-forwarded); False if it stopped with conflicts (MERGE_HEAD now

  • bool

    exists--resolve and call merge_continue(), or merge_abort()).

Source code in codingame_tools/contribution_manager/git_repo.py
334
335
336
337
338
339
340
341
342
343
def merge_branch(self, branch: str) -> bool:
    """`git merge <branch>` against the real working tree.

    Returns:
        True if the merge completed cleanly (a new merge commit was made, or it was already
        up to date/fast-forwarded); False if it stopped with conflicts (`MERGE_HEAD` now
        exists--resolve and call `merge_continue()`, or `merge_abort()`).
    """
    result = self._run(["merge", "--no-edit", branch], check=False)
    return result.returncode == 0

merge_continue

merge_continue()
stage everything currently on disk (add -A--so hand-

editing a conflicted file and running this directly, without git mergetool/manually git add-ing it first, works the same way it always did in the old marker-scan-based design) and commit.

Whether a path is conflicted at all is still git's own authoritative index-stage tracking (status_conflicts()), not content-scanning--but add -A blindly stages whatever's on disk regardless of content, so a leftover <<<<<<< marker in a path git does consider conflicted would otherwise get silently committed as real content if the user forgot to actually resolve it (only relevant for hand-editing; git mergetool itself already only stages a path once its own diff view reports no conflict left). So: check current content of just the still-unmerged paths for a leftover marker first, before staging anything--narrower and cheaper than the old design's whole-tree content scan, and it can't false-positive on a path that was never conflicted.

Raises:

  • CgGitError

    if unresolved conflict markers remain in a still-unmerged path, or (from git itself) if commit refuses for any other reason.

Source code in codingame_tools/contribution_manager/git_repo.py
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
def merge_continue(self) -> None:
    """Finish an in-progress merge: stage everything currently on disk (`add -A`--so hand-
       editing a conflicted file and running this directly, without `git mergetool`/manually
       `git add`-ing it first, works the same way it always did in the old marker-scan-based
       design) and commit.

       Whether a path is conflicted at all is still git's own authoritative index-stage
       tracking (`status_conflicts()`), not content-scanning--but `add -A` blindly stages
       whatever's on disk regardless of content, so a leftover `<<<<<<<` marker in a path git
       *does* consider conflicted would otherwise get silently committed as real content if
       the user forgot to actually resolve it (only relevant for hand-editing; `git mergetool`
       itself already only stages a path once its own diff view reports no conflict left).
       So: check current content of just the *still-unmerged* paths for a leftover marker
       first, before staging anything--narrower and cheaper than the old design's whole-tree
       content scan, and it can't false-positive on a path that was never conflicted.

    Raises:
        CgGitError: if unresolved conflict markers remain in a still-unmerged path, or (from
                    git itself) if `commit` refuses for any other reason.
    """
    conflicted = self.status_conflicts()
    for rel_path in conflicted:
        content = (self.work_tree / rel_path).read_bytes()
        if b"<<<<<<<" in content:
            raise CgGitError(
                    ["<merge_continue>"], 1,
                    f"{rel_path} still has an unresolved conflict marker--resolve it, then run "
                    "`cg contribution merge continue` again.",
                )
    self._run(["add", "-A"])
    self._run(["commit", "--no-edit"])

status_conflicts

status_conflicts()

Paths with unresolved merge conflicts (unmerged index stages)--authoritative, unlike the old content-based <<<<<<< marker scan it replaces.

Source code in codingame_tools/contribution_manager/git_repo.py
383
384
385
386
387
def status_conflicts(self) -> list[str]:
    """Paths with unresolved merge conflicts (unmerged index stages)--authoritative, unlike
       the old content-based `<<<<<<<` marker scan it replaces."""
    result = self._run(["diff", "--name-only", "--diff-filter=U"])
    return [line for line in result.stdout.decode("utf-8").splitlines() if line]

diff_text

diff_text(*refs)

git diff <refs...>--one ref diffs it against the working tree, two diffs between them.

Source code in codingame_tools/contribution_manager/git_repo.py
389
390
391
392
def diff_text(self, *refs: str) -> str:
    """`git diff <refs...>`--one ref diffs it against the working tree, two diffs between
       them."""
    return self._run(["diff", *refs]).stdout.decode("utf-8", errors="replace")

is_inside_existing_repo

is_inside_existing_repo(path)

Whether path is already inside some other git repository's working tree--checked purely via plain cwd-based discovery (deliberately: this is the one place that's appropriate, since we're asking "if I ran plain git here, would it find something"). Used once, at import_() time, to decide where this contribution's own (unrelated) git-dir should live-- see the module docstring and codingame_tools.contribution_manager.layout.

path itself usually doesn't exist yet (a brand-new cg contribution import target)--the check walks up to the nearest existing ancestor first, since cwd has to be a real directory.

Source code in codingame_tools/contribution_manager/git_repo.py
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
def is_inside_existing_repo(path: Path) -> bool:
    """Whether `path` is already inside some other git repository's working tree--checked purely
       via plain cwd-based discovery (deliberately: this is the one place that's appropriate,
       since we're asking "if I ran plain `git` here, would it find something"). Used once, at
       `import_()` time, to decide where this contribution's own (unrelated) git-dir should live--
       see the module docstring and `codingame_tools.contribution_manager.layout`.

       `path` itself usually doesn't exist yet (a brand-new `cg contribution import` target)--the
       check walks up to the nearest existing ancestor first, since `cwd` has to be a real
       directory."""
    existing = path
    while not existing.is_dir():
        parent = existing.parent
        if parent == existing:
            return False  # walked all the way to the filesystem root without finding anything real
        existing = parent
    result = subprocess.run(
            ["git", "rev-parse", "--is-inside-work-tree"],
            cwd=existing, capture_output=True, text=True, check=False,
        )
    return result.returncode == 0 and result.stdout.strip() == "true"

init_repo

init_repo(git_dir, work_tree)

Initialize a new, non-bare git repository whose metadata lives at git_dir and whose working tree is work_tree--the two need not be related in any way on disk (no .git entry is ever created inside work_tree).

Source code in codingame_tools/contribution_manager/git_repo.py
65
66
67
68
69
70
71
72
73
74
def init_repo(git_dir: Path, work_tree: Path) -> None:
    """Initialize a new, non-bare git repository whose metadata lives at `git_dir` and whose
       working tree is `work_tree`--the two need not be related in any way on disk (no `.git`
       entry is ever created inside `work_tree`)."""
    git_dir.mkdir(parents=True, exist_ok=True)
    work_tree.mkdir(parents=True, exist_ok=True)
    argv = ["git", f"--git-dir={git_dir}", f"--work-tree={work_tree}", "init", "--quiet"]
    result = subprocess.run(argv, cwd=work_tree, capture_output=True, text=True, check=False)
    if result.returncode != 0:
        raise CgGitError(argv, result.returncode, result.stderr)