Skip to content

Latest commit

 

History

History
339 lines (271 loc) · 13.8 KB

File metadata and controls

339 lines (271 loc) · 13.8 KB

Examples

A complete, runnable walkthrough of ezgitx on a realistic multi-repo workspace. Every command and block of output below is real. Run them and you'll get the same shape of result.

Captured 2026-06-13 against ezgitx 0.2.0; output updated for 0.4.0, which adds the build column to status, plus the verify and sessions commands. The upstream repos drift over time; if a step looks different from what you see, the exact commits this was run against are listed under Notes.

This walkthrough uses Claude Code as the coding agent. The ezgitx commands are identical everywhere. The pasted setup prompt in step 3 works with any agent, but ezgitx init-skill (step 8) writes a Claude Code skill file at .claude/skills/ezgitx/SKILL.md; other coding agents use different instruction-file conventions, so swap that step for yours.

A private multi-repo platform

Picture the backend platform at your company. It isn't one repo, it's five, each on its own release cadence:

  • two foundational utility libraries with no internal dependencies,
  • two higher-level libraries built on top of those,
  • and the service you actually ship, built on all of them.

You can't hand a stranger your company's private repos, so this walkthrough stands them in with five real, public packages that happen to have exactly that dependency shape: the aio-libs stack behind aiohttp. Treat the names as if they were your own internal repos.

Role in the "platform" Real repo Builds on (in this workspace)
foundational library multidict none
foundational library frozenlist none
higher-level library yarl multidict
higher-level library aiosignal frozenlist
the shipped service aiohttp multidict, yarl, frozenlist, aiosignal
graph TD
    multidict --> yarl --> aiohttp
    multidict --> aiohttp
    frozenlist --> aiosignal --> aiohttp
    frozenlist --> aiohttp
Loading

The point isn't aiohttp. It's that this is the shape of workspace ezgitx is built for: separate repos that build on each other locally. Unlike your private platform, you can run this one yourself.

1. Clone the workspace

mkdir acme && cd acme
git clone https://github.com/aio-libs/multidict.git
git clone https://github.com/aio-libs/frozenlist.git
git clone https://github.com/aio-libs/yarl.git
git clone https://github.com/aio-libs/aiosignal.git
git clone --recurse-submodules https://github.com/aio-libs/aiohttp.git

aiohttp vendors llhttp as a git submodule, so it needs --recurse-submodules or its build fails later. That's an aiohttp quirk, not an ezgitx one; your own repos may have none.

2. Create one shared environment

The repos install into a single environment so the higher-level ones build against the local copies of the lower-level ones, not published releases:

python3 -m venv .venv
source .venv/bin/activate
pip install -U pip setuptools wheel

Keep this environment active in the shell you run ezgitx from. ezgitx's child build commands inherit it.

3. Let your agent write the config

Don't hand-write the YAML. Open Claude Code at the workspace root and tell it to set ezgitx up. Since you know your own workspace, just describe the local relationships:

I'm adopting ezgitx (https://github.com/yuval-r/ezgitx) in this workspace. These five repos are a co-developed local stack, installed editable into the shared .venv: yarl builds on multidict, aiosignal builds on frozenlist, and aiohttp builds on all four. Generate .ezgitx.yml at the root with one platform group: each repo gets a pip install -e . build command, and depends_on edges for exactly those local relationships. aiohttp needs AIOHTTP_NO_EXTENSIONS=1 prefixed on its command. Then run ezgitx status to validate it.

The README has a fuller, evidence-first version of this prompt under Or let your agent write the config, which reads each repo's manifests and derives the commands itself.

The agent reads the repos and writes this:

version: 1
groups:
  platform:
    - path: ./multidict
      default_cmd: "pip install -e ."
      check_cmd: "python -c 'import multidict'"
    - path: ./frozenlist
      default_cmd: "pip install -e ."
      check_cmd: "python -c 'import frozenlist'"
    - path: ./yarl
      default_cmd: "pip install -e ."
      check_cmd: "python -c 'import yarl'"
      depends_on: ["multidict"]
    - path: ./aiosignal
      default_cmd: "pip install -e ."
      check_cmd: "python -c 'import aiosignal'"
      depends_on: ["frozenlist"]
    - path: ./aiohttp
      default_cmd: "AIOHTTP_NO_EXTENSIONS=1 pip install -e ."
      check_cmd: "python -c 'import aiohttp'"
      depends_on: ["multidict", "yarl", "frozenlist", "aiosignal"]

Two things to check in what it produced:

  • depends_on should appear only where a repo consumes another from this workspace, which here means the local editable installs, never a plain PyPI dependency. That rule is what makes staleness mean something, so don't let the agent wire an edge for a package that would come from the registry.
  • aiohttp's command carries AIOHTTP_NO_EXTENSIONS=1, because building its C speedups from a git checkout needs a Cython step pip won't run for you. A repo's per-repo default_cmd is where its build quirks belong.

4. Build everything, in dependency order

ezgitx run --all --with-deps

ezgitx builds the two foundational libraries first, then the two that depend on them, then the service. One JSONL line per repo as it finishes, then a summary (stdout trimmed here):

{"repo":"multidict","exit_code":0,"duration_ms":1984,"stdout_tail":"...Successfully installed multidict-6.7.2.dev0"}
{"repo":"frozenlist","exit_code":0,"duration_ms":5009,"stdout_tail":"...Successfully installed frozenlist-1.8.1.dev0"}
{"repo":"aiosignal","exit_code":0,"duration_ms":933,"stdout_tail":"...Requirement already satisfied: frozenlist>=1.1.0 in …/.venv/… (1.8.1.dev0)..."}
{"repo":"yarl","exit_code":0,"duration_ms":4454,"stdout_tail":"...Requirement already satisfied: multidict>=4.0 in …/.venv/… (6.7.2.dev0)..."}
{"repo":"aiohttp","exit_code":0,"duration_ms":992,"stdout_tail":"...Requirement already satisfied: multidict<7.0,>=4.5 …(6.7.2.dev0); yarl<2.0,>=1.17.0 …(1.24.2)..."}
{"type":"summary","total":5,"passed":5,"failed":0,"duration_ms":10482}

Look at yarl and aiohttp: Requirement already satisfied: multidict … (6.7.2.dev0). They resolved their dependency to the local checkout you just built, not a PyPI download, because ezgitx installed multidict first. Build yarl before multidict and pip pulls a published multidict instead. The ordering is the whole point.

Genuinely external dependencies (idna, propcache, and friends) still come from PyPI, as they should. ezgitx only orders what you declared.

5. The whole workspace at a glance

ezgitx status --all --human
REPO        BRANCH  HEAD     STATE  AHEAD  BEHIND  STALE_DEPS  BUILD
multidict   master  8b7c4d8  clean  0      0       -           fresh
frozenlist  master  0334ec8  dirty  0      0       -           fresh
aiosignal   master  2a67bbd  clean  0      0                   fresh
yarl        master  7b66654  dirty  0      0                   fresh
aiohttp     master  31702b2  clean  0      0                   fresh

Nothing is stale: every repo sits at the commit ezgitx last built it at, so the BUILD column reads fresh across the board. The STALE_DEPS column reads - for a repo with no declared upstreams and is blank when its upstreams are all fresh. (frozenlist and yarl show dirty because building C extensions writes generated files into their trees. In your own repos, .gitignore those build artifacts.)

6. Change something, and find what it breaks

You edit a foundational library and commit:

# ...edit multidict, then:
git -C multidict commit -am "tweak multidict internals"

multidict has now moved past the commit it was last built at. ezgitx notices, and flags everything downstream:

ezgitx status --all --human
REPO        BRANCH  HEAD     STATE  AHEAD  BEHIND  STALE_DEPS  BUILD
multidict   master  215c21e  clean  1      0       -           stale
frozenlist  master  0334ec8  dirty  0      0       -           fresh
aiosignal   master  2a67bbd  clean  0      0                   fresh
yarl        master  7b66654  dirty  0      0       multidict   stale
aiohttp     master  31702b2  clean  0      0       multidict   stale

The BUILD column flips to stale for multidict (its own HEAD moved past the recorded build) and for yarl/aiohttp (an upstream drifted); frozenlist and aiosignal are untouched and stay fresh.

ezgitx check-impact --repo multidict --human
REPO     DEPTH  VIA
aiohttp  1      multidict
yarl     1      multidict
2 affected downstream of multidict

No doc to keep up to date, no remembering that the service sits on top of the collections library. ezgitx computed the blast radius from the declared graph plus the commit it recorded at build time.

7. Rebuild only what changed

ezgitx run --repo aiohttp --with-deps

ezgitx rebuilds the service and every upstream it sits on that has moved since aiohttp was last built against it — here multidict (its own commit moved) and yarl (it was built against the old multidict) — in dependency order, and skips the rest:

{"repo":"multidict","exit_code":0,"duration_ms":1792,"stdout_tail":"...Successfully installed multidict-6.7.2.dev0"}
{"repo":"yarl","exit_code":0,"duration_ms":1233,"stdout_tail":"...Successfully installed yarl-1.22.0.dev0"}
{"repo":"aiohttp","exit_code":0,"duration_ms":1028,"stdout_tail":"...Successfully installed aiohttp-4.0.0a2.dev0"}
{"type":"summary","total":3,"passed":3,"failed":0,"duration_ms":4061}

frozenlist and aiosignal are left alone — nothing they depend on moved. yarl rebuilds even though its own source didn't change, because ezgitx records the commit each repo was built against: yarl was built against the old multidict, so it stays stale until rebuilt. That is the difference from a model that tracks only each repo's own commit, and it is what keeps a compiled or code-generated yarl from silently sitting on a stale multidict.

The flag is per-consumer. Had you rebuilt multidict for some other consumer, status would still show yarl with stale_deps: multidict until you rebuilt yarl itself — rebuilding a shared upstream for one consumer never clears another consumer's flag.

To push a change the other way — rebuild everything that sits on top of a repo you just changed — use the forward counterpart:

ezgitx run --repo multidict --with-dependents

That rebuilds multidict plus every stale dependent (yarl, aiohttp) in dependency order.

8. Confirm the change is green

Before you call a cross-repo change done, gate it. Say you've edited multidict but not committed yet. ezgitx verify finds every dirty repo, adds everything downstream of it, runs each one's check_cmd (fallback default_cmd) in dependency order, and won't go green until they all pass:

ezgitx verify
{"repo":"multidict","exit_code":0,"duration_ms":214,"stdout_tail":"","stderr_tail":"","truncated":false}
{"repo":"yarl","exit_code":0,"duration_ms":231,"stdout_tail":"","stderr_tail":"","truncated":false}
{"repo":"aiohttp","exit_code":0,"duration_ms":243,"stdout_tail":"","stderr_tail":"","truncated":false}
{"type":"verdict","verdict":"pass","checked":3,"failed":[]}

multidict is dirty, so it and its downstream closure (yarl, aiohttp) are checked; frozenlist and aiosignal are untouched and skipped. Had your edit broken aiohttp's import check, its run line would carry a non-zero exit_code and the final line would read {"type":"verdict","verdict":"fail","checked":3,"failed":["aiohttp"]}, with the command exiting 1. That single exit code is the gate: the agent isn't done until verify is green.

In a workspace several agents share, ezgitx sessions lists who currently holds a lock: one read-only line per live lock (repo, scope, pid, host, op, since), so a second agent can see that a repo is mid-pull before piling on:

ezgitx sessions
{"lock":"repo-aiohttp","scope":"repo","repo":"aiohttp","pid":48213,"host":"build-box","op":"pull","since":"2026-06-13T17:02:31Z"}

9. Teach your agent

Once you've seen ezgitx do its job, make every future session aware of it:

ezgitx init-skill

This writes .claude/skills/ezgitx/SKILL.md so a fresh Claude Code session in this workspace reaches for ezgitx on its own, without you re-explaining the layout.

Notes

  • These are real public packages used as a stand-in for a private workspace. You wouldn't normally co-develop them, but the dependency shape is exactly what ezgitx targets, and a public stack is something you can actually run.
  • ezgitx tracks staleness by commit: each repo records the commit it was built against for every upstream, so a downstream stays flagged until it is itself rebuilt. With the editable installs used here a downstream rebuild is often redundant (the import is live); in a compiled or code-generated workspace it is exactly what keeps artifacts honest. --with-deps walks upstreams, --with-dependents walks downstreams.
  • status surfaces that same freshness as a build: fresh|stale column for every repo (stale = no recorded build, own HEAD moved, or an upstream drifted). ezgitx verify is a gate, not a build: it runs check_cmd (fallback default_cmd) and records no freshness, so running it never changes what status/run consider stale.
  • This walkthrough was captured against these upstream commits. If the repos have moved since and a step diverges, check these out to reproduce it exactly: multidict 8b7c4d8, frozenlist 0334ec8, yarl 7b66654, aiosignal 2a67bbd, aiohttp 31702b2.