5. Changesets across repos
This is hawser’s signature feature — the one that pays for the whole tool. In this chapter you’ll take one feature that touches several repos and drive it as a single unit: one branch across all of them, cross-linked pull requests, and a merge that lands in the right order.
One feature, one review flow — even when the code is spread across repos and forges.
🎯 In this chapter, you’ll learn to…
- Understand what a changeset is: one feature = one branch across N repos.
- Start a changeset with
haw change start— the same branch in every repo, in one move. - Track the whole feature on one screen with
haw change status. - Open cross-linked PR/MRs with
haw change request— across GitHub, GitLab, and Bitbucket. - Merge in dependency order with
haw change land, somainnever breaks.
One feature, many repos: change start branches them all, status aggregates, request opens linked PRs.
😤 1. The pain this removes
Think back to the last time a feature spanned repos. You did this dance:
git checkout -b featurein repo A. And repo B. And repo C.- Push each. Open a PR in A. Then B. Then C.
- Chase reviews across three separate PR pages.
- Merge them — and remember that the shared library must merge before the services
that depend on it, or
mainbreaks.
Nothing tied those three PRs together. There was no single artifact that said “these are one feature.” A changeset is exactly that artifact: one feature = one branch across N repos, with linked PR/MRs and an ordered land.
Changesets need a real forge (GitHub, GitLab, or Bitbucket) and a token to open PRs.
The start and status steps are local and safe to try anywhere; request and land
talk to the forge. We’ll flag which is which.
🌱 2. Start the changeset
You name the feature and (optionally) the repos it touches:
haw change start FEAT-42 --repos api,billing,proto
changeset `FEAT-42` started across 3 repo(s):
proto -> change/FEAT-42
billing -> change/FEAT-42
api -> change/FEAT-42
In one move, haw created the same branch — change/FEAT-42 by default — in each of
the three repos. Now you make your changes and commit in each repo as normal Git. The
branch name ties them together.
A few useful options at start time:
--repos a,b,c— limit to the repos the feature touches (default: all repos).--branch <name>— use a custom branch name instead ofchange/<id>.--skip-branch— adopt whatever branch each repo is already on, instead of creating one.--label <l>— attach a label (repeatable) that gets forwarded to the PR/MRs later.
📊 3. Watch it come together — change status
At any point, get the whole feature on one screen:
haw change status FEAT-42
This aggregates, per repo: the branch, whether each repo is on it, whether it’s
dirty, its HEAD — and once PRs exist, the review state and CI status of each PR/MR.
Before any PRs are open it looks like this (run against the DEMO-1 changeset from the
“Your turn” below, so the repos are the two octocat clones):
changeset `DEMO-1`
REPO BRANCH ON IT DIRTY HEAD PR
hello-world change/DEMO-1 yes - 7fd1a60b —
spoon-knife change/DEMO-1 yes - d0dd1f61 —
(no PR/MRs yet — open them with `haw change request DEMO-1`)
Instead of three browser tabs, one dashboard. It’s the changeset equivalent of
haw status.
Tip: Add --format json to change status (it emits a stable haw.change-status/1
document) when you want to pipe the state into another tool or a script:
$ haw change status DEMO-1 --format json
{
"id": "DEMO-1",
"repos": [
{
"branch": "change/DEMO-1",
"dirty": false,
"head": "7fd1a60b01f91b314f59955a4e4d4e80d8edf11d",
"missing": false,
"name": "hello-world",
"on_branch": true,
"pr": null
}
],
"schema": "haw.change-status/1"
}
🔀 4. Open the pull requests — change request
When your branches are pushed and ready, one command opens cross-linked PR/MRs — one per repo — on whichever forge each repo lives on:
haw change request FEAT-42
Here’s the quietly powerful part: your repos don’t all have to be on the same forge.
haw speaks GitHub, GitLab, and Bitbucket, so a feature spanning a GitHub service
and a GitLab library gets a PR on GitHub and an MR on GitLab, cross-linked, from this
one command. Any labels you passed at start are forwarded here. Target a specific base
branch with --base <branch>.
This step needs a forge token in your environment — e.g. export GITHUB_TOKEN=$(gh auth token). haw reads tokens from env vars only, never stores them. Read-only steps
(start, status) need no token.
🛬 5. Land in dependency order — change land
Reviews are in, checks are green. Now merge — but in the right order. Remember proto
is a shared library that api and billing depend on. Merging a service before its
library lands is how you break main.
haw knows the order because your manifest declares it. Recall the deps key from the
microservices example:
[repo.gateway]
deps = ["proto"] # proto must land before gateway
So land merges the PR/MRs in stable topological order — dependencies first — and
stops at the first failure rather than leaving a half-merged mess:
haw change land FEAT-42
landing changeset `FEAT-42` in dependency order:
proto ✓ merged
billing ✓ merged
api ✓ merged
changeset `FEAT-42` landed.
Forge step — needs a token. Like request, land talks to the forge and requires
a token (e.g. export GITHUB_TOKEN=$(gh auth token)) plus merge rights on each repo. The
output above is illustrative: against the public read-only octocat repos this course
uses, request/land can’t push or merge, so don’t expect these lines without your own
writable repos and a token.
proto merged first because everything depends on it; the services followed. One command,
correct order, no broken main.
One command. Five repos merged in the right order. We did it.
change land merges the linked PR/MRs in topological order — dependencies first — and stops at the first failure.
🖼️ 6. The value, in one picture
one feature (FEAT-42)
│
┌────────────┼────────────┐
proto billing api ← change start: one branch across N repos
│ │ │
MR/PR PR PR ← change request: cross-linked, any forge
│ │ │
└──── land in deps order ──┘ ← change land: proto → billing → api
You never lost track of which PRs were “the feature,” you never merged in the wrong order, and you drove all of it from four commands. That’s the whole point of a changeset: a multi-repo feature that behaves like a single, coherent change.
Tip: Working across repos and want to hop into one? haw change goto FEAT-42 <repo>
prints its path so you can cd "$(haw change goto FEAT-42 api)". And haw change snapshot save <name> records every repo’s branch + HEAD so you can restore the exact
multi-repo state later.
🙌 Your turn
The local half of the flow needs no forge token, so try it in a workspace right now:
- Run
haw change start DEMO-1 –repos hello-world,spoon-knifeand confirm the samechange/DEMO-1branch appears in both repos (peek withhaw run ‘git branch –show-current’). - Run
haw change status DEMO-1and read the aggregated branch/dirty/HEAD — one dashboard instead of two tabs. - Now sketch it on paper: for a real feature spanning a shared lib and two services, in what order must they
land? (Lib first — that’s thedepskey doing its job.)
✅ Recap
- A changeset is one feature across N repos: a shared branch, linked PR/MRs, an ordered merge.
haw change start <id> --repos a,b,ccreates the same branch in each repo.haw change status <id>aggregates branch + review + CI across the whole feature.haw change request <id>opens cross-linked PR/MRs on GitHub, GitLab, and Bitbucket.haw change land <id>merges in dependency order (from each repo’sdeps), stopping on the first failure.start/statusare local;request/landneed a forge token in the environment.
👉 Next
You can ship a feature across the fleet. Now let’s build, test, and verify the whole thing — the daily workflow that keeps it green → 6. Build, test, and verify.