Skip to content

codingame_tools.contribution_manager

contribution_manager

Local working-directory management for CodinGame contributions (puzzles)--a real git working directory (data/), backed by a remote server rather than a git remote.

See CgContributionManager for import_/repair/create/push/rebase/fetch/ merge_start/merge_continue/merge_abort/merge_discard_local/merge_discard_server/ discard_local/delete/status/read_status_cache--its module docstring covers the main/ server/version-data branch design in full, push()'s covers the create-vs-update duality hidden behind that one method, and repair()'s covers reconstructing a missing/corrupted git-dir; codingame_tools.contribution_manager.schema for the working directory's own manifest files (CgContributionIdentity/CgContributionView/CgContributionStatusCache--the last one an offline, non-git-tracked cache of server metadata that isn't tied to any content version, e.g. votes/comments/the moderator approve-reject gate); codingame_tools. contribution_manager.contribution_commit_data for CgContributionCommitMetadata (the git-trailer-backed remote commit metadata) and redact_commit_contribution; codingame_tools. contribution_manager.git_repo for the low-level git plumbing wrapper; and codingame_tools. contribution_manager.resolver for how a contribution directory is located.

CONTRIBUTION_COMMIT_DATA_FILE_NAME module-attribute

CONTRIBUTION_COMMIT_DATA_FILE_NAME = 'contribution-version-data.json'

Name of the single file committed onto the version-data branch (see codingame_tools.contribution_manager.layout.VERSION_DATA_BRANCH_NAME) at each server version.

CONTRIBUTION_META_FILE_NAME module-attribute

CONTRIBUTION_META_FILE_NAME = 'contribution-meta.json'

Name of the .meta/ file recording how this working directory is put together--currently just where its git-dir is. See schema.CgContributionMeta.

CONTRIBUTION_STATUS_CACHE_FILE_NAME module-attribute

CONTRIBUTION_STATUS_CACHE_FILE_NAME = 'contribution-status.json'

Name of the offline cache of non-version-tied server metadata (score/votes/comment count/ views/moderator approve-reject tallies/etc.), under META_SUBDIR_NAME--see schema.CgContributionStatusCache. Deliberately NOT git-tracked (unlike contribution-data. json, which lives in data/)--this is a disposable, opportunistically-refreshed cache, not diffable/mergeable content, and none of it is tied to any particular content version.

DATA_SUBDIR_NAME module-attribute

DATA_SUBDIR_NAME = 'data'

The actual contribution content (sidecar files, solution.src, cover.png, tests/, contribution-data.json) lives under a data/ subdirectory of the working directory root-- this is also the git working tree for the main branch (see git_repo/manager). The working directory root itself holds only contribution.json (identity), the solution.<ext> convenience symlink, and .meta/ (bookkeeping).

GIT_METADATA_SUBDIR_NAME module-attribute

GIT_METADATA_SUBDIR_NAME = '.contribution-git'

Name of the git-dir directory (objects/refs/HEAD/index/config) under META_SUBDIR_NAME, used in the external layout--i.e. <contribution_dir>/.meta/.contribution-git/, with data/ as its work tree via --git-dir/--work-tree decoupling.

Deliberately not named .git: in this layout the working directory sits inside some outer git project, and a .git marker anywhere under <contribution_dir> would trip that project's own embedded-repository detection. See DATA_GIT_DIR_NAME for the other layout, and manager's module docstring for how one is chosen.

GITIGNORE_FILE_NAME module-attribute

GITIGNORE_FILE_NAME = '.gitignore'

Written (containing just .meta/) at creation time in <contribution_dir>, which is where META_SUBDIR_NAME always lives, so .meta/'s contents (this client's generated state) can never end up tracked by whatever outer project comes to track the working directory, now or later.

Written unconditionally, in both git-dir layouts. In the external layout there is an outer project tracking this directory today; in the embedded one there is not, but there may well be later, and a .gitignore costs nothing until then.

MAIN_BRANCH_NAME module-attribute

MAIN_BRANCH_NAME = 'main'

The user's own working line--see manager's module docstring.

META_SUBDIR_NAME module-attribute

META_SUBDIR_NAME = '.meta'

Container for this client's own generated bookkeeping--the status cache, the selected test, the solution snapshot, the generated editor/devcontainer files, and (in one of the two layouts) the git-dir itself.

Always <contribution_dir>/.meta, a sibling of data/--never inside it, in either layout. data/ holds user state and nothing else: it is the git working tree, it is what gets pushed to CodinGame, and it is the only part worth backing up. Generated, disposable, rebuildable-by- repair() state has no business in there. Paired with a .gitignore (see GITIGNORE_FILE_NAME) in <contribution_dir>, so it's never picked up by whatever outer project comes to track the working directory.

SERVER_BRANCH_NAME module-attribute

SERVER_BRANCH_NAME = 'server'

Mirrors known server state--see manager's module docstring. Its tip is always "the current remote"; git merge-base main server is always "the last synced point".

SERVER_TAG_PREFIX module-attribute

SERVER_TAG_PREFIX = 'server.'

server.<version> tags a SERVER_BRANCH_NAME commit by the server version it represents.

SOLUTION_FILE_STEM module-attribute

SOLUTION_FILE_STEM = 'solution'

Stem of the one real, editable/submittable solution file, which lives in data/.

TRAILER_COVER_BINARY_HASH module-attribute

TRAILER_COVER_BINARY_HASH = 'Cg-Cover-Binary-Hash'

Git trailer keys on every SERVER_BRANCH_NAME commit--see contribution_commit_data.CgContributionCommitMetadata, which is the single canonical shape these are built from/parsed back into.

VERSION_DATA_BRANCH_NAME module-attribute

VERSION_DATA_BRANCH_NAME = 'version-data'

Orphan branch, one commit per server version, holding only contribution-version-data.json-- see contribution_commit_data.

VERSION_DATA_TAG_PREFIX module-attribute

VERSION_DATA_TAG_PREFIX = 'version-data.'

version-data.<version> tags a VERSION_DATA_BRANCH_NAME commit the same way.

CG_CONTRIBUTION_DIR_ENV_VAR module-attribute

CG_CONTRIBUTION_DIR_ENV_VAR = 'CG_CONTRIBUTION_DIR'

