Skip to content

kitconfig.repo_root()'s fallback is depth-specific — it breaks the scripts/devkit/lib/ layout its own docstring promises #60

Description

@topij

Found by CodeRabbit on the cs-toolkit adoption (in-parallel-oy/cs-toolkit#1791), and
confirmed by execution.

The claim

scripts/lib/kitconfig.py:53

def repo_root(start: Path | None = None) -> Path:
    """Nearest ancestor carrying a ``.git`` entry ... Walking up for the marker —
    rather than counting ``parents[N]`` — is what lets the kit be vendored at any
    depth (``scripts/devkit/lib/``) without rewriting a single path."""
    here = (start or Path(__file__)).resolve()
    for candidate in (here, *here.parents):
        if (candidate / ".git").exists():
            return candidate
    return here.parents[2] if len(here.parents) >= 3 else here.parent

The docstring names scripts/devkit/lib/ as the layout this supports. The
fallback then does exactly what the docstring says it avoids — counts
parents[N] — and [2] is calibrated for the kit's own scripts/lib/ layout:

layout parents[2] correct?
scripts/lib/kitconfig.py (kit's own) <repo>
scripts/devkit/lib/kitconfig.py (vendored, and the one the docstring names) <repo>/scripts

Reproduction

$ mkdir -p /tmp/v1/repo/scripts/devkit/lib          # deliberately NO .git
$ cp scripts/lib/kitconfig.py /tmp/v1/repo/scripts/devkit/lib/
$ cd /tmp/v1/repo/scripts/devkit/lib && python3 -c "
import kitconfig, pathlib
print('returned :', kitconfig.repo_root())
print('expected :', pathlib.Path('/tmp/v1/repo').resolve())"

returned : /tmp/v1/repo/scripts
expected : /tmp/v1/repo

load_config() then looks for <repo>/scripts/config/dev-model.yaml and raises
FileNotFoundError with a hint pointing at the wrong path.

Scope

Latent, not live, for a normal checkout — the .git walk succeeds first, so the
fallback only fires where there is no .git marker at all: exported tarballs,
GIT_DIR-only setups, some git worktree edge cases. But it is precisely the
vendored-at-depth case the kit now actively recommends: /adopt Step 1 says to
vendor under scripts/devkit/ when the adopter's scripts/ has colliding names,
and cs-toolkit is the first real repo to take that branch.

Suggested fix

Probe for a second marker before falling back to arithmetic:

     for candidate in (here, *here.parents):
         if (candidate / ".git").exists():
             return candidate
-    return here.parents[2] if len(here.parents) >= 3 else here.parent
+    for candidate in (here, *here.parents):
+        if (candidate / DEFAULT_CONFIG_PATH).is_file():
+            return candidate
+    return here.parents[2] if len(here.parents) >= 3 else here.parent

This restores the "vendored at any depth" property for the fallback path too, and
removes the last parents[N] count from the function.

Worth a test that runs repo_root() from a scripts/devkit/lib/ tree with no
.git — there is currently no coverage of the fallback branch at any depth, which
is why a docstring and its own code could disagree this plainly.

Found on: in-parallel-oy/cs-toolkit#1791

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions