diff --git a/docs/PLUGIN.md b/docs/PLUGIN.md index 2e09257..8de0510 100644 --- a/docs/PLUGIN.md +++ b/docs/PLUGIN.md @@ -703,6 +703,10 @@ Which repository a session belongs to (`plugin/paths.py`, `plugin/repos.py`): `gitdir:` file naming one that exists -- and the walk up from a session's directory stops at the home directory, so a versioned (or stray, empty) `~/.git` is the repository of sessions started in `~` and of nothing below it. `GIT_CEILING_DIRECTORIES` is honoured as git does. + `$TOKENECONOMIZER_REPO`, when set, is the repository's remote URL where git names none: a + session in a directory without git metadata (an export of a checkout in a container, a new + folder per run), or in a checkout without an `origin`, is keyed as a clone of that remote and + shares what was learned there. A checkout's own `origin` wins. * **One repository per session.** A hook's `cwd` is where the agent is now (Claude Code's follows its `cd`), so the first hook of a session pins it (`sessions//.json`) and every later hook, and the fold it hands off, uses the repository it started in -- where the diff --git a/tests/test_repo_key_env.py b/tests/test_repo_key_env.py new file mode 100644 index 0000000..25fecf0 --- /dev/null +++ b/tests/test_repo_key_env.py @@ -0,0 +1,56 @@ +"""`$TOKENECONOMIZER_REPO`: the repository of a session where git names none.""" +import os + +from tokeneconomizer.plugin import paths + + +def git_repo(root, remote): + os.makedirs(os.path.join(root, ".git"), exist_ok=True) + with open(os.path.join(root, ".git", "HEAD"), "w") as fh: + fh.write("ref: refs/heads/main\n") + with open(os.path.join(root, ".git", "config"), "w") as fh: + fh.write("[core]\n\trepositoryformatversion = 0\n" + + (f'[remote "origin"]\n\turl = {remote}\n' if remote else "")) + return root + + +def make_env(tmp_path, **extra): + home = tmp_path / "home" + home.mkdir(exist_ok=True) + return dict({"TOKENECONOMIZER_HOME": str(tmp_path / "te"), "HOME": str(home)}, **extra) + + +def test_a_named_remote_keys_an_export_without_git_metadata_as_its_clones(tmp_path): + """An export of a checkout in a container, or a new folder per run, holds no `.git`; the + variable names its repository, so it shares its clones' key.""" + env = make_env(tmp_path) + named = make_env(tmp_path, TOKENECONOMIZER_REPO="git@github.com:Microsoft/TokenEconomizer.git") + clone = git_repo(str(tmp_path / "TokenEconomizer"), "https://github.com/microsoft/TokenEconomizer") + export = tmp_path / "runs" / "job-1" / "work" + export.mkdir(parents=True) + r = paths.repo_identity(str(export), named) + assert r.key == paths.repo_identity(clone, env).key + assert r.how == paths.BY_REMOTE and r.root == str(export) + assert r.label == "microsoft/tokeneconomizer" and r.remote == "github.com/microsoft/tokeneconomizer" + # Another run's folder gets the same key; without the variable each folder is its own. + other = tmp_path / "runs" / "job-2" / "work" + other.mkdir(parents=True) + assert paths.repo_identity(str(other), named).key == r.key + plain = paths.repo_identity(str(other), env) + assert plain.how == paths.BY_DIR and plain.key != r.key + + +def test_an_empty_value_changes_nothing_and_a_checkouts_own_remote_wins(tmp_path): + env = make_env(tmp_path) + export = tmp_path / "work" + export.mkdir() + assert paths.repo_identity(str(export), make_env(tmp_path, TOKENECONOMIZER_REPO="")).how == paths.BY_DIR + named = make_env(tmp_path, TOKENECONOMIZER_REPO="https://github.com/microsoft/TokenEconomizer") + # A checkout keeps the repository its `origin` names: a value left set in a shell must not + # merge every repository the person opens into one. + elsewhere = git_repo(str(tmp_path / "elsewhere"), "https://example.org/team/elsewhere.git") + assert paths.repo_identity(elsewhere, named) == paths.repo_identity(elsewhere, env) + # A checkout without an `origin` (a fresh `git init` in a sandbox) takes the named one. + fresh = git_repo(str(tmp_path / "fresh"), None) + assert paths.repo_identity(fresh, env).how == paths.BY_PATH + assert paths.repo_identity(fresh, named).key == paths.repo_identity(str(export), named).key diff --git a/tokeneconomizer/plugin/paths.py b/tokeneconomizer/plugin/paths.py index 6c4a27c..d52ae56 100644 --- a/tokeneconomizer/plugin/paths.py +++ b/tokeneconomizer/plugin/paths.py @@ -21,6 +21,8 @@ from dataclasses import dataclass HOME_ENV = "TOKENECONOMIZER_HOME" +# Names the repository of a session whose directory holds no git metadata, by its remote URL. +REPO_ENV = "TOKENECONOMIZER_REPO" def state_root(env=None): @@ -234,10 +236,19 @@ def repo_identity(cwd, env=None): it has one (every clone, worktree and re-clone), else the real path of its main checkout (a linked worktree shares its clone's), else -- outside any checkout -- the directory itself. The key's readable part is the repository's name (the remote's last component, or the main - checkout's folder), not the folder one particular checkout sits in.""" + checkout's folder), not the folder one particular checkout sits in. + + `$TOKENECONOMIZER_REPO`, when set, is the repository's remote URL where git names none: a + session in a directory that holds no git metadata -- an export of a checkout in a container + or a sandbox, a fresh folder per run -- or in a checkout without an `origin` is then keyed + as a clone of that remote is, and shares what was learned there. A checkout's own `origin` + wins, so a value left set in a shell never merges another repository's state into it.""" + env = os.environ if env is None else env found = find_repo_root(cwd, env) root = found or os.path.abspath(cwd or os.getcwd()) remote = normalize_remote(origin_url(root)) if found else "" + if not remote: + remote = normalize_remote(env.get(REPO_ENV) or "") if remote: how, canon, name = BY_REMOTE, remote, remote.rstrip("/").rsplit("/", 1)[-1] label = "/".join(remote.split("/")[-2:])