3 releases
Uses new Rust 2024
| 0.1.2 | Jul 16, 2026 |
|---|---|
| 0.1.1 | Jul 16, 2026 |
| 0.1.0 | Jul 16, 2026 |
#881 in Network programming
Used in hawser
150KB
3.5K
SLoC
hawser
Reproducible multi-repo stacks + cross-repo PR/MR orchestration. One binary, one TUI. In Rust.
Install · Quick start · Demos · Docs · Try the TUI in your browser
haw assembles a software stack out of many independent Git repositories — without
submodules, without detached HEADs, without a Python runtime. A declarative manifest
(haw.toml) describes stacks and the repos they are composed of; a committed
lockfile (haw.lock) pins every repo to an exact revision, so any teammate or CI
machine reproduces the exact same tree.
On top of composition, haw drives the day-to-day multi-repo workflow: branch a
feature across all affected repos at once, open the linked PRs/MRs on GitHub and
GitLab, and track review + CI state from one keyboard-driven cockpit.
Highlights
- Reproducible.
haw.lockpins every repo to a SHA — byte-identical run-to-run, cross-OS (a real argument in automotive/avionics audits). - No magic on disk. Repos are plain, autonomous clones. No symlinks, no detached
HEAD — works on Windows where
repostruggled. - Stacks compose. Named sets of repos share the same clones; overlays override revs per variant without duplicating repo lists.
- Changesets. One feature across N repos: one branch everywhere, N cross-linked
PR/MRs, one aggregated status,
landmerges in dependency order. - A k9s-grade TUI. Bare
hawopens the fleet cockpit: live auto-refresh, fuzzy/filter, column sort, marks + bulk actions, drill-ins that show a repo's git detail / a PR's checks / a CI run's steps — thenMmerge,Aapprove,Ccheckout, all from the keyboard.:command bar mirrors the CLI; six themes. - Fast and native. Reads go through gitoxide;
only the heavy plumbing shells out to
git. Parallelsync/run/build/test. - CI-friendly.
haw verifyexits 3 on drift,--format jsonwhere it matters,NO_COLOR/CLICOLOR_FORCEhonored likebat,eza,ripgrep.
Demos
Rendered with VHS from the tapes in
demo/ — CI re-renders them on every CLI/TUI change, so they never lie.
The CLI — sync, tree, status, cross-repo changesets, in full color:
The TUI cockpit — bare haw, k9s-style, keyboard-first:
Try the cockpit in your browser → — real
ratatui widgets over Ratzilla, Rust compiled
to WASM, no server. Source: site/.
Guided walkthroughs
Feature-by-feature tapes, paced to read along (demo/):
| Tape | Teaches |
|---|---|
cli-compose |
tree → sync → status → lock → pin → switch — the composition lifecycle |
cli-changeset |
change start / status across repos; where request / land open PR/MRs |
cli-run-verify |
parallel run, and verify as a CI drift gate (exit 3) |
cli-merge |
the collaborative merge: plan → resolve → cleanup |
The TUI demo above runs against a built-in demo controller (haw dash --demo) — no
workspace, git, or network — so the fleet PR/MR and CI views are always populated.
Install
Quick install — pick your package manager (all install the haw binary):
cargo install hawser # Rust / crates.io (canonical)
brew install nastwinns/tap/hawser # macOS + Linux (Homebrew)
scoop bucket add nastwinns https://github.com/Nastwinns/scoop-bucket && scoop install hawser # Windows (Scoop)
Linux / static / air-gap. A zero-dependency static musl binary is the recommended universal Linux install — no glibc, no runtime, drops into containers and air-gapped hosts as a single file:
curl -sSL https://github.com/Nastwinns/hawser/releases/download/v0.1.0/haw-0.1.0-x86_64-unknown-linux-musl.tar.gz \
| tar xz && sudo install haw /usr/local/bin/
Prebuilt archives (signed). Every platform — x86_64/aarch64 Linux (glibc), x86_64
musl (static), x86_64/aarch64 macOS, x86_64 Windows — ships on the
GitHub Release with a .sha256
and a cosign signature (.sig/.pem) — verify offline before installing on
locked-down hosts.
Docker:
docker build -t haw . && docker run --rm haw --version
From source:
cargo install --git https://github.com/Nastwinns/hawser hawser # latest main
cargo build --release # in a clone
Full channel matrix, signature verification, and the air-gap workflow: docs/INSTALL.md.
Quick start
haw init examples/quickstart/haw.toml # bootstrap from a ready-made example
haw sync # clone every repo, write haw.lock
haw # open the TUI cockpit
New here? The examples/ directory has runnable, copy-pasteable
manifests and workflows to learn from.
A typical session — compose, inspect, branch across repos:
$ haw tree
haw.toml
├─ gateway
│ ├─ kernel v6.1.2 (git@gitlab.company.com:firmware/kernel.git)
│ ├─ hal main (git@gitlab.company.com:firmware/hal.git)
│ └─ app-mqtt release/2.x (git@github.com:acme/app-mqtt.git)
└─ sensor-node
├─ kernel v6.1.2 (git@gitlab.company.com:firmware/kernel.git)
└─ hal main (git@gitlab.company.com:firmware/hal.git)
$ haw status
REPO BRANCH HEAD DIRTY DRIFT
kernel v6.1.2 a1b2c3d4 - -
hal main 9f8e7d6c yes -
app-mqtt release 4d5e6f7a - YES
$ haw change start FEAT-42 --repos kernel,app-mqtt
changeset `FEAT-42` started across 2 repo(s):
kernel -> change/FEAT-42
app-mqtt -> change/FEAT-42
Color on a TTY, plain when piped, NO_COLOR honored — one shared scheme everywhere:
cyan repo/stack names, yellow revs and branches, dim SHAs, green ✓ clean,
yellow dirty, red drift.
How it composes
One manifest declares repos (the Git repositories) and composes them into stacks (named sets of repos). A repo is shared, never duplicated. A committed lockfile pins every repo to an exact SHA.
haw.toml (intent) haw.lock (pinned SHAs, committed)
│ │
┌────────────┼────────────┐ ▼
▼ ▼ ▼ reproducible on any machine / CI
┌─────────┐ ┌─────────┐ ┌──────────┐
│ kernel │ │ hal │ │ app-mqtt │ ← repos (full autonomous git clones)
└────┬────┘ └────┬────┘ └────┬─────┘
│ │ │
├──────┬─────┤ │ stacks reuse the SAME repos,
▼ │ ▼ ▼ no submodules, no detached HEAD, no symlinks
┌──────────┴──┐ ┌────────────┴─────┐
│ gateway │ │ sensor-node │ ← stacks (compositions)
│ kernel+hal │ │ kernel + hal │
│ +app-mqtt │ │ │
└─────────────┘ └──────────────────┘
On disk — no symlinks, ever:
mystack/
├── haw.toml # manifest (intent)
├── haw.lock # lockfile (resolved SHAs, committed)
├── kernel/ # real, complete git repo
├── hal/ # real, complete git repo
└── app-mqtt/ # real, complete git repo
Object sharing across stacks on one machine is an opt-in optimization via git's
native alternates (git clone --reference) — a text file, not a symlink.
Manifest example
[remote.internal]
url = "git@gitlab.company.com:firmware"
[repo.kernel]
remote = "internal"
repo = "kernel.git"
rev = "v6.1.2" # tag or sha => pinned & reproducible
groups = ["firmware"]
[repo.hal]
remote = "internal"
repo = "hal.git"
rev = "main" # branch => follows head, until locked
[repo.app-mqtt]
url = "git@github.com:acme/app-mqtt.git"
rev = "release/2.x"
path = "apps/mqtt" # optional; defaults to the repo name
[stack.gateway]
repos = ["kernel", "hal", "app-mqtt"]
[stack.sensor-node]
repos = ["kernel", "hal"] # shares kernel + hal, no duplication
[overlay.dev.repo.kernel]
rev = "main" # `haw sync --overlay dev`: kernel follows main
Command surface
haw Open the TUI cockpit (no subcommand)
├── init <manifest-url|path> Bootstrap a workspace from a manifest
├── sync [--stack S] [--shared] Clone/pull repos to the state in haw.lock
├── lock / pin / unpin Resolve revs -> haw.lock / pin to checkouts / restore
├── switch <stack> Materialize a different stack in the workspace
├── status Aggregated fleet status (dirty/ahead/behind per repo)
├── run '<cmd>' Run a command across repos, in parallel
├── tree Print the stack -> repo tree
│
├── repo add|remove|list Edit repos in the manifest
├── stack add|remove|list Edit stacks in the manifest
│
├── verify Assert tree == haw.lock; exit 3 on drift (CI gate)
├── build / test Run each repo's declared build/test command, in parallel
├── hooks install|list Git integrity pre-commit + lifecycle hooks (.haw/hooks)
├── evidence Bundle manifest+lock+audit+status for audits
│
├── change Cross-repo feature ("changeset") workflow
│ ├── start <id> [--repos ..] Create one branch across the affected repos
│ ├── status Per-repo branch + PR/MR review + CI dashboard
│ ├── request Open linked PR/MRs on GitHub/GitLab for each repo
│ ├── goto Interactive picker; cd into a repo
│ ├── snapshot save|restore Save/restore the multi-repo state of a changeset
│ └── land Merge PR/MRs in dependency order
│
├── merge Parallel collaborative merge
│ ├── plan <source> Slice a big merge into per-directory conflict units
│ ├── resolve <slice> Resolve one slice (--take ours|theirs, or by hand)
│ └── status / cleanup / abort Track, seal, or undo the planned merge
│
├── import --from <west.yml|default.xml> Convert a west/repo manifest to haw.toml
└── dash Open the fleet dashboard (same as bare `haw`)
Verbs are one guessable word each; old names (graph, forall, freeze, tui)
stay as hidden aliases. Full lexicon: docs/CLI-DESIGN.md.
The TUI cockpit
Keyboard-first, modal, k9s-style — a genuine daily driver, not a status readout. The loop is read → drill in → act: see a repo's branch/SHA/status, inspect a PR's reviewers and CI checks, then merge or approve it — all without leaving the terminal. Everything runs on a background worker, so the UI never freezes.
haw ▸ ~/work/gateway ───────────────────────── stack: gateway lock: ✓ repos: 3/3
──────────────────────────────────────────────────────────────────────────────────────
REPO BRANCH ▲ HEAD DIRTY DRIFT ↑ / ↓ MERGE
kernel v6.1.2 a1b2c3d4 · · 0 / 0 —
◉hal main 9f8e7d6c yes · 2 / 0 —
▸app-mqtt release/2.x 4d5e6f7a · DRIFT 0 / 5 —
──────────────────────────────────────────────────────────────────────────────────────
hal › path hal/ branch main (ahead 2) dirty locked 9f8e7d6c grp firmware
──────────────────────────────────────────────────────────────────────────────────────
[s]ync [space]mark [S]witch [m]PRs [i]CI [v]gov [/]filter [<>]sort [:]cmd [?]help
Live — the fleet auto-refreshes (~5s when idle, on-demand F5/ctrl-r); the
grid stays current hands-free without ever disturbing input, overlays, or in-flight
work. Network views (PR/MR, CI, governance) stay strictly on-demand.
Find & order — / is a fuzzy filter (nucleo-based, case-insensitive: /knl
matches kernel). >/< move the active sort column and . toggles asc/desc; a
▲/▼ caret marks the sorted column on the Fleet, PR/MR, and CI tables.
Read → drill in (Enter) — on a fleet repo, drill into a scrollable git detail
(branch, short SHA, status, recent log, last-commit diffstat, remotes); on a
PR/MR, its reviewers, CI checks, body, and url; on a CI run, its jobs, steps, and
conclusion. Scroll with j/k and PageUp/PageDown, b to go back.
Act (confirm-gated, they reach the network) — M merges a PR/MR, A approves
it, C checks out its branch locally (haw-pr-<n>) to review the code. Cross-repo
changesets R request and L land in dependency order. All ask a y/n first.
Marks + bulk — space marks repos in the Fleet (shown as ◉); with marks set,
s syncs and r runs across just the marked set instead of the cursor row.
Views — stacks → fleet grid → repo detail; changesets with per-repo PR/MR review
- CI cells; fleet-wide open PR/MRs (
m) and recent CI runs (i) across every repo; governance (v) — registered plugins, SBOM/provenance artifacts, findings;oopens the cursor row (PR / run / artifact) in your browser.
Command bar : mirrors the CLI verbs, so learning one teaches the other:
:sync, :switch NAME, :run CMD, :tree, :prs, :ci, :governance/:plugins,
:change …, :merge …, :help.
Themes — six built-in skins (catppuccin default, dracula, nord, gruvbox,
solarized, monochrome). NO_COLOR forces monochrome; HAW_THEME=<name> picks
one at startup; :theme <name> switches live.
Full keymap: docs/CLI-DESIGN.md.
Why hawser exists
Splitting a stack across repositories is routine in embedded/automotive/avionics (shared BSW/HAL/MCAL repos reused across ECUs) and microservice backends. Existing tools each solve one slice:
| Tool | Gives you | Misses |
|---|---|---|
Google repo / west |
manifests | lockfile; Python runtime; detached HEADs; symlinks vs Windows |
| RepoFleet (Go) | issue → branches → PR/MR flow | stack composition; reproducible pinning |
| mergetopus (Rust) | parallel single-repo merges | multi-repo anything |
haw is the union nobody ships: reproducible stack composition + cross-repo
MR orchestration + optional parallel collaborative merge, behind one binary
and one TUI. It orchestrates Git and the forge APIs — it does not reimplement
Git's merge engine, replace a forge, or replace domain toolchains.
Development
cargo test --workspace # all green (unit + integration)
cargo fmt --all --check
cargo clippy --workspace --all-targets -- -D warnings
Covered: manifest parse + referential validation, TOML round-trip, resolver +
overlay precedence, lockfile read/write and determinism (byte-identical, LF-only,
cross-OS in CI), changeset lifecycle, the full collaborative merge against real git
repos, golden CLI-output snapshots (crates/hawser/tests/golden.rs), forge
orchestration against a fake forge, and the cockpit logic (fuzzy filter, cursor,
sorting, marks, command bar, drill-ins, theme selection, fleet PR/CI/governance
views).
Workspace layout:
| Crate | Role |
|---|---|
haw-core |
Manifest, lockfile, resolver, workspace, changesets — all domain logic |
haw-git |
Git backend: gitoxide reads, git shell-outs for plumbing |
haw-forge |
GitHub/GitLab behind one Forge trait; changeset + fleet orchestration |
haw-merge |
Collaborative merge: plan/resolve/cleanup/abort |
haw-tui |
The ratatui cockpit — renders and dispatches, nothing more |
hawser |
The haw binary: clap CLI, thin glue |
Documentation
Published at nastwinns.github.io/hawser/docs (mdBook, rebuilt on every push). Sources:
| Doc | What |
|---|---|
| docs/ARCHITECTURE.md | Crate layout, data flows, phased implementation plan |
| docs/CLI-DESIGN.md | Full CLI lexicon + TUI keymap |
| docs/EXTENDING.md | Extensions, plugins, hooks, auth, CI/CD integration |
| docs/PLUGINS.md | Writing subcommand plugins — haw <name> runs haw-<name> from PATH |
| docs/COMPLIANCE.md | Tool qualification, SBOM/CRA, crypto/signing, GDPR |
| docs/INSTALL.md | Full install matrix + signature verification |
License
Dual-licensed under MIT or Apache-2.0, at your option.
Dependencies
~28–45MB
~706K SLoC