Environment variable that can override contribution-dir discovery, same as an explicit --contribution-dir CLI flag (parsing/wiring that flag is the CLI layer's job--this module just accepts the resolved explicit value).

DEFAULT_CONTRIBUTION_SUBDIR_NAME module-attribute

DEFAULT_CONTRIBUTION_SUBDIR_NAME = 'contribution'

Name of the subdirectory of the current directory checked as a last-resort discovery step.

CONTRIBUTION_DATA_FILE_NAME module-attribute

CONTRIBUTION_DATA_FILE_NAME = 'contribution-data.json'

Name of the per-view materialized-content manifest file, inside every view's data/ subdirectory.

CONTRIBUTION_IDENTITY_FILE_NAME module-attribute

CONTRIBUTION_IDENTITY_FILE_NAME = 'contribution.json'

Name of the global-identity manifest file, directly inside the contribution directory root only (never inside data/)--never propagated to any materialized view.

CONTRIBUTION_SCHEMA_VERSION module-attribute

CONTRIBUTION_SCHEMA_VERSION = 1

Current on-disk format version for a contribution working directory, recorded in CgContributionIdentity.schema_version so a future format change can detect and offer to migrate an older working directory.

LOCAL_SUBDIR_NAME module-attribute

LOCAL_SUBDIR_NAME = 'local'

Name of the subdirectory holding a local (is_test=True) test case's input/output.

TEST_META_FILE_NAME module-attribute

TEST_META_FILE_NAME = 'test.json'

Name of the metadata file within each named test directory.

TESTS_SUBDIR_NAME module-attribute

TESTS_SUBDIR_NAME = 'tests'

Name of the contribution working directory's test-cases subdirectory.

VALIDATOR_SUBDIR_NAME module-attribute

VALIDATOR_SUBDIR_NAME = 'validator'

Name of the subdirectory holding a server-side validator (is_validator=True) test case's input/output.

CgContributionCommitMetadata dataclass

CgContributionCommitMetadata(extra_data=dict(), contribution_id='', version=0, cover_binary_id=None, cover_binary_hash=None)

Bases: JSONWizardX

The four fast facts about a server-branch commit--built from a CgContribution at fetch/ push time, and the single canonical shape both directions of git trailer conversion (layout.TRAILER_* keys) go through, so there's one definition instead of hand-rolling trailer keys ad hoc at each call site.

contribution_id class-attribute instance-attribute

contribution_id = ''

The opaque contribution ID (CgContribution.public_handle).

version class-attribute instance-attribute

version = 0

The server version number--passed to updateContribution's idempotency check on the next push().

cover_binary_id class-attribute instance-attribute

cover_binary_id = None

The binary ID of the cover image as of this commit (None if it has none).

cover_binary_hash class-attribute instance-attribute

cover_binary_hash = None

The SHA256 (hex) content hash of the cover image identified by cover_binary_id (None if there is none)--the source of truth for cover-image identity against the local working copy (see CgContribution.cover_binary_id for why the ID alone isn't enough there). Always computed by the caller (from the actual cover bytes) alongside redact_commit_contribution, not derivable from a CgContribution alone--so there's no from_contribution() convenience constructor here; callers build this directly with all four fields at hand.

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")

CgContributionBuildFailedError

CgContributionBuildFailedError(result)

Bases: CgContributionManagerError

Raised by CgContributionManager.run_local_tests when build_solution() failed, so no test case was run at all. Carries the full CgBuildResult (compiler diagnostics in .result.output) via .result.

Note build_solution() itself does not raise this--it returns the result, so a caller driving the build directly can display diagnostics however it likes. This exists for the batch wrapper, which has no other way to say "nothing ran".

Source code in codingame_tools/contribution_manager/manager.py
479
480
481
def __init__(self, result: CgBuildResult) -> None:
    self.result = result
    super().__init__(f"solution failed to build:\n{result.output}")

CgContributionLocalTestFailedError

CgContributionLocalTestFailedError(results)

Bases: CgContributionManagerError

Raised by CgContributionManager.run_local_test callers (not by run_local_test itself, which reports one test at a time) to summarize a batch where at least one test case failed. Carries every result (not just the failing ones) via .results.

Source code in codingame_tools/contribution_manager/manager.py
463
464
465
466
467
def __init__(self, results: list[CgContributionLocalTestResult]) -> None:
    self.results = results
    failed = [r for r in results if not r.passed]
    summary = ", ".join(f"{r.ordinal} {r.side} ({r.title})" for r in failed)
    super().__init__(f"{len(failed)}/{len(results)} local test case(s) failed: {summary}")

CgContributionLocalTestResult dataclass

CgContributionLocalTestResult(ordinal, side, title, passed, updated, input, expected_output, actual_output, stderr, timed_out, returncode, exception=None)

The outcome of running data/solution.src against one local tests/ test case--see CgContributionManager.run_local_test.

ordinal instance-attribute

ordinal

The test case's ordinal directory name (see CgContributionLocalTestCase.ordinal).

side instance-attribute

side

Either "local" or "validator".

title instance-attribute

title

The test case's real title.

passed instance-attribute

passed

In compare mode: whether the run completed without crashing/timing out and its stdout matched expected_output. In update mode: whether the run completed without crashing/ timing out at all (a crashed/timed-out run is never used to overwrite output.txt--there's nothing good to accept as the new baseline).

updated instance-attribute

updated

Whether output.txt was actually overwritten from this run (update mode only--always False in compare mode, and False even in update mode if the run crashed/timed out).

input instance-attribute

input

The test case's input, exactly as fed to the solution's stdin.

expected_output instance-attribute

expected_output

Compare mode: the test case's output.txt content as read before this run. Update mode: the same content this run just wrote to output.txt (i.e. actual_output)--so this field always means "whatever output.txt reads as immediately after this result", in both modes.

actual_output instance-attribute

actual_output

What the solution actually wrote to stdout.

stderr instance-attribute

stderr

What the solution wrote to stderr (not itself a failure condition, but useful context when a test does fail).

timed_out instance-attribute

timed_out

Whether the run was killed for exceeding its timeout rather than running to completion.

returncode instance-attribute

returncode

The subprocess's exit code (0 means it ran without crashing; meaningless--always -1--when timed_out is True, same as CgLocalRunResult.returncode). -1 when exception is set instead (the run never even got this far).

exception class-attribute instance-attribute

exception = None

Set by a caller (not by run_local_test itself, which raises rather than returning a result if something goes genuinely wrong) when a batch runner catches and continues past an unexpected exception for this one test case--see cg contribution play.

CgContributionManager

CgContributionManager(contribution_dir, client, *, toolchain_dir=None, mount_root=None, toolchain_languages=None, toolchain_image=None)

Builds/updates a contribution working directory (contribution_dir) against the server, via an already-authenticated CgClient. See the module docstring for the git repo this is backed by.

Source code in codingame_tools/contribution_manager/manager.py
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
def __init__(
            self,
            contribution_dir: Path | str,
            client: CgClient,
            *,
            toolchain_dir: Path | None = None,
            mount_root: Path | None = None,
            toolchain_languages: list[str] | None = None,
            toolchain_image: str | None = None,
        ) -> None:
    # Always resolved to an absolute path: git_repo.py's subprocess calls set `cwd` to
    # `git_dir`/`work_tree` themselves (see CgGitRepo._run), so a relative `contribution_dir`
    # here would make `--git-dir=`/`--work-tree=` (built from it) resolve against the *wrong*
    # cwd inside git's own subprocess--not the caller's original cwd--confirmed live (`cg
    # contribution import ... contribution` from a repo root failed `git init` outright, since
    # the relative `--git-dir=contribution/.meta/...` was interpreted relative to
    # `contribution/data`, not the original cwd).
    self.contribution_dir = Path(contribution_dir).resolve()
    self.client = client
    self.mount_root = Path(mount_root).resolve() if mount_root is not None else None
    self.toolchain_languages = toolchain_languages
    self.toolchain_image = toolchain_image
    self.toolchain_dir = (
            Path(toolchain_dir) if toolchain_dir is not None
            else default_global_data_dir() / TOOLCHAIN_SUBDIR_NAME
        )

mount_root instance-attribute

mount_root = Path(mount_root).resolve() if mount_root is not None else None

Editor workspace root to bind-mount for containerized languages, or None to derive it (see language_context). Normally VS Code's ${workspaceFolder}, passed through by the CLI: cg's own find_workspace_root is a heuristic, and the editor knows the real answer.

identity_file property

identity_file

Path to this working directory's contribution.json (global identity) manifest.

data_dir property

data_dir

Path to this working directory's data/ subdirectory--main's git working tree.

solution_file property

solution_file

The one real solution file, data/solution.<ext>.

Resolved by looking for whatever is actually there rather than by deriving the name from the recorded language: the two can disagree--a working directory written by an older cg still has solution.src, and a fetch can change the language before the rename runs--and the file that exists is the one the user has been editing.

meta_dir property

meta_dir

This working directory's .meta/--always <contribution_dir>/.meta, in both git-dir layouts (see the module docstring). Never inside data/, which holds user state only.

A plain path join, so it never raises and needs no contribution.json to answer. That matters because language_context() needs a meta dir and must stay infallible--cg contribution play works today on a directory holding nothing but data/contribution-data.json, and that must keep working.

meta_file property

meta_file

Path to .meta/contribution-meta.json--see CgContributionMeta.

git_dir property

git_dir

Path to this working directory's git-dir--see _resolve_git_dir for how it is found and the module docstring for the two places it can be.

Raises:

  • FileNotFoundError

    if this working directory has never been imported--nothing to derive it from.

status_cache_file property

status_cache_file

Path to .meta/contribution-status.json (see CgContributionStatusCache). Like meta_dir, and unlike git_dir, this never raises--nothing about its location depends on which git-dir layout is in force.

solution_snapshot_file property

solution_snapshot_file

Path to .meta/solution-snapshot.json--see CgContributionSolutionSnapshot.

selected_test_file property

selected_test_file

Path to .meta/selected-test.json--see CgContributionSelectedTest.

load_meta

load_meta()

This working directory's .meta/contribution-meta.json, or None if it doesn't exist (never created, or .meta/ was deleted) or fails to parse.

Unparseable is treated as absent rather than fatal, like every other .meta/ file: the whole directory is disposable and rebuildable, and git_dir can find the repository on disk without it.

Source code in codingame_tools/contribution_manager/manager.py
906
907
908
909
910
911
912
913
914
915
916
917
918
919
def load_meta(self) -> CgContributionMeta | None:
    """This working directory's `.meta/contribution-meta.json`, or `None` if it doesn't exist
       (never created, or `.meta/` was deleted) or fails to parse.

       Unparseable is treated as absent rather than fatal, like every other `.meta/` file: the
       whole directory is disposable and rebuildable, and `git_dir` can find the repository on
       disk without it."""
    if not self.meta_file.is_file():
        return None
    try:
        return CgContributionMeta.load(self.meta_file)
    except Exception:
        logger.warning("Failed to parse %s--treating as absent.", self.meta_file, exc_info=True)
        return None

server_metadata

server_metadata()

The CgContributionCommitMetadata (version, cover info) at server's current tip, or None if this working directory has never been imported. Public specifically so the CLI can display version numbers without reaching into git_repo/trailer-parsing details.

Source code in codingame_tools/contribution_manager/manager.py
1053
1054
1055
1056
1057
1058
1059
1060
def server_metadata(self) -> CgContributionCommitMetadata | None:
    """The `CgContributionCommitMetadata` (version, cover info) at `server`'s current tip, or
       None if this working directory has never been imported. Public specifically so the CLI
       can display version numbers without reaching into `git_repo`/trailer-parsing details."""
    server_sha = self.git_repo.resolve_ref(SERVER_BRANCH_NAME)
    if server_sha is None:
        return None
    return _trailers_to_metadata(self.git_repo.read_trailers(server_sha))

load_identity

load_identity()

Load contribution.json, or None if this directory has never been imported.

Source code in codingame_tools/contribution_manager/manager.py
1064
1065
1066
1067
1068
def load_identity(self) -> CgContributionIdentity | None:
    """Load `contribution.json`, or None if this directory has never been imported."""
    if not self.identity_file.is_file():
        return None
    return CgContributionIdentity.load(self.identity_file)

load

load()

Load data/contribution-data.json.

Raises:

  • FileNotFoundError

    if this working directory hasn't been imported/initialized yet.

Source code in codingame_tools/contribution_manager/manager.py
1070
1071
1072
1073
1074
1075
1076
def load(self) -> CgContributionView:
    """Load `data/contribution-data.json`.

    Raises:
        FileNotFoundError: if this working directory hasn't been imported/initialized yet.
    """
    return CgContributionView.load(self.contribution_data_file)

save

save(view)

Write view back to data/contribution-data.json, creating data/ if needed.

Source code in codingame_tools/contribution_manager/manager.py
1078
1079
1080
1081
def save(self, view: CgContributionView) -> None:
    """Write `view` back to `data/contribution-data.json`, creating `data/` if needed."""
    self.data_dir.mkdir(parents=True, exist_ok=True)
    view.save(self.contribution_data_file)

read_status_cache

read_status_cache()

Load .meta/contribution-status.json (see _refresh_status_cache), or None if it doesn't exist yet (never fetch()ed/import_()ed under a version of this package new enough to write it) or fails to parse (opportunistic cache, same self-healing spirit as the cover-image reuse in fetch()--corrupt/unreadable is treated as absent, not fatal).

Source code in codingame_tools/contribution_manager/manager.py
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
def read_status_cache(self) -> CgContributionStatusCache | None:
    """Load `.meta/contribution-status.json` (see `_refresh_status_cache`), or None if it
       doesn't exist yet (never `fetch()`ed/`import_()`ed under a version of this package new
       enough to write it) or fails to parse (opportunistic cache, same self-healing spirit as
       the cover-image reuse in `fetch()`--corrupt/unreadable is treated as absent, not fatal).
    """
    path = self.status_cache_file
    if not path.is_file():
        return None
    try:
        return CgContributionStatusCache.load(path)
    except Exception:
        logger.warning("Failed to parse %s--treating as absent.", path, exc_info=True)
        return None

import_ async

import_(contribution_id, *, contribution=None)
Build this working directory from an existing server-side contribution

findContribution (unless contribution is already given), downloading the cover image if one is set, then initializing the git repo with a single shared root commit on both main and server (so git merge-base main server starts out meaningful), plus the corresponding version-data commit. Writes contribution.json if this is a fresh working directory, and .meta/contribution-meta.json either way (repair mode runs precisely when .meta/ went missing, so the record needs rewriting there too).

Also doubles as one of repair()'s two modes: if contribution.json and data/ already exist (e.g. from cloning an outer project that tracks them, or a corrupted/ manually-deleted git-dir) but the git-dir itself is missing, this re-runs the same initialization without overwriting data/'s already-on-disk content for main--only server/version-data are seeded fresh from the current server state. There's no attempt to reconstruct the true historical sync point in this case--nothing durable survives to reconstruct it from; main and server simply start sharing a root again, from right now. See repair()'s docstring for the other mode (no contribution_handle yet at all--this method doesn't handle that one, since it always needs a real contribution_id to fetch).

Raises:

Source code in codingame_tools/contribution_manager/manager.py
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
async def import_(
            self,
            contribution_id: CgContributionId,
            *,
            contribution: CgContribution | None = None,
        ) -> CgContributionView:
    """Build this working directory from an existing server-side contribution:
       `findContribution` (unless `contribution` is already given), downloading the cover
       image if one is set, then initializing the git repo with a single shared root commit
       on both `main` and `server` (so `git merge-base main server` starts out meaningful),
       plus the corresponding `version-data` commit. Writes `contribution.json` if this is a
       fresh working directory, and `.meta/contribution-meta.json` either way (repair mode runs
       precisely when `.meta/` went missing, so the record needs rewriting there too).

       Also doubles as one of `repair()`'s two modes: if `contribution.json` and `data/`
       already exist (e.g. from cloning an outer project that tracks them, or a corrupted/
       manually-deleted git-dir) but the git-dir itself is missing, this re-runs the same
       initialization *without* overwriting `data/`'s already-on-disk content for `main`--only
       `server`/`version-data` are seeded fresh from the current server state. There's no
       attempt to reconstruct the *true* historical sync point in this case--nothing durable
       survives to reconstruct it from; `main` and `server` simply start sharing a root again,
       from right now. See `repair()`'s docstring for the other mode (no `contribution_handle`
       yet at all--this method doesn't handle that one, since it always needs a real
       `contribution_id` to fetch).

    Raises:
        CgContributionManagerError: if this directory already tracks a *different*
                                     contribution, or already has a git repository.
    """
    identity = self.load_identity()
    if identity is not None and identity.contribution_handle != contribution_id:
        raise CgContributionManagerError(
                f"{self.identity_file} already tracks contribution "
                f"{identity.contribution_handle!r}; refusing to import {contribution_id!r} "
                "into the same directory."
            )
    repairing = identity is not None
    self._reject_legacy_layout()
    git_dir = self._resolve_git_dir()
    if git_dir.is_dir():
        raise CgContributionManagerError(
                f"{git_dir} already exists--this working directory has already been imported "
                "(see `cg contribution git` to inspect it directly)."
            )

    if contribution is None:
        contribution = await self.client.services.contribution.find_contribution(contribution_id)
    version = contribution.last_version
    data = version.data

    cover_bytes: bytes | None = None
    if data.cover_binary_id is not None:
        download = await self.client.servlets.file_servlet(data.cover_binary_id)
        cover_bytes = download.content

    if not repairing:
        _materialize_data(
                self.data_dir,
                puzzle_type=contribution.contribution_type,
                draft=version.draft if version.draft is not None else True,
                ready_for_moderation=version.ready_for_moderation if version.ready_for_moderation is not None else False,
                data=data,
                cover_bytes=cover_bytes,
            )
        _write_meta_gitignore(self.contribution_dir)
        self._save_identity(contribution_id)
        _align_solution_file_name(self.contribution_dir, data.solution_language)

    if repairing:
        # Unlike the fresh-import path above (which built data/ itself, via
        # _materialize_data()--always normalized), repairing snapshots whatever's already
        # on disk, preserved as-is from the outer clone. If that on-disk tests/ layout isn't
        # already in the canonical ordinal-dirname form (e.g. it came from an older tool, or
        # from local edits that inserted/reordered directories), this commit's tree would
        # permanently encode that non-canonical layout--and a later fetch()/import_() (always
        # canonical, via _materialize_data()) would then show a spurious diff/conflict against
        # it even when the actual test content never changed. See push()/merge_continue()
        # for the same concern at those other points content ever gets committed from
        # whatever's on disk.
        renormalize_test_case_dirs(self.tests_dir)

    self._save_meta(git_dir)  # both paths: repair mode runs when .meta/ went missing
    init_repo(git_dir, self.data_dir)
    repo = CgGitRepo(git_dir, self.data_dir)
    repo.set_head(MAIN_BRANCH_NAME)

    tree = repo.write_tree_from_worktree()
    message = "Repair from server" if repairing else "Import from server"
    server_sha = self._record_server_commit(repo, tree, contribution, cover_bytes, f"{message} (version {version.version})")
    repo.reset_index_to(server_sha)

    await self._refresh_status_cache(contribution)

    return self.load()

repair async

repair()

Reconstruct this working directory's git-dir from scratch, without disturbing data/'s already-on-disk content--for recovering from a missing or corrupted .meta//git-dir (e.g. an outer project clone that deliberately didn't bring the git-dir along--see the module docstring--or the git-dir having been manually deleted/corrupted).

Two modes, chosen automatically from contribution.json's contribution_handle:

  • Set (this working directory has already been push()d/import_()d before): re-bases off the server--delegates to import_()'s own repair mode, which re-fetches current server state fresh to seed server/version-data's first commit, while main's first commit is built from data/'s current on-disk content, preserved as-is. No attempt is made to reconstruct the true historical sync point--nothing durable survives to reconstruct it from.
  • Not set (this working directory was create()d but never successfully push()d): purely local, no network access at all--just re-establishes main's initial commit from data/'s current on-disk content, the same way create() itself would, but preserving what's already there instead of overwriting it with placeholder content. No server/version-data branches are created; there's no server-side contribution yet to base them on.

Raises:

  • FileNotFoundError

    if this working directory has never been created/imported at all (no contribution.json), or if data/ itself is missing (nothing on disk to repair/preserve).

  • CgContributionManagerError

    if the git-dir already exists (nothing to repair).

Source code in codingame_tools/contribution_manager/manager.py
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
async def repair(self) -> CgContributionView:
    """Reconstruct this working directory's git-dir from scratch, without disturbing `data/`'s
       already-on-disk content--for recovering from a missing or corrupted `.meta/`/git-dir
       (e.g. an outer project clone that deliberately didn't bring the git-dir along--see the
       module docstring--or the git-dir having been manually deleted/corrupted).

       Two modes, chosen automatically from `contribution.json`'s `contribution_handle`:

       - Set (this working directory has already been `push()`d/`import_()`d before): re-bases
         off the server--delegates to `import_()`'s own repair mode, which re-fetches current
         server state fresh to seed `server`/`version-data`'s first commit, while `main`'s
         first commit is built from `data/`'s current on-disk content, preserved as-is. No
         attempt is made to reconstruct the *true* historical sync point--nothing durable
         survives to reconstruct it from.
       - Not set (this working directory was `create()`d but never successfully `push()`d):
         purely local, no network access at all--just re-establishes `main`'s initial commit
         from `data/`'s current on-disk content, the same way `create()` itself would, but
         preserving what's already there instead of overwriting it with placeholder content.
         No `server`/`version-data` branches are created; there's no server-side contribution
         yet to base them on.

    Raises:
        FileNotFoundError: if this working directory has never been created/imported at all
                            (no `contribution.json`), or if `data/` itself is missing (nothing
                            on disk to repair/preserve).
        CgContributionManagerError: if the git-dir already exists (nothing to repair).
    """
    identity = self.load_identity()
    if identity is None:
        raise FileNotFoundError(
                f"{self.identity_file} does not exist--nothing to repair (this working "
                "directory has never been created/imported)."
            )
    if identity.contribution_handle is not None:
        return await self.import_(identity.contribution_handle)

    # Never pushed--purely local reconstruction, no network access, mirroring create()'s own
    # git-init/commit steps but preserving data/'s current on-disk content instead of
    # overwriting it with placeholder content.
    self._reject_legacy_layout()
    git_dir = self._resolve_git_dir()
    if git_dir.is_dir():
        raise CgContributionManagerError(f"{git_dir} already exists--nothing to repair.")
    if not self.data_dir.is_dir():
        raise FileNotFoundError(f"{self.data_dir} does not exist--nothing to repair from.")

    _write_meta_gitignore(self.contribution_dir)
    renormalize_test_case_dirs(self.tests_dir)  # see import_()'s repair mode for why
    # Before the first commit, so the rename lands in the initial tree rather than showing up
    # as an uncommitted change immediately afterwards. This is also the migration path for a
    # working directory an older cg left holding `data/solution.src` plus a root symlink.
    _align_solution_file_name(self.contribution_dir, self.load().data.solution_language)

    self._save_meta(git_dir)
    init_repo(git_dir, self.data_dir)
    repo = CgGitRepo(git_dir, self.data_dir)
    repo.set_head(MAIN_BRANCH_NAME)
    repo.commit_worktree("Initial local content (repaired, not yet pushed to the server)")

    return self.load()

create async

create(*, title, puzzle_type='PUZZLE_INOUT', language='Python3')

Initialize a brand new, purely local contribution working directory--no network access at all (async only for interface consistency with every other method here), and deliberately so: no server-side contribution exists yet, matching how git init never touches a remote either. contribution.json's contribution_handle is left None; the first successful push() fills it in, via createContribution instead of the usual updateContribution--see push()'s docstring for the full create-vs-update story.

Seeds the same placeholder statement/difficulty/test-case content push()'s first call will need (confirmed live that createContribution 500s on a title-only payload--see push())--edit it via the usual sidecar files before that first push. Also seeds contribution-data.json's draft/ready_for_moderation to a private-draft default (True/False)--just a starting value, not locked down: like every other field here, freely editable before the first push, which reads whatever's actually there at that point, the same as any later push.

A real git repo is still initialized here, with an initial commit onto main--local history from before the first push is a normal, supported thing to have (e.g. via cg contribution git), it just isn't reachable from main after that first push succeeds (see push()'s docstring for why, same as every other place in this class that resets main directly onto a freshly-built commit rather than preserving prior lineage).

Refuses upfront if this directory already looks like a contribution working directory in any way--create() itself has no repair mode: a brand new contribution can't already have a matching contribution.json/git-dir from some earlier session, so any pre-existing state here means something is wrong, not something to press on through. If contribution.json exists but the git-dir is missing/corrupted (e.g. this directory was already create()d, possibly even already push()d), use repair() instead.

Parameters:

  • title (str) –

    The new contribution's title.

  • puzzle_type (CgPuzzleType, default: 'PUZZLE_INOUT' ) –

    The type of the contribution. Defaults to "PUZZLE_INOUT" (a standard noninteractive solo puzzle--the only type this package's contribution manager has been exercised against).

  • language (str, default: 'Python3' ) –

    The reference solution's language (see CgSolutionLanguage). Defaults to "Python3". Always gets the solution.<ext> convenience symlink (see _align_solution_file_name) if language maps to a known extension--but data/solution.src itself (the symlink's target) is only pre-populated with a real stub if codingame_tools.language.get_language(language). build_contribution_create_stub_source() returns one (currently only "Python3"); for any other language, the symlink is left dangling until you write data/solution.src yourself.

Raises:

  • CgContributionManagerError

    if this directory already tracks a contribution, or a git-dir already exists at the location this would use.

Source code in codingame_tools/contribution_manager/manager.py
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
async def create(
            self,
            *,
            title: str,
            puzzle_type: CgPuzzleType = "PUZZLE_INOUT",
            language: str = "Python3",
        ) -> CgContributionView:
    """Initialize a brand new, *purely local* contribution working directory--no network
       access at all (`async` only for interface consistency with every other method here),
       and deliberately so: no server-side contribution exists yet, matching
       how `git init` never touches a remote either. `contribution.json`'s
       `contribution_handle` is left `None`; the first successful `push()` fills it in, via
       `createContribution` instead of the usual `updateContribution`--see `push()`'s
       docstring for the full create-vs-update story.

       Seeds the same placeholder statement/difficulty/test-case content `push()`'s first call
       will need (confirmed live that `createContribution` 500s on a title-only payload--see
       `push()`)--edit it via the usual sidecar files before that first push. Also seeds
       `contribution-data.json`'s `draft`/`ready_for_moderation` to a private-draft default
       (`True`/`False`)--just a starting value, not locked down: like every other field here,
       freely editable before the first push, which reads whatever's actually there at that
       point, the same as any later push.

       A real git repo is still initialized here, with an initial commit onto `main`--local
       history from before the first push is a normal, supported thing to have (e.g. via `cg
       contribution git`), it just isn't reachable from `main` after that first push succeeds
       (see `push()`'s docstring for why, same as every other place in this class that resets
       `main` directly onto a freshly-built commit rather than preserving prior lineage).

       Refuses upfront if this directory already looks like a contribution working directory
       in any way--`create()` itself has no repair mode: a *brand new* contribution can't
       already have a matching `contribution.json`/git-dir from some earlier session, so any
       pre-existing state here means something is wrong, not something to press on through. If
       `contribution.json` exists but the git-dir is missing/corrupted (e.g. this directory
       was already `create()`d, possibly even already `push()`d), use `repair()` instead.

    Args:
        title:       The new contribution's title.
        puzzle_type: The type of the contribution. Defaults to "PUZZLE_INOUT" (a standard
                     noninteractive solo puzzle--the only type this package's contribution
                     manager has been exercised against).
        language:    The reference solution's language (see `CgSolutionLanguage`). Defaults
                     to "Python3". Always gets the `solution.<ext>` convenience symlink (see
                     `_align_solution_file_name`) if `language` maps to a known extension--but
                     `data/solution.src` itself (the symlink's target) is only pre-populated
                     with a real stub if `codingame_tools.language.get_language(language).
                     build_contribution_create_stub_source()` returns one (currently only
                     "Python3"); for any other language, the symlink is left dangling until
                     you write `data/solution.src` yourself.

    Raises:
        CgContributionManagerError: if this directory already tracks a contribution, or a
                                     git-dir already exists at the location this would use.
    """
    identity = self.load_identity()
    if identity is not None:
        raise CgContributionManagerError(
                f"{self.identity_file} already exists (tracks contribution "
                f"{identity.contribution_handle!r})--`create()` only makes sense for a brand "
                "new working directory."
            )
    self._reject_legacy_layout()
    git_dir = self._git_dir_for(not is_inside_existing_repo(self.contribution_dir))
    if git_dir.is_dir():
        raise CgContributionManagerError(
                f"{git_dir} already exists, though {self.identity_file} does not--refusing "
                "to create a new contribution into a directory in this inconsistent state."
            )

    # Both the seeded test and validator case are test_in="1"/test_out="1" (see
    # _minimal_valid_contribution_data)--a solution that just echoes its input back trivially
    # passes both. codingame_tools.language.CgLanguage.build_contribution_create_stub_source
    # only returns one for languages with such a trivial stub (currently just Python3); an
    # empty `data/solution.src` isn't meaningfully better than no file at all otherwise (the
    # symlink itself--see _align_solution_file_name--already tells the user exactly where to
    # put their code, for every language, regardless of this).
    solution = await get_language(language).build_contribution_create_stub_source()
    data = dataclasses.replace(
            _starter_contribution_data(title), solution_language=language, solution=solution)
    _materialize_data(
            self.data_dir, puzzle_type=puzzle_type, draft=True, ready_for_moderation=False,
            data=data, cover_bytes=_cover_placeholder_bytes(),
        )
    _write_meta_gitignore(self.contribution_dir)
    self._save_identity(None)
    self._save_meta(git_dir)
    # Record the generated stub, so `set_language()` can tell a brand-new contribution (nothing
    # to lose) from one whose solution.src holds real work.
    self._write_solution_snapshot(language, solution)
    _align_solution_file_name(self.contribution_dir, data.solution_language)

    init_repo(git_dir, self.data_dir)
    repo = CgGitRepo(git_dir, self.data_dir)
    repo.set_head(MAIN_BRANCH_NAME)
    repo.commit_worktree("Initial local content (not yet pushed to the server)")

    return self.load()

push async

push(*, direct_create=False, force=False)

Push this working directory's content to the server, updating server/version-data to reflect the result on success, then auto-committing main to match (its content already matches what was just pushed, by construction).

Deliberately hides a create-vs-update decision that real git never has to make. git push always requires an already-configured remote (git remote add/git push -u first)--pushing establishes no new identity, it only updates one that already exists. This method is different: if this working directory has never been pushed before (i.e. it was built via create(), not import_(), and no push() has succeeded yet), it establishes a contribution on the server first, and on success writes its handle into contribution.json (CgContributionIdentity.contribution_handle, previously None)-- establishing the "remote" implicitly, as a side effect of the very first push, rather than as a separate explicit step. Every later push() against the same working directory takes the normal updateContribution path, exactly like today. This is a deliberate simplification of the git model, chosen specifically so that create() itself never has to call createContribution with placeholder content just to get a handle to import--it stays purely local (see create()'s docstring) until the user has real content ready to push.

The first push is itself two API calls, not one--direct_create opts back into the single-call version. createContribution, unlike updateContribution, has no prevVersion-style idempotency check--if a request succeeds server-side but the response is lost (timeout, network error, and especially the same Cloudflare/524 origin timeout CgContributionServiceHelper.update_contribution already has to recover from for heavy content), there is no reliable way to learn the resulting handle, and blindly retrying risks a genuine duplicate contribution. This risk scales with the size/ complexity of what's being validated--exactly what a first push often has a lot of (hand-written test suites, or, worse, an entire test suite carried over via delete( keep_local=True)'s "use an existing contribution as a template" workflow). So by default, the first push doesn't send the real content to createContribution at all: 1. createContribution is called with a minimal, throwaway, in-memory-only stub (real title, otherwise just enough to be accepted--see _minimal_valid_contribution_data--always a private draft, never for moderation, no cover)--small and fast enough that a 524 here is unlikely in the first place. 2. The returned handle is written into contribution.json immediately, before doing anything else with it--so if step 3 below fails, a retried push() sees contribution_handle already set and raises (see the next paragraph) rather than risking another createContribution call. 3. A commit representing that stub (not the real content) becomes server's first commit, via the same plumbing fetch() uses to build a tree without touching main. 4. The real content is then submitted the normal way--a plain updateContribution call, version 1 -> 2, with CgContributionServiceHelper's existing 524-retry/ polling already protecting it via prevVersion. If this step itself fails/times out, the fix is exactly the same as any other failed push: just run push() again.

Passing direct_create=True skips all of that and calls createContribution once, directly, with the real content--the original, simpler behavior, for callers confident their first push is small/fast enough not to need the extra round trip.

The create-vs-update decision is made from contribution.json's contribution_handle (None => first push), not from whether the server git branch happens to exist-- those two can disagree, and when they do, contribution.json is authoritative: e.g. an outer project clone whose git-dir was deliberately not brought along (see repair()'s docstring) has a real contribution_handle but no server branch yet, and someone could always delete/corrupt the git-dir by hand. Trusting "server branch missing" as "never pushed" in either case would call createContribution again for a contribution that already exists--a duplicate, not a recoverable mistake. So if contribution_handle is already set but server still doesn't resolve, this raises instead of guessing--see Raises below.

A push with nothing to push is a no-op, returning None without contacting the server, because updateContribution has no notion of an empty update: it increments the version and re-runs moderation regardless of whether anything differs. Publishing a new version of identical content costs a review cycle and buries the history of real changes, so it has to be asked for explicitly rather than happening by accident.

"Nothing to push" is decided by comparing the working tree against server's tip tree, which is exactly what the last push or fetch recorded--so it covers every content file including the cover image, and is unaffected by commit metadata. Checked before the cover upload, so an unchanged cover isn't re-uploaded just to discover there was no update to make.

Parameters:

  • direct_create (bool, default: False ) –

    Skip the minimal-stub-first safety step on a first push, and call createContribution once, directly, with the real content--see above. Ignored (has no effect) on anything but a first push.

  • force (bool, default: False ) –

    Push even when the working tree is identical to what the server already has. Ignored on a first push, which always has something to establish.

Returns:

  • CgContribution | None

    The contribution as the server now holds it, or None if there was nothing to push.

Raises:

  • FileNotFoundError

    if this working directory hasn't been created/imported yet.

  • CgContributionManagerError

    if puzzle_type isn't set, if a merge is in progress, or if contribution.json already has a contribution_handle but this working directory's git repo has no server branch (run repair() first--see above).

Source code in codingame_tools/contribution_manager/manager.py
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
async def push(self, *, direct_create: bool = False, force: bool = False) -> CgContribution | None:
    """Push this working directory's content to the server, updating `server`/`version-data`
       to reflect the result on success, then auto-committing `main` to match (its content
       already matches what was just pushed, by construction).

       **Deliberately hides a create-vs-update decision that real git never has to make.**
       `git push` always requires an already-configured remote (`git remote add`/`git push -u`
       first)--pushing establishes no new identity, it only updates one that already exists.
       This method is different: if this working directory has never been pushed before (i.e.
       it was built via `create()`, not `import_()`, and no `push()` has succeeded yet), it
       establishes a contribution on the server first, and on success writes its handle into
       `contribution.json` (`CgContributionIdentity.contribution_handle`, previously `None`)--
       establishing the "remote" implicitly, as a side effect of the very first push, rather
       than as a separate explicit step. Every later `push()` against the same working
       directory takes the normal `updateContribution` path, exactly like today. This is a
       deliberate simplification of the git model, chosen specifically so that `create()`
       itself never has to call `createContribution` with placeholder content just to get a
       handle to import--it stays purely local (see `create()`'s docstring) until the user has
       real content ready to push.

       **The first push is itself two API calls, not one--`direct_create` opts back into the
       single-call version.** `createContribution`, unlike `updateContribution`, has no
       `prevVersion`-style idempotency check--if a request succeeds server-side but the
       response is lost (timeout, network error, and especially the same Cloudflare/524 origin
       timeout `CgContributionServiceHelper.update_contribution` already has to recover
       from for *heavy* content), there is no reliable way to learn the resulting handle, and
       blindly retrying risks a genuine duplicate contribution. This risk scales with the size/
       complexity of what's being validated--exactly what a first push often has a lot of
       (hand-written test suites, or, worse, an entire test suite carried over via `delete(
       keep_local=True)`'s "use an existing contribution as a template" workflow). So by
       default, the first push doesn't send the real content to `createContribution` at all:
       1. `createContribution` is called with a minimal, throwaway, in-memory-only stub (real
          title, otherwise just enough to be accepted--see
          `_minimal_valid_contribution_data`--always a private draft, never for moderation, no
          cover)--small and fast enough that a 524 here is unlikely in the first place.
       2. The returned handle is written into `contribution.json` *immediately*, before doing
          anything else with it--so if step 3 below fails, a retried `push()` sees
          `contribution_handle` already set and raises (see the next paragraph) rather than
          risking another `createContribution` call.
       3. A commit representing that stub (not the real content) becomes `server`'s first
          commit, via the same plumbing `fetch()` uses to build a tree without touching `main`.
       4. The *real* content is then submitted the normal way--a plain `updateContribution`
          call, version 1 -> 2, with `CgContributionServiceHelper`'s existing 524-retry/
          polling already protecting it via `prevVersion`. If this step itself fails/times out,
          the fix is exactly the same as any other failed push: just run `push()` again.

       Passing `direct_create=True` skips all of that and calls `createContribution` once,
       directly, with the real content--the original, simpler behavior, for callers confident
       their first push is small/fast enough not to need the extra round trip.

       The create-vs-update decision is made from `contribution.json`'s `contribution_handle`
       (`None` => first push), *not* from whether the `server` git branch happens to exist--
       those two can disagree, and when they do, `contribution.json` is authoritative: e.g. an
       outer project clone whose git-dir was deliberately not brought along (see `repair()`'s
       docstring) has a real `contribution_handle` but no `server` branch *yet*, and someone
       could always delete/corrupt the git-dir by hand. Trusting "`server` branch missing" as
       "never pushed" in either case would call `createContribution` *again* for a contribution
       that already exists--a duplicate, not a recoverable mistake. So if `contribution_handle`
       is already set but `server` still doesn't resolve, this raises instead of guessing--see
       `Raises` below.

       **A push with nothing to push is a no-op**, returning None without contacting the server,
       because `updateContribution` has no notion of an empty update: it increments the version
       and re-runs moderation regardless of whether anything differs. Publishing a new version
       of identical content costs a review cycle and buries the history of real changes, so it
       has to be asked for explicitly rather than happening by accident.

       "Nothing to push" is decided by comparing the working tree against `server`'s tip *tree*,
       which is exactly what the last push or fetch recorded--so it covers every content file
       including the cover image, and is unaffected by commit metadata. Checked before the cover
       upload, so an unchanged cover isn't re-uploaded just to discover there was no update to
       make.

    Args:
        direct_create: Skip the minimal-stub-first safety step on a first push, and call
                        `createContribution` once, directly, with the real content--see above.
                        Ignored (has no effect) on anything but a first push.
        force:         Push even when the working tree is identical to what the server already
                        has. Ignored on a first push, which always has something to establish.

    Returns:
        The contribution as the server now holds it, or None if there was nothing to push.

    Raises:
        FileNotFoundError: if this working directory hasn't been created/imported yet.
        CgContributionManagerError: if `puzzle_type` isn't set, if a merge is in progress, or
                                     if `contribution.json` already has a `contribution_handle`
                                     but this working directory's git repo has no `server`
                                     branch (run `repair()` first--see above).
    """
    if self.merge_in_progress:
        raise CgContributionManagerError(
                "A merge is in progress (see `cg contribution merge continue`/`abort`)--"
                "resolve or abort it before pushing."
            )
    view = self.load()
    if view.puzzle_type is None:
        raise CgContributionManagerError("Cannot push: puzzle_type is not set in contribution-data.json.")

    # Canonicalize tests/'s ordinal directory names before snapshotting data_dir below: this
    # commit's tree becomes server's new tip verbatim (see the write_tree_from_worktree() call
    # further down), and server's *next* tree (built fresh from a later fetch()/import_(), via
    # _materialize_data()--always canonical) would otherwise show a spurious diff/conflict
    # against a non-canonical layout committed here, even when the actual test content never
    # changed. Content-preserving (only directory names change)--see
    # test_cases_dir.renormalize_test_case_dirs.
    renormalize_test_case_dirs(self.tests_dir)

    identity = self.load_identity()
    assert identity is not None  # merge_in_progress above already required a loadable git_dir
    first_push = identity.contribution_handle is None

    repo = self.git_repo
    server_sha = repo.resolve_ref(SERVER_BRANCH_NAME)
    if not first_push and server_sha is None:
        raise CgContributionManagerError(
                f"{self.identity_file} already tracks contribution "
                f"{identity.contribution_handle!r}, but this working directory's git repo has "
                f"no {SERVER_BRANCH_NAME} branch (missing/corrupted git-dir, or a freshly "
                "cloned outer project that hasn't been repaired yet)--call repair() (`cg "
                "contribution repair`) before pushing."
            )

    if first_push and not direct_create:
        stub_data = _minimal_valid_contribution_data(view.data.title)
        stub_handle = await self.client.services.contribution.helper.create_contribution(
                view.puzzle_type, stub_data, draft=True, ready_for_moderation=False)
        self._write_contribution_handle(stub_handle)  # see the docstring--persisted before find_contribution
        stub_contribution = await self.client.services.contribution.find_contribution(stub_handle)
        with tempfile.TemporaryDirectory(prefix="cg-contribution-stub-") as tmp:
            staging = Path(tmp)
            _materialize_data(
                    staging, puzzle_type=view.puzzle_type, draft=True, ready_for_moderation=False,
                    data=stub_data, cover_bytes=None,
                )
            stub_tree = repo.write_tree_from_dir(staging)
        server_sha = self._record_server_commit(
                repo, stub_tree, stub_contribution, None,
                f"Create placeholder on server (version {stub_contribution.last_version.version})",
            )

    # Only true if direct_create was requested--the stub step above (when it ran) already
    # turned this into an ordinary update, same as any push against an existing contribution.
    needs_direct_create = first_push and direct_create

    # Nothing to push? Say so and stop, before the cover upload below spends a request finding
    # out. server's tip tree is what the last push/fetch recorded, so comparing the worktree
    # against it covers every content file, cover image included.
    if not first_push and not force:
        assert server_sha is not None  # guaranteed by the raise above for a non-first push
        server_tree = repo.rev_parse(f"{server_sha}^{{tree}}", check=False)
        if server_tree is not None and repo.write_tree_from_worktree() == server_tree:
            return None

    # No prior server state to compare a cover image's hash against when creating (directly,
    # or via the stub established above, which never has a cover either)--the empty metadata
    # below just always fails that comparison, forcing a fresh upload, same as any other cover
    # change.
    if needs_direct_create:
        current_metadata = CgContributionCommitMetadata(contribution_id="", version=0)
    else:
        assert server_sha is not None  # the raise above (or the stub step) already ensured this
        current_metadata = _trailers_to_metadata(repo.read_trailers(server_sha))

    cover_path = self.data_dir / COVER_IMAGE_FILE_NAME
    cover_binary_id: int | None
    cover_bytes: bytes | None
    if cover_path.is_file():
        cover_bytes = cover_path.read_bytes()
        cover_content_hash = compute_content_hash(cover_bytes)
        if cover_content_hash == current_metadata.cover_binary_hash and current_metadata.cover_binary_id is not None:
            cover_binary_id = current_metadata.cover_binary_id
        else:
            upload = await self.client.servlets.file_upload(
                    cover_bytes, filename=COVER_IMAGE_FILE_NAME, content_type="image/png")
            cover_binary_id = upload.id
    else:
        cover_binary_id = None
        cover_bytes = None

    local_data, _ = _read_local_data(self.data_dir, view.data)
    data = dataclasses.replace(local_data, cover_binary_id=cover_binary_id)

    if needs_direct_create:
        contribution_id = await self.client.services.contribution.helper.create_contribution(
                view.puzzle_type, data, view.draft, view.ready_for_moderation)
        result = await self.client.services.contribution.find_contribution(contribution_id)
        self._write_contribution_handle(contribution_id)
    else:
        result = await self.client.services.contribution.helper.update_contribution(
                current_metadata.contribution_id,
                view.puzzle_type,
                data,
                view.draft,
                view.ready_for_moderation,
                current_metadata.version,
            )
        result = await self._refresh_active_version(result, current_metadata.contribution_id)

    tree = repo.write_tree_from_worktree()
    new_server_sha = self._record_server_commit(
            repo, tree, result, cover_bytes, f"Push to server (version {result.last_version.version})")
    # main's ref moves directly onto server's new commit (not a separate sibling commit with
    # matching content)--deliberately, so `git merge-base main server` still equals server's
    # tip afterward. A sibling commit here (e.g. via commit_worktree()) would have the *same*
    # tree but a *different* SHA (different parent/message/trailers), leaving merge-base stuck
    # at the pre-push point and making the next rebase()/merge_start() wrongly see "local
    # changed" even though content-wise nothing has, confirmed by direct testing. Uses
    # reset_index_to() rather than a raw update_ref(), so the real index (never touched by
    # write_tree_from_worktree()'s scratch-index tree build) stays in sync with main's new
    # tip too--otherwise a later real `git merge` (merge_start()) reads a stale index. On a
    # first push specifically, this also means any *local-only* history main had before the
    # push (e.g. commits made via `cg contribution git` while drafting) stops being reachable
    # from main's new tip--not deleted, just no longer part of main's ancestry, recoverable via
    # the reflog for as long as it lasts. Same tradeoff already accepted everywhere else this
    # class resets main's ref directly instead of preserving lineage; not special-cased here.
    repo.reset_index_to(new_server_sha)
    return result

fetch async

fetch()

Refresh server's tip from a fresh findContribution, and unconditionally refresh .meta/contribution-status.json (see _refresh_status_cache)--even when the content version hasn't changed, since none of that cache's fields (score/votes/comment count/ views/moderator approve-reject tallies/etc.) are tied to it; only the server/ version-data git commit is skipped in that case. Never touches main, the working tree, or the real index--the fetched content is staged into a throwaway temp directory purely to build a tree object from, so this is safe to call regardless of what's currently on disk in data/.

Reuses the previous cover image's bytes (read straight out of the object database, via server's current tip) rather than re-downloading, if its binary ID is unchanged; self-heals (re-downloads) rather than raising if that reuse ever turns out to be stale/ corrupted--this cache is opportunistic, not sacred.

Raises:

  • FileNotFoundError

    if this working directory has never been imported/committed.

  • CgContributionManagerError

    if a merge is in progress.

Source code in codingame_tools/contribution_manager/manager.py
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
async def fetch(self) -> CgContribution:
    """Refresh `server`'s tip from a fresh `findContribution`, and unconditionally refresh
       `.meta/contribution-status.json` (see `_refresh_status_cache`)--even when the content
       version hasn't changed, since none of that cache's fields (score/votes/comment count/
       views/moderator approve-reject tallies/etc.) are tied to it; only the `server`/
       `version-data` git commit is skipped in that case. Never touches `main`, the working
       tree, or the real index--the fetched content is staged into a throwaway temp directory
       purely to build a tree object from, so this is safe to call regardless of what's
       currently on disk in `data/`.

       Reuses the previous cover image's bytes (read straight out of the object database, via
       `server`'s current tip) rather than re-downloading, if its binary ID is unchanged;
       self-heals (re-downloads) rather than raising if that reuse ever turns out to be stale/
       corrupted--this cache is opportunistic, not sacred.

    Raises:
        FileNotFoundError: if this working directory has never been imported/committed.
        CgContributionManagerError: if a merge is in progress.
    """
    if self.merge_in_progress:
        raise CgContributionManagerError(
                "A merge is in progress (see `cg contribution merge continue`/`abort`)--"
                "resolve or abort it before fetching."
            )
    identity = self.load_identity()
    assert identity is not None  # merge_in_progress above already required a loadable git_dir

    repo = self.git_repo
    server_sha = repo.resolve_ref(SERVER_BRANCH_NAME)
    if server_sha is None:
        raise FileNotFoundError(f"{self.git_dir} has no {SERVER_BRANCH_NAME} branch--nothing to fetch against.")
    current_metadata = _trailers_to_metadata(repo.read_trailers(server_sha))

    contribution = await self.client.services.contribution.find_contribution(current_metadata.contribution_id)
    await self._refresh_status_cache(contribution)
    if contribution.last_version.version == current_metadata.version:
        return contribution  # server's tip already reflects this exact version; status cache still refreshed above

    version = contribution.last_version
    data = version.data
    new_binary_id = data.cover_binary_id
    cover_bytes: bytes | None
    if new_binary_id is None:
        cover_bytes = None
    elif new_binary_id == current_metadata.cover_binary_id:
        cached = repo.read_file_at(SERVER_BRANCH_NAME, COVER_IMAGE_FILE_NAME)
        if cached is not None and compute_content_hash(cached) == current_metadata.cover_binary_hash:
            cover_bytes = cached
        else:
            download = await self.client.servlets.file_servlet(new_binary_id)
            cover_bytes = download.content
    else:
        download = await self.client.servlets.file_servlet(new_binary_id)
        cover_bytes = download.content

    with tempfile.TemporaryDirectory(prefix="cg-contribution-fetch-") as tmp:
        staging = Path(tmp)
        _materialize_data(
                staging,
                puzzle_type=contribution.contribution_type,
                draft=version.draft if version.draft is not None else True,
                ready_for_moderation=version.ready_for_moderation if version.ready_for_moderation is not None else False,
                data=data,
                cover_bytes=cover_bytes,
            )
        tree = repo.write_tree_from_dir(staging)
    self._record_server_commit(repo, tree, contribution, cover_bytes, f"Fetch from server (version {version.version})")
    return contribution

rebase async

rebase()

Detect drift between server and main, and automatically resolve it when that's unambiguous:

  • server unchanged since main last synced: nothing to do, regardless of local edits (CgRebaseStatus.UP_TO_DATE).
  • server changed, main unchanged since it last synced: fast-forward--main gets a new commit matching server's new tip (CgRebaseStatus.FAST_FORWARDED).
  • Both changed: a real conflict, left entirely alone (CgRebaseStatus.CONFLICT)--use cg contribution diff to inspect, and cg contribution merge to resolve.

Raises:

  • FileNotFoundError

    if this working directory has never been imported/committed.

  • CgContributionManagerError

    if a merge is already in progress.

Source code in codingame_tools/contribution_manager/manager.py
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
async def rebase(self) -> CgRebaseStatus:
    """Detect drift between `server` and `main`, and automatically resolve it when that's
       unambiguous:

       - `server` unchanged since `main` last synced: nothing to do, regardless of local edits
         (`CgRebaseStatus.UP_TO_DATE`).
       - `server` changed, `main` unchanged since it last synced: fast-forward--`main` gets a
         new commit matching `server`'s new tip (`CgRebaseStatus.FAST_FORWARDED`).
       - Both changed: a real conflict, left entirely alone (`CgRebaseStatus.CONFLICT`)--use
         `cg contribution diff` to inspect, and `cg contribution merge` to resolve.

    Raises:
        FileNotFoundError: if this working directory has never been imported/committed.
        CgContributionManagerError: if a merge is already in progress.
    """
    if self.merge_in_progress:
        raise CgContributionManagerError(
                "A merge is in progress (see `cg contribution merge continue`/`abort`)--"
                "resolve or abort it before rebasing."
            )
    repo = self.git_repo
    base_before = repo.merge_base(MAIN_BRANCH_NAME, SERVER_BRANCH_NAME)
    if base_before is None:
        raise FileNotFoundError(f"{self.git_dir} has no shared history between main/server--nothing to rebase against.")

    await self.fetch()

    server_after = repo.resolve_ref(SERVER_BRANCH_NAME)
    if server_after == base_before:
        return CgRebaseStatus.UP_TO_DATE
    assert server_after is not None  # can't have changed away from a real SHA to nothing

    main_sha = repo.resolve_ref(MAIN_BRANCH_NAME)
    if main_sha == base_before:
        # A *true* fast-forward: main's ref moves directly onto server's tip, same as real
        # git's own definition--no new commit created. checkout_all() still needs to run to
        # bring the working tree/index along with it.
        repo.checkout_all(SERVER_BRANCH_NAME)
        repo.update_ref(f"refs/heads/{MAIN_BRANCH_NAME}", server_after)
        _align_solution_file_name(self.contribution_dir, self.load().data.solution_language)
        return CgRebaseStatus.FAST_FORWARDED

    return CgRebaseStatus.CONFLICT

merge_discard_local async

merge_discard_local()
unconditionally fetch, then move main's ref directly onto

server's new tip (same as git reset --hard server--no new commit, and deliberately not a new commit with matching content either: a sibling commit here would leave git merge-base main server stuck at the old sync point instead of advancing to server's tip, making the next rebase()/merge_start() wrongly see "local changed" even though nothing would be, confirmed by direct testing--see push()'s docstring for the same reasoning). Any local commits main had are not deleted, just no longer reachable from main itself--recoverable via main's reflog for as long as it lasts. Unlike rebase(), doesn't check whether local actually diverged first--always overwrites. Instant--never touches MERGE_HEAD.

Raises:

  • FileNotFoundError

    if this working directory has never been imported/committed.

  • CgContributionManagerError

    if a merge is already in progress.

Source code in codingame_tools/contribution_manager/manager.py
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
async def merge_discard_local(self) -> CgContributionView:
    """Discard all local edits: unconditionally fetch, then move `main`'s ref directly onto
       `server`'s new tip (same as `git reset --hard server`--no new commit, and deliberately
       *not* a new commit with matching content either: a sibling commit here would leave
       `git merge-base main server` stuck at the old sync point instead of advancing to
       server's tip, making the next `rebase()`/`merge_start()` wrongly see "local changed"
       even though nothing would be, confirmed by direct testing--see `push()`'s docstring
       for the same reasoning). Any local commits `main` had are not deleted, just no longer
       reachable from `main` itself--recoverable via `main`'s reflog for as long as it lasts.
       Unlike `rebase()`, doesn't check whether local actually diverged first--always
       overwrites. Instant--never touches `MERGE_HEAD`.

    Raises:
        FileNotFoundError: if this working directory has never been imported/committed.
        CgContributionManagerError: if a merge is already in progress.
    """
    if self.merge_in_progress:
        raise CgContributionManagerError(
                "A merge is in progress (see `cg contribution merge continue`/`abort`)--"
                "resolve or abort it first."
            )
    await self.fetch()
    repo = self.git_repo
    server_sha = repo.resolve_ref(SERVER_BRANCH_NAME)
    if server_sha is None:
        raise FileNotFoundError(f"{self.git_dir} has no {SERVER_BRANCH_NAME} branch--nothing to discard local changes to.")
    repo.checkout_all(SERVER_BRANCH_NAME)
    repo.update_ref(f"refs/heads/{MAIN_BRANCH_NAME}", server_sha)
    _align_solution_file_name(self.contribution_dir, self.load().data.solution_language)
    return self.load()

merge_discard_server async

merge_discard_server()

Update server to reflect the current server state, without touching main/the working tree at all--just fetch() under a different name, kept as its own method for CLI-naming continuity with the old design (where last_committed/remote were distinct concepts this bridged; they no longer are).

Raises:

  • FileNotFoundError

    if this working directory has never been imported/committed.

  • CgContributionManagerError

    if a merge is already in progress.

Source code in codingame_tools/contribution_manager/manager.py
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
async def merge_discard_server(self) -> CgContribution:
    """Update `server` to reflect the current server state, without touching `main`/the
       working tree at all--just `fetch()` under a different name, kept as its own method for
       CLI-naming continuity with the old design (where `last_committed`/`remote` were
       distinct concepts this bridged; they no longer are).

    Raises:
        FileNotFoundError: if this working directory has never been imported/committed.
        CgContributionManagerError: if a merge is already in progress.
    """
    return await self.fetch()

merge_start async

merge_start()

Begin (or, if one's already in progress, do nothing and report it) a merge:

  1. fetch() (refuses if a merge is already in progress--checked first, so this never runs in that case).
  2. If server's tip already equals git merge-base main server, there's nothing to merge (CgMergeStartStatus.UP_TO_DATE).
  3. Otherwise, a real git merge server against the working tree. If it completes cleanly (including a trivial fast-forward), git has already committed the result-- merge_in_progress is False again, nothing more to do (except renormalizing tests/'s directory layout--see below--and re-generating the solution symlink). If it stops with conflicts, text_conflicts/binary_conflicts (split by content-- see _looks_like_text) list the affected paths; resolve them (by hand, or cg contribution merge interactive) and run merge_continue().

A clean merge's own auto-commit (from git itself) can leave tests/'s ordinal directories in a non-canonical layout (e.g. both sides added test cases using different numbering)--see push()'s docstring for why that matters for a stable round trip with server. So a clean merge here also renormalizes tests/ and folds any resulting rename into that same commit via restage_and_amend_if_dirty(), rather than leaving it for the next push() to silently fix up.

Raises:

  • FileNotFoundError

    if this working directory has never been imported/committed.

Source code in codingame_tools/contribution_manager/manager.py
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
async def merge_start(self) -> CgMergeStartResult:
    """Begin (or, if one's already in progress, do nothing and report it) a merge:

       1. `fetch()` (refuses if a merge is already in progress--checked first, so this never
          runs in that case).
       2. If `server`'s tip already equals `git merge-base main server`, there's nothing to
          merge (`CgMergeStartStatus.UP_TO_DATE`).
       3. Otherwise, a real `git merge server` against the working tree. If it completes
          cleanly (including a trivial fast-forward), git has already committed the result--
          `merge_in_progress` is `False` again, nothing more to do (except renormalizing
          `tests/`'s directory layout--see below--and re-generating the solution symlink).
          If it stops with conflicts, `text_conflicts`/`binary_conflicts` (split by content--
          see `_looks_like_text`) list the affected paths; resolve them (by hand, or `cg
          contribution merge interactive`) and run `merge_continue()`.

       A clean merge's own auto-commit (from git itself) can leave `tests/`'s ordinal
       directories in a non-canonical layout (e.g. both sides added test cases using
       different numbering)--see `push()`'s docstring for why that matters for a stable
       round trip with `server`. So a clean merge here also renormalizes `tests/` and folds
       any resulting rename into that same commit via `restage_and_amend_if_dirty()`, rather
       than leaving it for the next `push()` to silently fix up.

    Raises:
        FileNotFoundError: if this working directory has never been imported/committed.
    """
    if self.merge_in_progress:
        return CgMergeStartResult(status=CgMergeStartStatus.ALREADY_IN_PROGRESS)

    repo = self.git_repo
    await self.fetch()

    base = repo.merge_base(MAIN_BRANCH_NAME, SERVER_BRANCH_NAME)
    server_sha = repo.resolve_ref(SERVER_BRANCH_NAME)
    if server_sha == base:
        return CgMergeStartResult(status=CgMergeStartStatus.UP_TO_DATE)

    clean = repo.merge_branch(SERVER_BRANCH_NAME)
    if clean:
        renormalize_test_case_dirs(self.tests_dir)
        repo.restage_and_amend_if_dirty()
        _align_solution_file_name(self.contribution_dir, self.load().data.solution_language)
        return CgMergeStartResult(status=CgMergeStartStatus.STARTED)

    conflicts = repo.status_conflicts()
    text_conflicts: list[str] = []
    binary_conflicts: list[str] = []
    for rel_path in conflicts:
        content = (self.data_dir / rel_path).read_bytes() if (self.data_dir / rel_path).is_file() else b""
        (text_conflicts if _looks_like_text(content) else binary_conflicts).append(rel_path)
    return CgMergeStartResult(
            status=CgMergeStartStatus.STARTED,
            text_conflicts=tuple(text_conflicts),
            binary_conflicts=tuple(binary_conflicts),
        )

merge_continue

merge_continue()
stage everything (refusing first if a still-unmerged path

still has a leftover <<<<<<< marker--see CgGitRepo.merge_continue) and commit, then renormalize tests/'s ordinal directory layout--see push()'s docstring for why that matters for a stable round trip with server--folding any resulting rename into that same merge commit via restage_and_amend_if_dirty() (done after the merge commit exists, deliberately: renaming a conflicted-but-still-unresolved path before git's own unmerged-index-stage bookkeeping is resolved and committed would confuse status_conflicts(), which looks paths up by their pre-rename name). Refreshes the solution symlink afterward (a resolved contribution-data.json conflict may have changed solution_language).

Raises:

Source code in codingame_tools/contribution_manager/manager.py
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
def merge_continue(self) -> None:
    """Finish an in-progress merge: stage everything (refusing first if a still-unmerged path
       still has a leftover `<<<<<<<` marker--see `CgGitRepo.merge_continue`) and commit, then
       renormalize `tests/`'s ordinal directory layout--see `push()`'s docstring for why
       that matters for a stable round trip with `server`--folding any resulting rename into
       that same merge commit via `restage_and_amend_if_dirty()` (done *after* the merge
       commit exists, deliberately: renaming a conflicted-but-still-unresolved path before
       git's own unmerged-index-stage bookkeeping is resolved and committed would confuse
       `status_conflicts()`, which looks paths up by their pre-rename name). Refreshes the
       solution symlink afterward (a resolved `contribution-data.json` conflict may have
       changed `solution_language`).

    Raises:
        CgContributionManagerError: if no merge is in progress, or (wrapping git's own error)
                                     if unresolved conflict markers remain.
    """
    if not self.merge_in_progress:
        raise CgContributionManagerError("No merge in progress (run `cg contribution merge` to start one).")
    repo = self.git_repo
    try:
        repo.merge_continue()
    except CgGitError as e:
        raise CgContributionManagerError(str(e)) from e
    renormalize_test_case_dirs(self.tests_dir)
    repo.restage_and_amend_if_dirty()
    _align_solution_file_name(self.contribution_dir, self.load().data.solution_language)

merge_abort

merge_abort()
restore main's pre-merge working tree state and discard

MERGE_HEAD. server is left untouched--the merge never reached merge_continue(), so nothing about it was ever recorded anywhere.

Raises:

Source code in codingame_tools/contribution_manager/manager.py
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
def merge_abort(self) -> None:
    """Abort an in-progress merge: restore `main`'s pre-merge working tree state and discard
       `MERGE_HEAD`. `server` is left untouched--the merge never reached `merge_continue()`, so
       nothing about it was ever recorded anywhere.

    Raises:
        CgContributionManagerError: if no merge is in progress.
    """
    if not self.merge_in_progress:
        raise CgContributionManagerError("No merge in progress.")
    self.git_repo.merge_abort()
    _align_solution_file_name(self.contribution_dir, self.load().data.solution_language)

discard_local

discard_local()
reset this working directory's content to match server's

current tip exactly--purely local, no network access at all (unlike merge_discard_local(), which fetch()es fresh first--this uses whatever server already has). Resets both the index and working tree (via CgGitRepo.checkout_all, i.e. git read-tree --reset -u--git checkout <ref> -- . would not remove a file that exists locally but not in server's tree, confirmed by direct testing), without moving main's ref or creating a commit--if main had local commits beyond the last sync, this discards them from the working tree too (matching the old, since-renamed revert()'s "match the last synced state exactly" contract), but they remain recoverable via main's own history, since this never does a hard reset of the ref itself.

Named to match merge_discard_local()/merge_discard_server()'s existing "discard" vocabulary (all three answer "throw away one side and take the other," differing only in whether a merge is in progress and whether they fetch first)--deliberately not revert() (the original name), which collides with real git's very different meaning (a new commit that undoes a past one, preserving history)--and not bare discard(), which reads as "discard the whole contribution" rather than "discard my local edits."

Raises:

  • FileNotFoundError

    if this working directory has never been imported/committed.

  • CgContributionManagerError

    if a merge is in progress.

Source code in codingame_tools/contribution_manager/manager.py
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
def discard_local(self) -> CgContributionView:
    """Discard local edits: reset this working directory's content to match `server`'s
       current tip exactly--purely local, no network access at all (unlike
       `merge_discard_local()`, which `fetch()`es fresh first--this uses whatever `server`
       already has). Resets both the index and working tree (via `CgGitRepo.checkout_all`,
       i.e. `git read-tree --reset -u`--`git checkout <ref> -- .` would *not* remove a file
       that exists locally but not in `server`'s tree, confirmed by direct testing), without
       moving `main`'s ref or creating a commit--if `main` had local commits beyond the last
       sync, this discards them from the working tree too (matching the old, since-renamed
       `revert()`'s "match the last synced state exactly" contract), but they remain
       recoverable via `main`'s own history, since this never does a hard reset of the ref
       itself.

       Named to match `merge_discard_local()`/`merge_discard_server()`'s existing "discard"
       vocabulary (all three answer "throw away one side and take the other," differing only
       in whether a merge is in progress and whether they fetch first)--deliberately not
       `revert()` (the original name), which collides with real git's very different meaning
       (a new commit that undoes a past one, preserving history)--and not bare `discard()`,
       which reads as "discard the whole contribution" rather than "discard my local edits."

    Raises:
        FileNotFoundError: if this working directory has never been imported/committed.
        CgContributionManagerError: if a merge is in progress.
    """
    if self.merge_in_progress:
        raise CgContributionManagerError(
                "A merge is in progress (see `cg contribution merge continue`/`abort`)--"
                "resolve or abort it before discarding local edits."
            )
    repo = self.git_repo
    if repo.resolve_ref(SERVER_BRANCH_NAME) is None:
        raise FileNotFoundError(f"{self.git_dir} has no {SERVER_BRANCH_NAME} branch--nothing to discard to.")
    repo.checkout_all(SERVER_BRANCH_NAME)
    _align_solution_file_name(self.contribution_dir, self.load().data.solution_language)
    return self.load()

delete async

delete(*, keep_local=False, keep_server=False)

Delete this contribution from the server (Contribution/deleteContribution-- unrecoverable), then remove this entire working directory (the default)--or, with keep_local, detach it instead: drop the server/version-data branches and reset contribution.json's contribution_handle back to None, leaving a purely local working directory in exactly the state create() would have left it in, ready for its current content to be pushed as a brand new contribution on the next push() (see push()'s create-vs-update docstring)--e.g. for using an existing contribution as a template for a new one.

keep_server skips the server-side deletion entirely (nothing sent to deleteContribution) and just removes this working directory--for when you only want to stop tracking a contribution locally without touching it on the server. Mutually exclusive with keep_local (together they'd mean "delete nothing," which isn't a delete() at all).

A working directory that was create()d but never successfully push()d has no server-side contribution at all (contribution.json's contribution_handle is None--the authoritative signal here, same as push()'s create-vs-update decision; see that method's docstring for why this is trusted over the server git branch's mere existence)--by default (neither keep_local nor keep_server), that's not an error: there's simply nothing to send to deleteContribution, so this just removes the local working directory, same as it would for any other directory. keep_local and keep_server each DO require a real contribution_handle to exist, though, and raise if not--both are explicit statements about server state (keep_local: "detach from the thing I'm currently tracking"; keep_server: "leave the thing I'm currently tracking alone") that don't make sense to honor silently as no-ops when there's nothing being tracked yet.

main and its commit history (including anything reachable only via the old server/version-data branches, by SHA, until a real git gc eventually collects it) are left untouched by keep_local; only the branches/identity that pointed at the now-deleted contribution are affected.

No confirmation prompt here--that's the CLI's job (cg contribution delete), not this class's (matches every other method here: no interactive behavior, ever).

Raises:

  • FileNotFoundError

    if this working directory has never been created/imported, or (only with keep_local or keep_server) has no contribution_handle yet (create()d but never successfully push()d).

  • CgContributionManagerError

    if a merge is in progress, or both keep_local and keep_server are set.

Source code in codingame_tools/contribution_manager/manager.py
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
async def delete(self, *, keep_local: bool = False, keep_server: bool = False) -> None:
    """Delete this contribution from the server (`Contribution/deleteContribution`--
       unrecoverable), then remove this entire working directory (the default)--or, with
       `keep_local`, detach it instead: drop the `server`/`version-data` branches and reset
       `contribution.json`'s `contribution_handle` back to `None`, leaving a purely local
       working directory in exactly the state `create()` would have left it in, ready for its
       *current* content to be pushed as a brand new contribution on the next `push()` (see
       `push()`'s create-vs-update docstring)--e.g. for using an existing contribution as a
       template for a new one.

       `keep_server` skips the server-side deletion entirely (nothing sent to
       `deleteContribution`) and just removes this working directory--for when you only want
       to stop tracking a contribution locally without touching it on the server. Mutually
       exclusive with `keep_local` (together they'd mean "delete nothing," which isn't a
       `delete()` at all).

       A working directory that was `create()`d but never successfully `push()`d has no
       server-side contribution at all (`contribution.json`'s `contribution_handle` is
       `None`--the authoritative signal here, same as `push()`'s create-vs-update decision;
       see that method's docstring for why this is trusted over the `server` git branch's
       mere existence)--by default (neither `keep_local` nor `keep_server`), that's not an
       error: there's simply nothing to send to `deleteContribution`, so this just removes the
       local working directory, same as it would for any other directory. `keep_local` and
       `keep_server` each DO require a real `contribution_handle` to exist, though, and raise
       if not--both are explicit statements about server state (`keep_local`: "detach from the
       thing I'm currently tracking"; `keep_server`: "leave the thing I'm currently tracking
       alone") that don't make sense to honor silently as no-ops when there's nothing being
       tracked yet.

       `main` and its commit history (including anything reachable only via the old
       `server`/`version-data` branches, by SHA, until a real `git gc` eventually collects it)
       are left untouched by `keep_local`; only the branches/identity that pointed at the
       now-deleted contribution are affected.

       No confirmation prompt here--that's the CLI's job (`cg contribution delete`), not this
       class's (matches every other method here: no interactive behavior, ever).

    Raises:
        FileNotFoundError: if this working directory has never been created/imported, or (only
                            with `keep_local` or `keep_server`) has no `contribution_handle`
                            yet (`create()`d but never successfully `push()`d).
        CgContributionManagerError: if a merge is in progress, or both `keep_local` and
                                     `keep_server` are set.
    """
    if keep_local and keep_server:
        raise CgContributionManagerError(
                "keep_local and keep_server are mutually exclusive--together they'd mean "
                "deleting nothing at all."
            )
    if self.merge_in_progress:
        raise CgContributionManagerError(
                "A merge is in progress (see `cg contribution merge continue`/`abort`)--"
                "resolve or abort it before deleting."
            )
    identity = self.load_identity()
    assert identity is not None  # merge_in_progress above already required a loadable git_dir
    contribution_handle = identity.contribution_handle
    if contribution_handle is None and keep_local:
        raise FileNotFoundError(
                f"{self.identity_file} has no contribution_handle--nothing to detach from "
                "(this working directory was create()d but never successfully pushed)."
            )
    if contribution_handle is None and keep_server:
        raise FileNotFoundError(
                f"{self.identity_file} has no contribution_handle--nothing server-side for "
                "keep_server to leave alone (this working directory was create()d but never "
                "successfully pushed). Omit keep_server to just remove the local working "
                "directory."
            )
    if contribution_handle is not None and not keep_server:
        await self.client.services.contribution.delete_contribution(contribution_handle)

    if keep_local:
        repo = self.git_repo
        repo.delete_ref(f"refs/heads/{SERVER_BRANCH_NAME}")
        repo.delete_ref(f"refs/heads/{VERSION_DATA_BRANCH_NAME}")
        self._write_contribution_handle(None)
    else:
        # See CgPuzzleManager.delete: a containerized language leaves a container bind-mounted
        # to this directory, and its name derives from the path--orphaning one means a future
        # working directory at the same path silently attaches to it.
        await remove_containers_for_root(self.contribution_dir)
        shutil.rmtree(self.contribution_dir)

status async

status(*, remote=False)

A point-in-time summary of this working directory--see CgContributionStatus.

By default, entirely local/cheap: no network access at all--server/ moderator_approvals/moderator_denials (if this working directory has ever been pushed) come straight from .meta/contribution-status.json (see read_status_cache), refreshed on some earlier fetch()/import_()/repair() call. Pass remote=True to fetch() fresh first (same tradeoff as cg contribution diff --remote)--skipped automatically if this working directory has never been pushed (nothing to fetch) or a merge is in progress (fetching mid-merge is refused by fetch() itself); fetch() unconditionally refreshes that cache file (see its docstring), so this always reflects whatever fetch() just saw.

Parameters:

  • remote (bool, default: False ) –

    If True, fetch() fresh from the server before reporting--otherwise reports whatever .meta/contribution-status.json last cached (possibly stale, or entirely absent if this working directory has never been fetched under a version of this package new enough to write it). Defaults to False.

Raises:

  • FileNotFoundError

    if this working directory has never been imported/created.

Source code in codingame_tools/contribution_manager/manager.py
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
async def status(self, *, remote: bool = False) -> CgContributionStatus:
    """A point-in-time summary of this working directory--see `CgContributionStatus`.

       By default, entirely local/cheap: no network access at all--`server`/
       `moderator_approvals`/`moderator_denials` (if this working directory has ever been
       pushed) come straight from `.meta/contribution-status.json` (see `read_status_cache`),
       refreshed on some earlier `fetch()`/`import_()`/`repair()` call. Pass `remote=True` to
       `fetch()` fresh first (same tradeoff as `cg contribution diff --remote`)--skipped
       automatically if this working directory has never been pushed (nothing to fetch) or a
       merge is in progress (fetching mid-merge is refused by `fetch()` itself); `fetch()`
       unconditionally refreshes that cache file (see its docstring), so this always reflects
       whatever `fetch()` just saw.

    Args:
        remote: If True, `fetch()` fresh from the server before reporting--otherwise reports
                whatever `.meta/contribution-status.json` last cached (possibly stale, or
                entirely absent if this working directory has never been fetched under a
                version of this package new enough to write it). Defaults to False.

    Raises:
        FileNotFoundError: if this working directory has never been imported/created.
    """
    identity = self.load_identity()
    if identity is None:
        raise FileNotFoundError(f"{self.identity_file} does not exist--this working directory has never been imported/created.")
    merge_in_progress = self.merge_in_progress
    if remote and not merge_in_progress and identity.contribution_handle is not None:
        await self.fetch()

    repo = self.git_repo
    view = self.load()
    local_title = view.data.title
    local_dirty = False if merge_in_progress else bool(repo.diff_name_status(MAIN_BRANCH_NAME))

    server_sha = repo.resolve_ref(SERVER_BRANCH_NAME)
    metadata = self.server_metadata()
    status_cache = self.read_status_cache()
    server_contribution = status_cache.contribution if status_cache is not None else None
    moderator_approvals = status_cache.moderator_approvals if status_cache is not None else None
    moderator_denials = status_cache.moderator_denials if status_cache is not None else None
    status_cache_refreshed_at = status_cache.refreshed_at if status_cache is not None else None

    sync_status: CgContributionSyncStatus
    if merge_in_progress:
        sync_status = CgContributionSyncStatus.MERGE_IN_PROGRESS
    elif server_sha is None:
        sync_status = CgContributionSyncStatus.NOT_PUSHED
    else:
        base = repo.merge_base(MAIN_BRANCH_NAME, SERVER_BRANCH_NAME)
        main_sha = repo.resolve_ref(MAIN_BRANCH_NAME)
        server_changed = server_sha != base
        local_changed = local_dirty or main_sha != base
        if server_changed and local_changed:
            sync_status = CgContributionSyncStatus.DIVERGED
        elif server_changed:
            sync_status = CgContributionSyncStatus.SERVER_AHEAD
        elif local_changed:
            sync_status = CgContributionSyncStatus.LOCAL_AHEAD
        else:
            sync_status = CgContributionSyncStatus.UP_TO_DATE

    return CgContributionStatus(
            contribution_dir=self.contribution_dir,
            pushed=identity.contribution_handle is not None,
            contribution_handle=identity.contribution_handle,
            local_title=local_title,
            local_dirty=local_dirty,
            merge_in_progress=merge_in_progress,
            sync_status=sync_status,
            local_version=metadata.version if metadata is not None else None,
            local_draft=view.draft,
            local_ready_for_moderation=view.ready_for_moderation,
            local_puzzle_type=view.puzzle_type,
            local_solution_language=view.data.solution_language,
            local_difficulty=view.data.difficulty,
            server=server_contribution,
            moderator_approvals=moderator_approvals,
            moderator_denials=moderator_denials,
            status_cache_refreshed_at=status_cache_refreshed_at,
        )

list_local_tests

list_local_tests(ordinals=None, *, local=True, validator=True)

Enumerate tests/ (see codingame_tools.contribution_manager.test_cases_dir. list_local_test_cases), optionally filtered.

Parameters:

  • ordinals (list[str] | None, default: None ) –

    If given, only test cases whose ordinal matches one of these (by exact string match, or--if both sides are purely numeric--by numeric equality, so "1" matches ordinal directory "01"). Defaults to every ordinal.

  • local (bool, default: True ) –

    Include "local"-side test cases. Defaults to True.

  • validator (bool, default: True ) –

    Include "validator"-side test cases. Defaults to True.

Returns:

Source code in codingame_tools/contribution_manager/manager.py
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
def list_local_tests(
            self,
            ordinals: list[str] | None = None,
            *,
            local: bool = True,
            validator: bool = True,
        ) -> list[CgContributionLocalTestCase]:
    """Enumerate `tests/` (see `codingame_tools.contribution_manager.test_cases_dir.
       list_local_test_cases`), optionally filtered.

    Args:
        ordinals:  If given, only test cases whose ordinal matches one of these (by exact
                   string match, or--if both sides are purely numeric--by numeric equality,
                   so `"1"` matches ordinal directory `"01"`). Defaults to every ordinal.
        local:     Include `"local"`-side test cases. Defaults to True.
        validator: Include `"validator"`-side test cases. Defaults to True.

    Returns:
        Matching test cases, in the same order `list_local_test_cases` returns them.
    """
    test_cases = list_local_test_cases(self.tests_dir)
    if ordinals is not None:
        test_cases = [tc for tc in test_cases if any(_ordinal_matches(o, tc.ordinal) for o in ordinals)]
    if not local:
        test_cases = [tc for tc in test_cases if tc.side != "local"]
    if not validator:
        test_cases = [tc for tc in test_cases if tc.side != "validator"]
    return test_cases

language_context

language_context(solution_language=None, *, mount_root=None)

Describe this working directory to codingame_tools.language--see CgLanguageContext.

mount_root is what a containerized language bind-mounts. It defaults to the enclosing VS Code workspace root, so that in-container paths equal host paths and one container serves the whole workspace; pass it explicitly (VS Code's ${workspaceFolder}) when the real workspace is known, since find_workspace_root is only a guess.

Infallible by design: never requires this directory to have been imported (meta_dir falls back to the non-data/ layout when there's no contribution.json to say otherwise). solution_language is accepted for signature stability but no longer selects a path: there is one real solution file and solution_file finds it whatever extension it carries.

Source code in codingame_tools/contribution_manager/manager.py
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
def language_context(
            self,
            solution_language: CgSolutionLanguage | None = None,
            *,
            mount_root: Path | None = None,
        ) -> CgLanguageContext:
    """Describe this working directory to `codingame_tools.language`--see `CgLanguageContext`.

       `mount_root` is what a containerized language bind-mounts. It defaults to the enclosing
       VS Code workspace root, so that in-container paths equal host paths and one container
       serves the whole workspace; pass it explicitly (VS Code's `${workspaceFolder}`) when the
       real workspace is known, since `find_workspace_root` is only a guess.

       Infallible by design: never requires this directory to have been imported (`meta_dir`
       falls back to the non-`data/` layout when there's no `contribution.json` to say
       otherwise). `solution_language` is accepted for signature stability but no longer selects
       a path: there is one real solution file and `solution_file` finds it whatever extension
       it carries.
    """
    return CgLanguageContext(
            root=self.contribution_dir,
            solution_file=self.solution_file,
            meta_dir=self.meta_dir,
            toolchain_dir=self.toolchain_dir,
            mount_root=mount_root or self.mount_root or find_workspace_root(self.contribution_dir),
            toolchain_languages=self.toolchain_languages,
            toolchain_image=self.toolchain_image,
        )

load_selected_test

load_selected_test()

The explicitly selected test case, or None if none has been chosen.

Source code in codingame_tools/contribution_manager/manager.py
2218
2219
2220
2221
2222
def load_selected_test(self) -> CgContributionSelectedTest | None:
    """The explicitly selected test case, or None if none has been chosen."""
    if not self.selected_test_file.is_file():
        return None
    return CgContributionSelectedTest.load(self.selected_test_file)

select_test

select_test(ordinal, side)

Choose which test case the debugger runs against.

Raises:

Source code in codingame_tools/contribution_manager/manager.py
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
def select_test(self, ordinal: str, side: str) -> None:
    """Choose which test case the debugger runs against.

    Raises:
        CgContributionManagerError: if no test case matches, so a typo surfaces now rather than
                                     when a debug session fails to start.
    """
    matching = self.list_local_tests(
            [ordinal], local=side == "local", validator=side == "validator")
    if not matching:
        available = ", ".join(f"{tc.ordinal}/{tc.side}" for tc in self.list_local_tests()) or "(none)"
        raise CgContributionManagerError(
                f"No {side} test case with ordinal {ordinal!r}. Available: {available}.")
    self.meta_dir.mkdir(parents=True, exist_ok=True)
    CgContributionSelectedTest(ordinal=matching[0].ordinal, side=side).save(self.selected_test_file)

clear_selected_test

clear_selected_test()

Forget the explicit selection, falling back to the default (the first local test).

Source code in codingame_tools/contribution_manager/manager.py
2240
2241
2242
def clear_selected_test(self) -> None:
    """Forget the explicit selection, falling back to the default (the first local test)."""
    self.selected_test_file.unlink(missing_ok=True)

resolve_debug_test

resolve_debug_test()

Which single test a debug session should use: the selection, else the first local test.

Local rather than merely first: validators are the hidden, scoring cases, and landing in a debugger on one by default would be surprising. Falls back to the first test of any side only if there are no local ones at all.

Raises:

Source code in codingame_tools/contribution_manager/manager.py
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
def resolve_debug_test(self) -> CgContributionLocalTestCase:
    """Which single test a debug session should use: the selection, else the first *local* test.

       Local rather than merely first: validators are the hidden, scoring cases, and landing in
       a debugger on one by default would be surprising. Falls back to the first test of any
       side only if there are no local ones at all.

    Raises:
        CgContributionManagerError: if `tests/` holds no test cases.
    """
    all_tests = self.list_local_tests()
    if not all_tests:
        raise CgContributionManagerError(f"No test cases in {self.tests_dir}.")
    selected = self.load_selected_test()
    if selected is not None:
        for test_case in all_tests:
            if test_case.ordinal == selected.ordinal and test_case.side == selected.side:
                return test_case
    locals_first = [tc for tc in all_tests if tc.side == "local"]
    return (locals_first or all_tests)[0]

load_solution_snapshot

load_solution_snapshot()

The starter stub this client last generated into data/solution.src, or None if there isn't one (never generated, or .meta/ predates the snapshot).

Source code in codingame_tools/contribution_manager/manager.py
2265
2266
2267
2268
2269
2270
def load_solution_snapshot(self) -> CgContributionSolutionSnapshot | None:
    """The starter stub this client last generated into `data/solution.src`, or `None` if there
       isn't one (never generated, or `.meta/` predates the snapshot)."""
    if not self.solution_snapshot_file.is_file():
        return None
    return CgContributionSolutionSnapshot.load(self.solution_snapshot_file)

set_language async

set_language(language, *, force=False)

Switch this contribution's reference-solution language, writing a fresh starter stub.

This is destructive in a way the puzzle equivalent is not, and deliberately harder to do by accident. A contribution stores exactly one solution server-side, with no per-language history--unlike a puzzle, where CodinGame keeps your latest source for each language and switching is reversible (see CgTestSessionService.get_previous_code_by_language_id). Here there is nothing to restore and nothing to switch back to: the existing solution is replaced by a stub, and the last durable copy is overwritten as soon as the next push() lands.

So the only non-destructive case is "data/solution.src is still exactly the stub this client generated" (see _solution_is_generated_stub). Notably, matching what the server currently has does not count as safe, unlike the puzzle version--the server copy is precisely what the next push destroys.

Purely local: no network call, because there is no per-language code to fetch.

Parameters:

  • language (CgSolutionLanguage) –

    CodinGame language ID to switch to, e.g. "C++" (see CgSolutionLanguage).

  • force (bool, default: False ) –

    Switch even though a real reference solution would be discarded.

Returns:

Raises:

  • FileNotFoundError

    if this working directory hasn't been imported/initialized.

  • CgContributionManagerError

    if language isn't one this client knows, if it's already the current language, or if a real solution would be lost and force is False.

Source code in codingame_tools/contribution_manager/manager.py
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
async def set_language(
            self,
            language: CgSolutionLanguage,
            *,
            force: bool = False,
        ) -> CgContributionSetLanguageResult:
    """Switch this contribution's reference-solution language, writing a fresh starter stub.

       **This is destructive in a way the puzzle equivalent is not, and deliberately harder to
       do by accident.** A contribution stores exactly one solution server-side, with no
       per-language history--unlike a puzzle, where CodinGame keeps your latest source for each
       language and switching is reversible (see
       `CgTestSessionService.get_previous_code_by_language_id`). Here there is nothing to
       restore and nothing to switch back to: the existing solution is replaced by a stub, and
       the last durable copy is overwritten as soon as the next `push()` lands.

       So the only non-destructive case is "`data/solution.src` is still exactly the stub this
       client generated" (see `_solution_is_generated_stub`). Notably, *matching what the server
       currently has* does **not** count as safe, unlike the puzzle version--the server copy is
       precisely what the next push destroys.

       Purely local: no network call, because there is no per-language code to fetch.

    Args:
        language: CodinGame language ID to switch to, e.g. "C++" (see `CgSolutionLanguage`).
        force:    Switch even though a real reference solution would be discarded.

    Returns:
        A `CgContributionSetLanguageResult`--`wrote_stub` is False when the new language has no
        stub to offer and `solution.src` was removed instead.

    Raises:
        FileNotFoundError: if this working directory hasn't been imported/initialized.
        CgContributionManagerError: if `language` isn't one this client knows, if it's already
                                     the current language, or if a real solution would be lost
                                     and `force` is False.
    """
    view = self.load()
    previous_language = view.data.solution_language
    if language not in list_language_cg_ids():
        raise CgContributionManagerError(
                f"{language!r} isn't a language this client knows. Known languages: "
                f"{', '.join(list_language_cg_ids())}."
            )
    if language == previous_language:
        raise CgContributionManagerError(
                f"{self.contribution_dir} is already using {language!r}--nothing to switch."
            )
    if not force and not self._solution_is_generated_stub(previous_language):
        raise CgContributionManagerError(
                f"{self.solution_file} holds a real {previous_language!r} reference solution. "
                "A contribution stores only ONE solution, with no per-language history, so "
                "switching replaces it with a starter stub and the next `cg contribution push` "
                "overwrites the last durable copy--there is nothing to switch back to. Save it "
                "somewhere outside this working directory first, then pass --force."
            )

    # A language with no stub yields None, written as an *empty* solution.src rather than no
    # file at all--empty is this client's spelling of a null solutionSource (see
    # _read_local_data), so `push()` still skips solution validation while the author keeps a
    # file to type into and a symlink that resolves. Writing a comment-only placeholder instead
    # would be non-null, fail validation, and block the push--see
    # CgLanguage.build_contribution_create_stub_source.
    stub = await get_language(language).build_contribution_create_stub_source()
    _write_sidecar(self.solution_file, stub or "")
    self._write_solution_snapshot(language, stub)
    self.save(dataclasses.replace(
            view, data=dataclasses.replace(view.data, solution_language=language)))
    _align_solution_file_name(self.contribution_dir, language)
    return CgContributionSetLanguageResult(
            language=language, previous_language=previous_language, wrote_stub=stub is not None,
        )

provision_vscode async

provision_vscode(solution_language, *, workspace_root=None, force=False, check=False, debug_adapter_logging=False)

Generate this working directory's VS Code run/debug configuration, if solution_language has any, and write it into the workspace.

What's generated is the same for every working directory of that language, so this is run once per language rather than once per directory, and nothing here goes stale when test cases or the solution language change.

Parameters:

  • solution_language (CgSolutionLanguage) –

    The language data/solution.src is written in.

  • workspace_root (Path | None, default: None ) –

    Where .vscode/ goes. Defaults to find_workspace_root()--VS Code reads launch.json only from the workspace root, which is often not this working directory (see codingame_tools.language.vscode).

  • force (bool, default: False ) –

    Overwrite an existing config file that isn't strict JSON (i.e. uses JSONC comments) instead of refusing.

  • debug_adapter_logging (bool, default: False ) –
                Generate a configuration that logs the debug adapter's own
                 protocol exchange--see `CgVsCodeRequest`.
    
  • check (bool, default: False ) –

    Report what would change without touching anything. This is how staleness is detected: generated entries carry no version stamp, so "would rewriting change anything?" is the whole question, and it stays correct when a future release alters what gets generated.

Returns:

  • list[Path]

    Every path that changed (or, under check, would change), in write order. Empty means

  • list[Path]

    already up to date, or that this language has no VS Code integration.

Raises:

  • CgVsCodeMergeError

    if an existing config file can't be safely merged into.

Source code in codingame_tools/contribution_manager/manager.py
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
async def provision_vscode(
            self,
            solution_language: CgSolutionLanguage,
            *,
            workspace_root: Path | None = None,
            force: bool = False,
            check: bool = False,
            debug_adapter_logging: bool = False,
        ) -> list[Path]:
    """Generate this working directory's VS Code run/debug configuration, if `solution_language`
       has any, and write it into the workspace.

       What's generated is the same for every working directory of that language, so this is run
       once per language rather than once per directory, and nothing here goes stale when test
       cases or the solution language change.

    Args:
        solution_language: The language `data/solution.src` is written in.
        workspace_root:    Where `.vscode/` goes. Defaults to `find_workspace_root()`--VS Code
                            reads `launch.json` only from the workspace *root*, which is often
                            not this working directory (see `codingame_tools.language.vscode`).
        force:             Overwrite an existing config file that isn't strict JSON (i.e. uses
                            JSONC comments) instead of refusing.
        debug_adapter_logging:
                            Generate a configuration that logs the debug adapter's own
                             protocol exchange--see `CgVsCodeRequest`.
        check:             Report what *would* change without touching anything. This is how
                            staleness is detected: generated entries carry no version stamp, so
                            "would rewriting change anything?" is the whole question, and it
                            stays correct when a future release alters what gets generated.

    Returns:
        Every path that changed (or, under `check`, would change), in write order. Empty means
        already up to date, or that this language has no VS Code integration.

    Raises:
        CgVsCodeMergeError: if an existing config file can't be safely merged into.
    """
    resolved_workspace_root = (
            Path(workspace_root).resolve() if workspace_root is not None
            else find_workspace_root(self.contribution_dir)
        )
    request = CgVsCodeRequest(
            ctx=self.language_context(
                    solution_language, mount_root=resolved_workspace_root),
            workspace_root=resolved_workspace_root,
            debug_adapter_logging=debug_adapter_logging,
        )
    provisioning = await get_language(solution_language).build_vscode_provisioning(request)
    if provisioning is None:
        return []
    return write_provisioning(
            provisioning, root=self.contribution_dir,
            workspace_root=resolved_workspace_root, language=solution_language, force=force, dry_run=check)

start_debug_session async

start_debug_session(solution_language, ordinal, side, *, timeout=DEFAULT_BUILD_TIMEOUT_SECONDS)

Get the solution ready for a debugger to attach to, fed by the given test case's input-- see codingame_tools.language.CgLanguage.start_debug_session.

Only meaningful for a language whose debugger attaches to a running target (C++ via gdbserver). Python3's debugger launches the program itself, so it never calls this.

Raises:

  • CgContributionManagerError

    if no test case matches ordinal/side.

  • CgLanguageOperationNotSupportedError

    if this language has no attach-style debugging.

Source code in codingame_tools/contribution_manager/manager.py
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
async def start_debug_session(
            self,
            solution_language: CgSolutionLanguage,
            ordinal: str,
            side: str,
            *,
            timeout: float = DEFAULT_BUILD_TIMEOUT_SECONDS,
        ) -> CgDebugSession:
    """Get the solution ready for a debugger to attach to, fed by the given test case's input--
       see `codingame_tools.language.CgLanguage.start_debug_session`.

       Only meaningful for a language whose debugger attaches to a running target (C++ via
       gdbserver). Python3's debugger launches the program itself, so it never calls this.

    Raises:
        CgContributionManagerError: if no test case matches `ordinal`/`side`.
        CgLanguageOperationNotSupportedError: if this language has no attach-style debugging.
    """
    matching = self.list_local_tests(
            [ordinal], local=side == "local", validator=side == "validator")
    if not matching:
        raise CgContributionManagerError(
                f"No {side} test case with ordinal {ordinal!r} under {self.tests_dir}.")
    ctx = self.language_context(solution_language)
    # `input_text`, not `input_file`: the file carries a final newline this client added,
    # which isn't part of the value and must not reach the solution--see common.text_files.
    return await get_language(solution_language).start_debug_session(
            ctx, matching[0].input_text, timeout=timeout)

stop_debug_session async

stop_debug_session(solution_language)

Tear down whatever start_debug_session() started. Safe to call when nothing is running.

Source code in codingame_tools/contribution_manager/manager.py
2448
2449
2450
2451
2452
async def stop_debug_session(self, solution_language: CgSolutionLanguage) -> None:
    """Tear down whatever `start_debug_session()` started. Safe to call when nothing is
       running."""
    await get_language(solution_language).stop_debug_session(
            self.language_context(solution_language))

build_solution async

build_solution(solution_language, *, profile='run', timeout=DEFAULT_BUILD_TIMEOUT_SECONDS)

Build data/solution.src for local execution, if solution_language needs building at all (Python3 doesn't--this is then an immediate no-op success).

A separate step from run_local_test() so a caller can display build diagnostics apart from program output, report a compile error once rather than once per test case, and give building its own generous timeout. Cheap to call repeatedly: an unchanged source since the last successful build returns up_to_date=True having done nothing.

run_local_tests() calls this for you. A caller driving run_local_test() itself (as cg contribution play does, to stream results) must call this first.

Returns:

  • CgBuildResult

    A CgBuildResult--check .ok; a build failure is reported, never raised.

Source code in codingame_tools/contribution_manager/manager.py
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
async def build_solution(
            self,
            solution_language: CgSolutionLanguage,
            *,
            profile: CgBuildProfile = "run",
            timeout: float = DEFAULT_BUILD_TIMEOUT_SECONDS,
        ) -> CgBuildResult:
    """Build `data/solution.src` for local execution, if `solution_language` needs building at
       all (Python3 doesn't--this is then an immediate no-op success).

       A separate step from `run_local_test()` so a caller can display build diagnostics apart
       from program output, report a compile error once rather than once per test case, and give
       building its own generous timeout. Cheap to call repeatedly: an unchanged source since the
       last successful build returns `up_to_date=True` having done nothing.

       `run_local_tests()` calls this for you. A caller driving `run_local_test()` itself (as
       `cg contribution play` does, to stream results) must call this first.

    Returns:
        A `CgBuildResult`--check `.ok`; a build failure is reported, never raised.
    """
    ctx = self.language_context(solution_language)
    return await get_language(solution_language).build(ctx, profile=profile, timeout=timeout)

run_local_test async

run_local_test(test_case, solution_language, *, update_expected=False, timeout=DEFAULT_RUN_TIMEOUT_SECONDS)

Run data/solution.src against one local test case's input, entirely locally--no network access at all--by shelling out to the appropriate interpreter/compiler as a subprocess (see codingame_tools.language.CgLanguage.run).

Never raises just because the test failed (crashed, timed out, or mismatched)--that's reflected in the returned result's passed, for a caller running a batch of these to collect and report on afterward (see cg contribution play, which is also where "a test raising an unexpected exception" is caught and turned into a result with exception set--this method itself doesn't do that, since it only ever runs one test).

Does not build. For a language that needs compiling, call build_solution() first (run_local_tests() does this for you); this method only runs the already-built artifact.

Parameters:

  • test_case (CgContributionLocalTestCase) –

    Which test case to run (see list_local_tests).

  • solution_language (CgSolutionLanguage) –

    The language to run data/solution.src as (see CgContributionView.data.solution_language).

  • update_expected (bool, default: False ) –

    If True, overwrite test_case.output_file with the solution's actual output instead of comparing against it--for accepting the solution's current behavior as the new known-good baseline. Only written if the run completed without crashing/timing out.

  • timeout (float, default: DEFAULT_RUN_TIMEOUT_SECONDS ) –

    Wall-clock timeout in seconds--see codingame_tools.language. DEFAULT_RUN_TIMEOUT_SECONDS.

Returns:

Raises:

  • CgLanguageOperationNotSupportedError

    if solution_language isn't yet supported by codingame_tools.language.

Source code in codingame_tools/contribution_manager/manager.py
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
async def run_local_test(
            self,
            test_case: CgContributionLocalTestCase,
            solution_language: CgSolutionLanguage,
            *,
            update_expected: bool = False,
            timeout: float = DEFAULT_RUN_TIMEOUT_SECONDS,
        ) -> CgContributionLocalTestResult:
    """Run `data/solution.src` against one local test case's input, entirely locally--no
       network access at all--by shelling out to the appropriate interpreter/compiler as a
       subprocess (see `codingame_tools.language.CgLanguage.run`).

       Never raises just because the test failed (crashed, timed out, or mismatched)--that's
       reflected in the returned result's `passed`, for a caller running a batch of these to
       collect and report on afterward (see `cg contribution play`, which is also where
       "a test raising an unexpected exception" is caught and turned into a result with
       `exception` set--this method itself doesn't do that, since it only ever runs one test).

       **Does not build.** For a language that needs compiling, call `build_solution()` first
       (`run_local_tests()` does this for you); this method only runs the already-built
       artifact.

    Args:
        test_case:         Which test case to run (see `list_local_tests`).
        solution_language: The language to run `data/solution.src` as (see
                            `CgContributionView.data.solution_language`).
        update_expected:   If True, overwrite `test_case.output_file` with the solution's
                            actual output instead of comparing against it--for accepting the
                            solution's current behavior as the new known-good baseline. Only
                            written if the run completed without crashing/timing out.
        timeout:            Wall-clock timeout in seconds--see `codingame_tools.language.
                            DEFAULT_RUN_TIMEOUT_SECONDS`.

    Returns:
        The outcome--see `CgContributionLocalTestResult`.

    Raises:
        CgLanguageOperationNotSupportedError: if `solution_language` isn't yet supported by
                                               `codingame_tools.language`.
    """
    ctx = self.language_context(solution_language)
    run_result = await get_language(solution_language).run(
            ctx, test_case.input_text, timeout=timeout)
    ok = not run_result.timed_out and run_result.returncode == 0
    if update_expected:
        if ok:
            test_case.output_file.write_text(run_result.output, encoding="utf-8")
        expected_output = run_result.output if ok else test_case.output_text
        return CgContributionLocalTestResult(
                ordinal=test_case.ordinal, side=test_case.side, title=test_case.title,
                passed=ok, updated=ok, input=test_case.input_text,
                expected_output=expected_output, actual_output=run_result.output,
                stderr=run_result.stderr, timed_out=run_result.timed_out,
                returncode=run_result.returncode,
            )
    passed = ok and outputs_match(run_result.output, test_case.output_text)
    return CgContributionLocalTestResult(
            ordinal=test_case.ordinal, side=test_case.side, title=test_case.title,
            passed=passed, updated=False, input=test_case.input_text,
            expected_output=test_case.output_text, actual_output=run_result.output,
            stderr=run_result.stderr, timed_out=run_result.timed_out,
            returncode=run_result.returncode,
        )

run_local_tests async

run_local_tests(test_cases, solution_language, *, update_expected=False, timeout=DEFAULT_RUN_TIMEOUT_SECONDS, build_timeout=DEFAULT_BUILD_TIMEOUT_SECONDS)
call build_solution() once, then run every test case in

test_cases (e.g. from list_local_tests) via run_local_test, and raise if any failed--for programmatic callers that just want a pass/fail outcome without cg contribution play's own interleaved per-test console output (which needs its own loop, to catch and continue past an unexpected exception for one test case rather than aborting the whole batch--see the CLI command itself for that version).

Returns:

Raises:

  • CgContributionBuildFailedError

    if the solution failed to build--carries the build output; no test case is run.

  • CgLanguageOperationNotSupportedError

    if solution_language isn't yet supported-- raised immediately, from whichever test case hits it first (every other test case would fail identically, so this doesn't run the rest first).

  • CgContributionLocalTestFailedError

    if any test case failed--carries every result via .results.

Source code in codingame_tools/contribution_manager/manager.py
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
async def run_local_tests(
            self,
            test_cases: list[CgContributionLocalTestCase],
            solution_language: CgSolutionLanguage,
            *,
            update_expected: bool = False,
            timeout: float = DEFAULT_RUN_TIMEOUT_SECONDS,
            build_timeout: float = DEFAULT_BUILD_TIMEOUT_SECONDS,
        ) -> list[CgContributionLocalTestResult]:
    """Convenience batch wrapper: call `build_solution()` once, then run every test case in
       `test_cases` (e.g. from `list_local_tests`) via `run_local_test`, and raise if any
       failed--for programmatic callers that just want a pass/fail outcome without
       `cg contribution play`'s own interleaved per-test console output (which needs its own
       loop, to catch and continue past an unexpected exception for one test case rather than
       aborting the whole batch--see the CLI command itself for that version).

    Returns:
        One `CgContributionLocalTestResult` per test case, in `test_cases`' order.

    Raises:
        CgContributionBuildFailedError: if the solution failed to build--carries the build
                                         output; no test case is run.
        CgLanguageOperationNotSupportedError: if `solution_language` isn't yet supported--
                                               raised immediately, from whichever test case
                                               hits it first (every other test case would fail
                                               identically, so this doesn't run the rest first).
        CgContributionLocalTestFailedError: if any test case failed--carries every result via
                                             `.results`.
    """
    build_result = await self.build_solution(solution_language, timeout=build_timeout)
    if not build_result.ok:
        raise CgContributionBuildFailedError(build_result)
    results = [
            await self.run_local_test(tc, solution_language, update_expected=update_expected, timeout=timeout)
            for tc in test_cases
        ]
    if any(not r.passed for r in results):
        raise CgContributionLocalTestFailedError(results)
    return results

CgContributionManagerError

Bases: Exception

Raised for contribution-manager-level errors not better represented by a more specific exception (e.g. attempting to push() without a puzzle_type set, or an operation that refuses because a merge is in progress).

CgContributionSetLanguageResult dataclass

CgContributionSetLanguageResult(language, previous_language, wrote_stub)

The outcome of CgContributionManager.set_language().

language instance-attribute

language

The language now recorded in data/contribution-data.json.

previous_language instance-attribute

previous_language

What it was before (None if the contribution had no language set yet).

wrote_stub instance-attribute

wrote_stub

True when a starter data/solution.src was written for the new language; False when that language has no stub to offer and solution.src was left empty for you to fill in (the same thing create() does for such a language).

An empty file rather than a placeholder is correct, not a shortfall: it's sent as a null solutionSource, which makes updateContribution skip solution validation, whereas any non-null one must pass every test case--so a placeholder would block push().

CgContributionStatus dataclass

CgContributionStatus(contribution_dir, pushed, contribution_handle, local_title, local_dirty, merge_in_progress, sync_status, local_version, local_draft, local_ready_for_moderation, local_puzzle_type, local_solution_language, local_difficulty, server, moderator_approvals, moderator_denials, status_cache_refreshed_at)

A point-in-time summary of a contribution working directory--see CgContributionManager.status(). Combines purely local facts (sync_status, local_dirty, local_title) with the last-known server state (server/ moderator_approvals/moderator_denials/status_cache_refreshed_at), which is either served from .meta/contribution-status.json (cheap, no network access) or freshly re-fetched first, depending on status(remote=...).

contribution_dir instance-attribute

contribution_dir

The working directory this status describes.

pushed instance-attribute

pushed

Whether this working directory has ever been successfully push()d--i.e. whether contribution_handle is set. If False, server/local_version are always None and sync_status is always NOT_PUSHED.

contribution_handle instance-attribute

contribution_handle

The public handle this working directory tracks, or None if never pushed.

local_title instance-attribute

local_title

data/contribution-data.json's current title, always available once imported/created, regardless of push/sync state.

local_dirty instance-attribute

local_dirty

Whether the working tree currently differs from main's tip (staged or unstaged)--False whenever merge_in_progress is True (not meaningful mid-merge).

merge_in_progress instance-attribute

merge_in_progress

Whether a cg contribution merge is currently unresolved.

sync_status instance-attribute

sync_status

How main currently relates to server--see CgContributionSyncStatus.

local_version instance-attribute

local_version

The server version main last synced with (server's tip's Cg-Version trailer), or None if never pushed. Not necessarily the server's current version unless sync_status is UP_TO_DATE or LOCAL_AHEAD--see server.last_version.version for that, when server is populated fresh (status(remote=True)).

local_draft instance-attribute

local_draft

data/contribution-data.json's draft flag--what's currently on disk (i.e. what the next push() would send), which may differ from server.draft if there are local edits not yet pushed. Always available once imported/created. Prefer this over server.draft for "what will be pushed"--server reflects the server's state as of the last fetch, not necessarily what's currently on disk here.

local_ready_for_moderation instance-attribute

local_ready_for_moderation

data/contribution-data.json's ready_for_moderation flag--see local_draft's docstring; same local-vs-server caveat applies to server.ready_for_moderation.

local_puzzle_type instance-attribute

local_puzzle_type

data/contribution-data.json's puzzle_type (e.g. "PUZZLE_INOUT"), always available once imported/created--see local_draft's docstring for why this (not server. contribution_type) is the one to use for "what will be pushed".

local_solution_language instance-attribute

local_solution_language

data/contribution-data.json's data.solution_language (e.g. "Python3")--the reference solution's language. May be None if a solution hasn't been provided yet. Same local-vs- server rationale as local_puzzle_type--this is versioned (content) state, changed only via push(), not part of CgContributionStatusCache's non-versioned metadata.

local_difficulty instance-attribute

local_difficulty

data/contribution-data.json's data.difficulty (e.g. "easy"). May be None if not set yet. Same local-vs-server rationale as local_puzzle_type/local_solution_language-- versioned content state, not part of CgContributionStatusCache.

server instance-attribute

server

The last-known full, unredacted contribution record from the server (from .meta/ contribution-status.json's contribution field--see CgContributionStatusCache), or None if never pushed or never fetched under a version of this package new enough to write that cache. Reflects the server's state as of status_cache_refreshed_at, which may lag behind local edits--see local_draft/local_ready_for_moderation/local_puzzle_type/ local_solution_language/local_difficulty for what's actually on disk right now.

moderator_approvals instance-attribute

moderator_approvals

Moderators who had cast a "validate" (approve) vote on this contribution's privileged approve/reject moderation gate (Contribution/findContributionModerators) as of status_cache_refreshed_at--3 needed to publish. None under the same conditions as server (never pushed, or never fetched yet). Distinct from the ungated community vote (server.up_votes/down_votes)--never conflate the two.

moderator_denials instance-attribute

moderator_denials

Moderators who had cast a "deny" (reject) vote as of status_cache_refreshed_at--see moderator_approvals's docstring; 3 needed to reject.

status_cache_refreshed_at instance-attribute

status_cache_refreshed_at

When server/moderator_approvals/moderator_denials were captured (.meta/ contribution-status.json's own refreshed_at)--None exactly when those three are None. Always UTC.

CgContributionSyncStatus

Bases: str, Enum

Read-only classification of how main and server currently relate--see CgContributionManager.status(). Distinct from CgRebaseStatus (the outcome of taking an action): this describes the current state without changing anything, and distinguishes LOCAL_AHEAD/SERVER_AHEAD from each other, which CgRebaseStatus doesn't need to (it only cares whether server moved).

NOT_PUSHED class-attribute instance-attribute

NOT_PUSHED = 'not_pushed'

create()d but never successfully push()d--no server branch exists at all yet.

UP_TO_DATE class-attribute instance-attribute

UP_TO_DATE = 'up_to_date'

main and server agree, and there are no uncommitted local edits either.

LOCAL_AHEAD class-attribute instance-attribute

LOCAL_AHEAD = 'local_ahead'

main has commits and/or uncommitted edits beyond the last sync point, but server hasn't moved--a plain push() would succeed with no conflict.

SERVER_AHEAD class-attribute instance-attribute

SERVER_AHEAD = 'server_ahead'

server has moved since the last sync, but main hasn't changed--cg contribution rebase would fast-forward cleanly.

DIVERGED class-attribute instance-attribute

DIVERGED = 'diverged'

Both sides have changed since they last synced--cg contribution rebase/push() would report a conflict; use cg contribution merge to resolve.

MERGE_IN_PROGRESS class-attribute instance-attribute

MERGE_IN_PROGRESS = 'merge_in_progress'

A cg contribution merge is currently unresolved (MERGE_HEAD exists)--other sync-status classification doesn't apply until it's finished (merge continue) or merge aborted.

CgMergeStartResult dataclass

CgMergeStartResult(status, text_conflicts=(), binary_conflicts=())

The outcome of CgContributionManager.merge_start().

text_conflicts class-attribute instance-attribute

text_conflicts = ()

Relative paths where git left <<<<<<<-style conflict markers for manual resolution.

binary_conflicts class-attribute instance-attribute

binary_conflicts = ()

Relative paths where both sides changed differently but the content isn't text--git's own default behavior for a binary conflict is to leave main's (local) version as-is, no markers; pull .git show server:<path> (or cg contribution git show server:<path>) by hand if you want the server's version instead.

CgMergeStartStatus

Bases: str, Enum

The outcome of CgContributionManager.merge_start().

STARTED class-attribute instance-attribute

STARTED = 'started'

A real git merge server was attempted. If text_conflicts/binary_conflicts are both empty, it already completed (git commits automatically when there's nothing left unresolved)--merge_in_progress is already False again, no merge_continue() needed or possible. Otherwise, resolve the conflicts and run merge_continue().

ALREADY_IN_PROGRESS class-attribute instance-attribute

ALREADY_IN_PROGRESS = 'already_in_progress'

merge_start() is idempotent--if a merge is already in progress (MERGE_HEAD exists), it leaves it completely untouched rather than erroring or restarting it.

UP_TO_DATE class-attribute instance-attribute

UP_TO_DATE = 'up_to_date'

server's tip already equals git merge-base main server--nothing to merge. Consistent with CgRebaseStatus.UP_TO_DATE.

CgRebaseStatus

Bases: str, Enum

The outcome of CgContributionManager.rebase().

UP_TO_DATE class-attribute instance-attribute

UP_TO_DATE = 'up_to_date'

server hasn't advanced since main last synced with it (its tip already equals git merge-base main server)--nothing to do, regardless of whether main/the working directory have uncommitted edits.

FAST_FORWARDED class-attribute instance-attribute

FAST_FORWARDED = 'fast_forwarded'

server advanced, but main had no edits since it last synced (main's tip still equals the old merge-base)--fast-forward: main gets a new commit matching server's new tip.

CONFLICT class-attribute instance-attribute

CONFLICT = 'conflict'

Both server and main have diverged since they last synced--nothing was changed. Use cg contribution diff to inspect, and cg contribution merge to resolve.

CgContributionDirInferenceError

Bases: Exception

Raised by infer_contribution_dir when target_file doesn't resolve into a contribution working directory.

CgContributionDirNotFoundError

CgContributionDirNotFoundError()

Bases: Exception

Raised by resolve_contribution_dir() (unless allow_default=True) when no contribution working directory could be located by any discovery step. Does not indicate a bug--this is the normal outcome before a contribution has been imported/started in the current directory.

Source code in codingame_tools/contribution_manager/resolver.py
44
45
46
47
48
49
def __init__(self) -> None:
    super().__init__(
            "No contribution working directory found (checked the current directory and "
            "\"./contribution\" for a contribution.json). Pass an explicit directory, set "
            f"{CG_CONTRIBUTION_DIR_ENV_VAR}, or run `cg settings set contribution-dir DIR`."
        )

CgContributionIdentity dataclass

CgContributionIdentity(schema_version, extra_data=dict(), contribution_handle=None)

Bases: JSONWizardX

The contribution.json manifest: global identity for a contribution working directory, constant for its lifetime (never changes across import_/push/merge/etc.--unlike everything else in the working directory, this is not tied to any specific commit/version).

Exception: contribution_handle itself transitions exactly once, from None to a real value, the first time CgContributionManager.push() succeeds against a working directory created via create() rather than import_()--see push()'s docstring for why.

schema_version instance-attribute

schema_version

The on-disk format version this working directory was written in--see CONTRIBUTION_SCHEMA_VERSION.

contribution_handle class-attribute instance-attribute

contribution_handle = None

The opaque contribution ID (CgContribution.public_handle) this working directory tracks--None if it was create()d and has never been successfully push()d yet (there's no server-side contribution to have a handle for). Also the one fact that decides which of CgContributionManager.repair()'s two modes applies, and (when set) the one fact that mode actually needs--everything else about prior git history is either present (git-dir found where recorded) or, if not, deliberately not reconstructed, just re-fetched fresh.

CgContributionMeta dataclass

CgContributionMeta(git_repo, extra_data=dict())

Bases: JSONWizardX

.meta/contribution-meta.json: this client's own state about how the working directory is put together, as opposed to what it is (contribution.json) or what it holds (data/).

Chosen and maintained entirely by the meta infrastructure, so it belongs here rather than in the identity manifest, which describes the contribution itself and is constant for the directory's lifetime.

Not merely a tidiness argument--see manager's "portability contract". contribution.json and data/ are the exportable state: copy them elsewhere, repair(), and you have a working directory. So they may hold only facts true of the contribution wherever it is. Where the git-dir goes is a fact about this checkout on this machine, and two checkouts of one contribution can legitimately differ--the same content exported from a standalone directory into a colleague's monorepo must come up external rather than embedded. Recorded in contribution.json it would travel and be wrong on arrival, which is what 1.0.x did.

Being in .meta/ makes it disposable, like everything else here--so nothing may depend on it surviving. CgContributionManager.git_dir treats it as a cached answer and falls back to looking for the repository on disk, which is why deleting .meta/ can never orphan a data/.git, and why a freshly exported directory with no .meta/ at all still works (see _resolve_git_dir).

git_repo instance-attribute

git_repo

Where this working directory's git-dir is, relative to the working directory root, in POSIX form--either ".meta/.contribution-git" (external, with data/ as its work tree) or "data/.git" (embedded, making data/ an ordinary git working directory). Decided once at create()/import_() time--see manager's module docstring for how, and why it is not re-derived on every command.

A path rather than a flag because it is read far more often than it is written, and a path is directly usable. It is still only ever one of those two values; anything else is not something the rest of this package knows how to drive.

CgContributionStatusCache dataclass

CgContributionStatusCache(version, contribution, moderator_approvals, moderator_denials, _refreshed_at=Alias('refreshedAt'), extra_data=dict())

Bases: JSONWizardX

The .meta/contribution-status.json cache: an offline snapshot of every piece of server metadata that is NOT tied to any particular content version--status/status_history/ score/up_votes/down_votes/comment_count/views/editable/active_version/ validate_action/the moderation-window timestamps (all live on contribution.last_version or contribution itself), plus moderator_approvals/moderator_denials (from a wholly separate endpoint, Contribution/findContributionModerators--not part of CgContribution at all).

Deliberately NOT git-tracked (see layout.CONTRIBUTION_STATUS_CACHE_FILE_NAME)--unlike contribution-data.json, none of this is diffable/mergeable content, it's just the most recent snapshot available, refreshed every time CgContributionManager.fetch()/import_()/ repair() obtain a fresh CgContribution from the server--regardless of whether the content version changed, since none of these fields are tied to it (a moderator vote or a new comment doesn't bump the content version).

contribution is stored whole and unredacted here (unlike the version-data git branch's copy, which redacts draft/ready_for_moderation/contribution_type/last_version.data to keep those out of diffable git history)--this file isn't git-tracked at all, so nothing is gained by redacting it, and keeping the full object avoids having to duplicate every field name into a narrower cache-specific shape.

version instance-attribute

version

The content version (contribution.last_version.version) as of this refresh--informational only; this cache's own fields are current as of refreshed_at regardless of whether the content version has since moved on.

contribution instance-attribute

contribution

The complete, unredacted CgContribution as returned by findContribution at refreshed_at.

moderator_approvals instance-attribute

moderator_approvals

Moderators who had cast a "validate" (approve) vote as of refreshed_at.

moderator_denials instance-attribute

moderator_denials

Moderators who had cast a "deny" (reject) vote as of refreshed_at.

refreshed_at property writable

refreshed_at

See the field docstring for _refreshed_at. Always UTC.

CgContributionView dataclass

CgContributionView(extra_data=dict(), puzzle_type=None, draft=True, ready_for_moderation=False, data=(lambda: CgContributionData(title=''))())

Bases: JSONWizardX

The contribution-data.json manifest: the content of data/--everything needed to push() it, or to compare it against main/server at any other commit via git diff.

data is a working version of CgContributionData, with several fields deliberately kept always-empty by convention (not schema-enforced) because their real content lives in sibling files/directories instead--overwritten from those sources when a view is materialized, so a stray hand-edited value here is harmless, just confusing to read:

  • statement -> statement.cgmd
  • input_description -> input_description.cgmd
  • output_description -> output_description.cgmd
  • constraints -> constraints.cgmd
  • stub_generator -> stub_generator.cgstub
  • solution -> solution.src (always this exact name--see codingame_tools.contribution_manager.layout.SOLUTION_FILE_NAME)
  • test_cases -> built from the tests/ subdirectory (see test_cases_dir)
  • cover_binary_id -> built from cover.png

All other fields of data (title, difficulty, topics, solution_language) are used normally--there's no sidecar file for them.

puzzle_type class-attribute instance-attribute

puzzle_type = None

The contribution type, e.g. "PUZZLE_INOUT". A required top-level parameter to updateContribution--must be set before push() can succeed.

draft class-attribute instance-attribute

draft = True

Whether the version being committed is a private draft. A required top-level parameter to updateContribution. Defaults to True (the safe default for a working dir that hasn't explicitly decided to publish yet).

ready_for_moderation class-attribute instance-attribute

ready_for_moderation = False

Whether the version being committed is being formally submitted for moderation. A required top-level parameter to updateContribution.

data class-attribute instance-attribute

data = field(default_factory=lambda: CgContributionData(title=''))

The materialized contribution content--see the class docstring for which fields are real and which are always-empty placeholders backed by sibling files/directories instead.

CgContributionLocalTestCase dataclass

CgContributionLocalTestCase(ordinal, side, title, input_file, output_file, input_text, output_text)

One (ordinal, side) test case as read back from tests_dir by list_local_test_cases-- the local-execution counterpart to commit_test_cases's flat list[CgTestCase] (which is shaped for submission to updateContribution, not for running locally: it drops the ordinal, and--unlike CgTestCase--this carries file paths, not just content, so a caller can overwrite output_file in place, e.g. to accept a solution's current output as the new expected baseline).

ordinal instance-attribute

ordinal

The ordinal directory name this test case lives under (e.g. "03")--not necessarily a clean integer (see the module docstring's "05a" example), but comparable via natural sort like everything else here.

side instance-attribute

side

Either "local" or "validator"--which of tests_dir's two subdirectories (see LOCAL_SUBDIR_NAME/VALIDATOR_SUBDIR_NAME) this test case came from.

title instance-attribute

title

The test case's real title (see CgTestCaseFileMeta.title).

input_file instance-attribute

input_file

Path to this test case's input.txt.

output_file instance-attribute

output_file

Path to this test case's output.txt--the expected/known-good output. A caller running this test case locally may overwrite this file with the solution's actual output to accept it as the new baseline (see codingame_tools.test_runner.debug_stdin's --update-expected, and CgContributionManager.run_local_test).

input_text instance-attribute

input_text

input_file's content as the server would hold it--decoded as UTF-8, then passed through file_to_server_text to drop the file's line terminator.

This is deliberately the server-side value rather than the file's raw bytes: it's what gets fed to a solution's stdin locally, and CodinGame feeds the stored string itself (confirmed by downloading published puzzles' test-case files, which are byte-identical to the contribution strings they came from). Feeding the raw file would silently give a local run one more byte of stdin than the same test gets on the server.

output_text instance-attribute

output_text

output_file's content as the server would hold it (see input_text), as of when this was read--stale the moment output_file is overwritten by an update, so re-list_local_test_cases after updating rather than reusing an old CgContributionLocalTestCase.

CgContributionTestCaseError

Bases: Exception

Raised by commit_test_cases when tests/'s on-disk layout is malformed in a way that can't be interpreted--e.g. more than one local (or validator) test case directory under a single ordinal directory. Only possible via manual editing; import_test_cases never produces a layout that triggers this.

CgTestCaseFileMeta dataclass

CgTestCaseFileMeta(title, extra_data=dict())

Bases: JSONWizardX

The content of a single test case directory's test.json: just the test's real, unnormalized title (the directory name itself is a lossy filename-friendly slug--see normalize_test_title).

title instance-attribute

title

The test case's real title, exactly as it appears in CgTestCase.title.

redact_commit_contribution

redact_commit_contribution(contribution)

Return a copy of contribution with every field that's duplicated in CgContributionView/contribution-data.json redacted to an empty placeholder--draft, ready_for_moderation, contribution_type (top-level), and last_version.data (the full content payload, including cover_binary_id--tracked separately as CgContributionCommitMetadata.cover_binary_id instead)--plus last_version.statement_html, which is never needed at all (purely derivative, see its own docstring).

Source code in codingame_tools/contribution_manager/contribution_commit_data.py
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
def redact_commit_contribution(contribution: CgContribution) -> CgContribution:
    """Return a copy of `contribution` with every field that's duplicated in
       `CgContributionView`/`contribution-data.json` redacted to an empty placeholder--`draft`,
       `ready_for_moderation`, `contribution_type` (top-level), and `last_version.data` (the full
       content payload, including `cover_binary_id`--tracked separately as
       `CgContributionCommitMetadata.cover_binary_id` instead)--plus `last_version.statement_html`,
       which is never needed at all (purely derivative, see its own docstring)."""
    cleaned_version = dataclasses.replace(
            contribution.last_version,
            data=CgContributionData(title=""),
            draft=None,
            ready_for_moderation=None,
            statement_html=None,
        )
    return dataclasses.replace(
            contribution,
            last_version=cleaned_version,
            draft=False,
            ready_for_moderation=False,
            contribution_type="",
        )

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)

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"

find_solution_file

find_solution_file(data_dir, extension=None)

The existing solution file in data_dir, whatever extension it currently carries.

Callers generally know the language and so know the name, but not always: a working directory whose language changed out from under it, or one written by an older cg that used a fixed solution.src, still has to be found. The expected name wins when present, so a stray leftover can never shadow the real file; otherwise a lone solution.* is accepted.

Returns None if there is no solution file, or if several exist with no way to choose--the caller decides whether that's an error or a thing to repair.

Source code in codingame_tools/contribution_manager/layout.py
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
def find_solution_file(data_dir: Path, extension: str | None = None) -> Path | None:
    """The existing solution file in `data_dir`, whatever extension it currently carries.

       Callers generally know the language and so know the name, but not always: a working
       directory whose language changed out from under it, or one written by an older cg that used
       a fixed `solution.src`, still has to be found. The expected name wins when present, so a
       stray leftover can never shadow the real file; otherwise a lone `solution.*` is accepted.

       Returns None if there is no solution file, or if several exist with no way to choose--the
       caller decides whether that's an error or a thing to repair."""
    if extension is not None:
        expected = data_dir / solution_file_name(extension)
        if expected.is_file():
            return expected
    candidates = sorted(p for p in data_dir.glob(f"{SOLUTION_FILE_STEM}.*") if p.is_file())
    if len(candidates) == 1:
        return candidates[0]
    if not candidates:
        return None
    # Ambiguous: prefer the fallback name if it is one of them, since that is what an older cg
    # wrote and what a migration is most likely looking at.
    fallback = data_dir / solution_file_name(None)
    return fallback if fallback in candidates else None

solution_file_name

solution_file_name(extension)

solution.<ext> for a known language extension, else solution.src.

The file carries the language's real extension rather than a fixed one because every tool that reads it--language servers, debuggers, the compiler--dispatches on the extension. cg previously kept data/solution.src fixed and maintained a solution.<ext> symlink beside it, which cost a day of debugging: the debug info named one path, the editor resolved the other, and breakpoints silently failed to bind. One real file with the right name has no such gap, and needs no symlink support from the filesystem (which Windows only grants with developer mode enabled).

Source code in codingame_tools/contribution_manager/layout.py
60
61
62
63
64
65
66
67
68
69
70
def solution_file_name(extension: str | None) -> str:
    """`solution.<ext>` for a known language extension, else `solution.src`.

       The file carries the language's real extension rather than a fixed one because every tool
       that reads it--language servers, debuggers, the compiler--dispatches on the extension. cg
       previously kept `data/solution.src` fixed and maintained a `solution.<ext>` symlink beside
       it, which cost a day of debugging: the debug info named one path, the editor resolved the
       other, and breakpoints silently failed to bind. One real file with the right name has no
       such gap, and needs no symlink support from the filesystem (which Windows only grants with
       developer mode enabled)."""
    return f"{SOLUTION_FILE_STEM}.{extension or SOLUTION_FALLBACK_EXTENSION}"

find_contribution_dir

find_contribution_dir(explicit=None, *, settings=None, start_dir=None)

Locate the contribution working directory to use, following the documented discovery precedence:

1. `explicit` (typically the resolved value of a `--contribution-dir` CLI flag), if given.
2. The `CG_CONTRIBUTION_DIR` environment variable, if set.
3. `settings.current_contribution_dir`--the *active* working directory, set by
   `cg contribution import`/`create` and `cg contribution activate`. Outranks the configured default
   below so that creating a working directory somewhere isn't silently overridden by a
   standing `contribution_dir` preference pointing elsewhere.
4. `settings.contribution_dir` (see `CgSettings.contribution_dir`), if given and set.
5. `start_dir` (or the current directory, if not given), if it contains a
   `contribution.json`.
6. `start_dir / "contribution"`, if it contains a `contribution.json`.

Steps 1-4 are taken at face value--the resolved directory need not contain a contribution.json yet (e.g. a fresh, empty target directory for cg contribution import). Steps 5-6 are implicit inference and are deliberately conservative: they only match if a contribution.json is actually already there.

Returns:

  • Path | None

    The resolved contribution directory path, or None if nothing was found at all. This

  • Path | None

    function never creates anything.

Source code in codingame_tools/contribution_manager/resolver.py
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
def find_contribution_dir(
            explicit: Path | str | None = None,
            *,
            settings: CgSettings | None = None,
            start_dir: Path | str | None = None,
        ) -> Path | None:
    """Locate the contribution working directory to use, following the documented discovery
       precedence:

        1. `explicit` (typically the resolved value of a `--contribution-dir` CLI flag), if given.
        2. The `CG_CONTRIBUTION_DIR` environment variable, if set.
        3. `settings.current_contribution_dir`--the *active* working directory, set by
           `cg contribution import`/`create` and `cg contribution activate`. Outranks the configured default
           below so that creating a working directory somewhere isn't silently overridden by a
           standing `contribution_dir` preference pointing elsewhere.
        4. `settings.contribution_dir` (see `CgSettings.contribution_dir`), if given and set.
        5. `start_dir` (or the current directory, if not given), if it contains a
           `contribution.json`.
        6. `start_dir / "contribution"`, if it contains a `contribution.json`.

       Steps 1-4 are taken at face value--the resolved directory need not contain a
       `contribution.json` yet (e.g. a fresh, empty target directory for `cg contribution
       import`). Steps 5-6 are implicit inference and are deliberately conservative: they only
       match if a `contribution.json` is actually already there.

    Returns:
        The resolved contribution directory path, or None if nothing was found at all. This
        function never creates anything.
    """
    if explicit is not None:
        return Path(explicit).expanduser().resolve()
    env_value = os.environ.get(CG_CONTRIBUTION_DIR_ENV_VAR)
    if env_value:
        return Path(env_value).expanduser().resolve()
    if settings is not None and settings.current_contribution_dir is not None:
        return settings.current_contribution_dir
    if settings is not None and settings.contribution_dir is not None:
        return settings.contribution_dir
    start = Path(start_dir).resolve() if start_dir is not None else Path.cwd()
    if (start / CONTRIBUTION_IDENTITY_FILE_NAME).is_file():
        return start
    default_subdir = start / DEFAULT_CONTRIBUTION_SUBDIR_NAME
    if (default_subdir / CONTRIBUTION_IDENTITY_FILE_NAME).is_file():
        return default_subdir
    return None

infer_contribution_dir

infer_contribution_dir(target_file)

Infer a contribution working directory's root from a solution file somewhere within it-- see codingame_tools.puzzle_manager.resolver.infer_puzzle_dir's docstring for the full rationale (identical here, just contribution.json instead of puzzle.json): the only two things ever promised about target_file are that a debugger's breakpoints bind to whatever path was actually open in the editor, and that resolving every symlink in it always eventually lands on data/solution.src--so this isn't a search, it's two fixed path segments up from the fully-resolved target_file.

Raises:

  • CgContributionDirInferenceError

    if target_file, once fully resolved, isn't .../data/solution.src, or contribution.json isn't present at the inferred root.

Source code in codingame_tools/contribution_manager/resolver.py
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
def infer_contribution_dir(target_file: Path | str) -> Path:
    """Infer a contribution working directory's root from a solution file somewhere within it--
       see `codingame_tools.puzzle_manager.resolver.infer_puzzle_dir`'s docstring for the full
       rationale (identical here, just `contribution.json` instead of `puzzle.json`): the only
       two things ever promised about `target_file` are that a debugger's breakpoints bind to
       whatever path was actually open in the editor, and that resolving every symlink in it
       always eventually lands on `data/solution.src`--so this isn't a search, it's two fixed
       path segments up from the fully-resolved `target_file`.

    Raises:
        CgContributionDirInferenceError: if `target_file`, once fully resolved, isn't
                                          `.../data/solution.src`, or `contribution.json` isn't
                                          present at the inferred root.
    """
    resolved = Path(target_file).resolve()
    # Matched on the stem, not the full name: the solution file carries its language's extension
    # and is renamed when the language changes, so `solution.cpp` and `solution.py` are equally
    # valid here and the set of legal names is open-ended.
    if resolved.stem != SOLUTION_FILE_STEM or resolved.parent.name != DATA_SUBDIR_NAME:
        raise CgContributionDirInferenceError(
                f"{target_file} does not resolve to a {DATA_SUBDIR_NAME}/{SOLUTION_FILE_STEM}.* "
                "file--not part of a contribution working directory."
            )
    root = resolved.parent.parent
    if not (root / CONTRIBUTION_IDENTITY_FILE_NAME).is_file():
        raise CgContributionDirInferenceError(
                f"{root} has no {CONTRIBUTION_IDENTITY_FILE_NAME}--not a contribution working directory.")
    return root

resolve_contribution_dir

resolve_contribution_dir(explicit=None, *, settings=None, start_dir=None, allow_default=False)

Locate the contribution working directory, following the discovery precedence documented on find_contribution_dir.

If allow_default is True and no directory can be found, falls back to start_dir (or the current directory)--useful for commands like cg contribution import that are happy to treat "nothing found" as "use the current directory as the new working directory". push()-style callers, where there must already be a working directory, should leave this False.

Raises:

Source code in codingame_tools/contribution_manager/resolver.py
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
def resolve_contribution_dir(
            explicit: Path | str | None = None,
            *,
            settings: CgSettings | None = None,
            start_dir: Path | str | None = None,
            allow_default: bool = False,
        ) -> Path:
    """Locate the contribution working directory, following the discovery precedence documented
       on `find_contribution_dir`.

       If `allow_default` is True and no directory can be found, falls back to `start_dir` (or the
       current directory)--useful for commands like `cg contribution import` that are happy to
       treat "nothing found" as "use the current directory as the new working directory".
       `push()`-style callers, where there must already be a working directory, should leave
       this False.

    Raises:
        CgContributionDirNotFoundError: if no directory could be located anywhere, and
                                         `allow_default` is False.
    """
    found = find_contribution_dir(explicit, settings=settings, start_dir=start_dir)
    if found is not None:
        return found
    if allow_default:
        return Path(start_dir).resolve() if start_dir is not None else Path.cwd()
    raise CgContributionDirNotFoundError()

commit_test_cases

commit_test_cases(tests_dir)

Read tests_dir back into a flat list[CgTestCase], in the order updateContribution should submit them: ordinal directories in natural-sort order, and within each, the local test case (if present) followed by the validator test case (if present)--see the module docstring for why this ordering is safe (each side's own relative order is preserved, even though the two may be interleaved differently than in the original API response).

Returns an empty list if tests_dir doesn't exist (no test cases yet).

Source code in codingame_tools/contribution_manager/test_cases_dir.py
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
def commit_test_cases(tests_dir: Path) -> list[CgTestCase]:
    """Read `tests_dir` back into a flat `list[CgTestCase]`, in the order `updateContribution`
       should submit them: ordinal directories in natural-sort order, and within each, the local
       test case (if present) followed by the validator test case (if present)--see the module
       docstring for why this ordering is safe (each side's own relative order is preserved, even
       though the two may be interleaved differently than in the original API response).

       Returns an empty list if `tests_dir` doesn't exist (no test cases yet).
    """
    if not tests_dir.is_dir():
        return []
    ordinal_dirs = sorted(
            (d for d in tests_dir.iterdir() if d.is_dir()),
            key=lambda d: _natural_sort_key(d.name),
        )
    result: list[CgTestCase] = []
    for ordinal_dir in ordinal_dirs:
        local_entry: CgTestCase | None = None
        validator_entry: CgTestCase | None = None
        named_dirs = sorted((d for d in ordinal_dir.iterdir() if d.is_dir()), key=lambda d: d.name)
        for named_dir in named_dirs:
            title = _read_test_meta_title(named_dir)
            local_side = named_dir / LOCAL_SUBDIR_NAME
            if local_side.is_dir():
                if local_entry is not None:
                    raise CgContributionTestCaseError(
                            f"Multiple local test case directories found under {ordinal_dir}")
                local_entry = _read_test_side(local_side, title, is_test=True, is_validator=False)
            validator_side = named_dir / VALIDATOR_SUBDIR_NAME
            if validator_side.is_dir():
                if validator_entry is not None:
                    raise CgContributionTestCaseError(
                            f"Multiple validator test case directories found under {ordinal_dir}")
                validator_entry = _read_test_side(validator_side, title, is_test=False, is_validator=True)
        if local_entry is not None:
            result.append(local_entry)
        if validator_entry is not None:
            result.append(validator_entry)
    return result

import_test_cases

import_test_cases(test_cases, tests_dir)

(Re)build tests_dir from a flat list of test cases (as returned by findContribution), entirely replacing any existing content there. Local (is_test=True) and validator (is_validator=True) test cases are each numbered separately, in their existing relative order, and paired by that number into ordinal directories--see the module docstring.

Ordinal directories are always written as zero-padded numbers (width based on the total count, minimum 2 digits, e.g. "01"); commit_test_cases is agnostic to this and tolerates any renaming.

Source code in codingame_tools/contribution_manager/test_cases_dir.py
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
def import_test_cases(test_cases: list[CgTestCase], tests_dir: Path) -> None:
    """(Re)build `tests_dir` from a flat list of test cases (as returned by `findContribution`),
       entirely replacing any existing content there. Local (`is_test=True`) and validator
       (`is_validator=True`) test cases are each numbered separately, in their existing relative
       order, and paired by that number into ordinal directories--see the module docstring.

       Ordinal directories are always written as zero-padded numbers (width based on the total
       count, minimum 2 digits, e.g. "01"); `commit_test_cases` is agnostic to this and tolerates
       any renaming.
    """
    locals_ = [tc for tc in test_cases if tc.is_test]
    validators = [tc for tc in test_cases if tc.is_validator]
    if tests_dir.exists():
        shutil.rmtree(tests_dir)
    count = max(len(locals_), len(validators))
    if count == 0:
        return
    width = max(2, len(str(count)))
    tests_dir.mkdir(parents=True, exist_ok=True)
    for i in range(count):
        local = locals_[i] if i < len(locals_) else None
        validator = validators[i] if i < len(validators) else None
        ordinal_dir = tests_dir / str(i + 1).zfill(width)
        _place_ordinal(ordinal_dir, local, validator)

list_local_test_cases

list_local_test_cases(tests_dir)

Read tests_dir back into a flat list of individually-runnable local test cases--one per (ordinal, side) pair actually present on disk, in the same ordinal natural-sort order as commit_test_cases, local before validator within an ordinal. Unlike commit_test_cases, both sides of an ordinal are kept as separate entries here (rather than being merged into API submission order) since each is independently runnable, and file paths are included (rather than just content) since a caller may want to update output_file in place.

Returns:

Source code in codingame_tools/contribution_manager/test_cases_dir.py
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
def list_local_test_cases(tests_dir: Path) -> list[CgContributionLocalTestCase]:
    """Read `tests_dir` back into a flat list of individually-runnable local test cases--one per
       (ordinal, side) pair actually present on disk, in the same ordinal natural-sort order as
       `commit_test_cases`, local before validator within an ordinal. Unlike `commit_test_cases`,
       both sides of an ordinal are kept as separate entries here (rather than being merged into
       API submission order) since each is independently runnable, and file paths are included
       (rather than just content) since a caller may want to update `output_file` in place.

    Returns:
        An empty list if `tests_dir` doesn't exist (no test cases yet).
    """
    if not tests_dir.is_dir():
        return []
    ordinal_dirs = sorted(
            (d for d in tests_dir.iterdir() if d.is_dir()),
            key=lambda d: _natural_sort_key(d.name),
        )
    result: list[CgContributionLocalTestCase] = []
    for ordinal_dir in ordinal_dirs:
        named_dirs = sorted((d for d in ordinal_dir.iterdir() if d.is_dir()), key=lambda d: d.name)
        for named_dir in named_dirs:
            title = _read_test_meta_title(named_dir)
            for side, subdir_name in (("local", LOCAL_SUBDIR_NAME), ("validator", VALIDATOR_SUBDIR_NAME)):
                side_dir = named_dir / subdir_name
                if not side_dir.is_dir():
                    continue
                input_file = side_dir / _INPUT_FILE_NAME
                output_file = side_dir / _OUTPUT_FILE_NAME
                result.append(CgContributionLocalTestCase(
                        ordinal=ordinal_dir.name, side=side, title=title,
                        input_file=input_file, output_file=output_file,
                        input_text=file_to_server_text(input_file.read_text(encoding="utf-8")),
                        output_text=file_to_server_text(output_file.read_text(encoding="utf-8")),
                    ))
    return result

normalize_test_title

normalize_test_title(title)

Convert a test case title into a filename-friendly slug: runs of whitespace/punctuation become a single dash, and leading/trailing dashes are stripped. Falls back to "test" if nothing alphanumeric remains (e.g. a title that's pure punctuation).

Source code in codingame_tools/contribution_manager/test_cases_dir.py
103
104
105
106
107
108
def normalize_test_title(title: str) -> str:
    """Convert a test case title into a filename-friendly slug: runs of whitespace/punctuation
       become a single dash, and leading/trailing dashes are stripped. Falls back to "test" if
       nothing alphanumeric remains (e.g. a title that's pure punctuation)."""
    slug = _SLUG_INVALID_RUN_RE.sub("-", title).strip("-")
    return slug or "test"

renormalize_test_case_dirs

renormalize_test_case_dirs(tests_dir)

Rewrite tests_dir's ordinal directory names as a clean, sequential, zero-padded sort key (matching what import_test_cases would produce), preserving relative order via the same natural-sort comparator commit_test_cases uses. Named/local/validator subdirectory content is untouched--only the ordinal directories themselves are renamed. A no-op if tests_dir doesn't exist or is empty.

Source code in codingame_tools/contribution_manager/test_cases_dir.py
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
def renormalize_test_case_dirs(tests_dir: Path) -> None:
    """Rewrite `tests_dir`'s ordinal directory names as a clean, sequential, zero-padded sort key
       (matching what `import_test_cases` would produce), preserving relative order via the same
       natural-sort comparator `commit_test_cases` uses. Named/local/validator subdirectory
       content is untouched--only the ordinal directories themselves are renamed. A no-op if
       `tests_dir` doesn't exist or is empty.
    """
    if not tests_dir.is_dir():
        return
    ordinal_dirs = sorted(
            (d for d in tests_dir.iterdir() if d.is_dir()),
            key=lambda d: _natural_sort_key(d.name),
        )
    if not ordinal_dirs:
        return
    width = max(2, len(str(len(ordinal_dirs))))
    # Two-phase rename (via temporary names) so that renumbering never collides with another
    # ordinal directory's current (pre-renumbering) name.
    staged: list[tuple[Path, str]] = []
    for i, d in enumerate(ordinal_dirs):
        temp_path = tests_dir / f".renormalize-tmp-{i}"
        d.rename(temp_path)
        staged.append((temp_path, str(i + 1).zfill(width)))
    for temp_path, target_name in staged:
        temp_path.rename(tests_dir / target_name)