Contributing
PRs welcome. This page is the orientation: where the code lives, how to build and test, what we care about in reviews, and what to skip.
Repository layout
Cargo workspace at the root, with a apps/ directory for the JS apps and a
docs/ directory for design specs. Five Rust crates, two web apps:
flowchart LR
root(("<b>giff/</b>"))
crates["<b>crates/</b>"]
apps["<b>apps/</b>"]
docs["<b>docs/superpowers/</b><br/><span style='color:#86868b;font-size:11px'>specs & plans</span>"]
compose["<b>docker-compose.yml</b><br/><span style='color:#86868b;font-size:11px'>runs the runner</span>"]
gaps["<b>GAPS.md</b><br/><span style='color:#86868b;font-size:11px'>known gaps</span>"]
root --> crates
root --> apps
root --> docs
root --> compose
root --> gaps
core["<b>giff-core</b><br/><span style='color:#86868b;font-size:11px'>pure Rust · no I/O</span>"]
git["<b>giff-git</b><br/><span style='color:#86868b;font-size:11px'>GitBackend · shells out to git</span>"]
forge["<b>giff-github</b><br/><span style='color:#86868b;font-size:11px'>ForgeBackend · ureq, sync</span>"]
cli["<b>giffstack (CLI)</b><br/><span style='color:#86868b;font-size:11px'>crate at crates/giff-cli/<br/>ships the giff binary</span>"]:::brand
runner["<b>giff-runner</b><br/><span style='color:#86868b;font-size:11px'>axum + sqlite + worker</span>"]:::brand
wasm["<b>giff-wasm</b><br/><span style='color:#86868b;font-size:11px'>future browser binding</span>"]
crates --> core
crates --> git
crates --> forge
crates --> cli
crates --> runner
crates --> wasm
web["<b>web</b><br/><span style='color:#86868b;font-size:11px'>dashboard · adapter-static</span>"]
site["<b>docs</b><br/><span style='color:#86868b;font-size:11px'>this site · adapter-vercel</span>"]
apps --> web
apps --> site
classDef brand fill:#ff0035,stroke:#ff0035,color:#ffffff;Architectural rules
giff-coreis I/O-free. Nostd::fs,std::process, network, notokio. Anything touching the outside world lives ingiff-git,giff-github, the CLI, or the runner. Keeps the core compilable to WASM without a porting layer.giff-githubis sync. Usesureq, not async. Async-ifies only inside the runner, which wraps sync calls intokio::task::spawn_blocking.- The CLI is single-binary. Don't introduce daemons, IPC, or background services in the CLI — that's the runner's job.
- The web dashboard talks only to GitHub directly. No backend dependency. Static deploy.
Build & test
# clone git clone https://github.com/nidheesh-m-vakharia/giff.git cd giff # build everything cargo build # test everything (~120 tests as of this writing) cargo test # build a single crate cargo build -p giffstack cargo test -p giff-runner # run a single test cargo test -p giff-core stack_frame_bottom_has_no_parent # the CLI in dev mode cargo run -p giffstack -- log --all
For the JS apps:
cd apps/web # or apps/docs npm install npm run dev # local dev server npm run check # svelte-check npm run build # production build
What "done" looks like for a PR
Roughly, in priority order:
- Tests for the change. New behaviour gets a test that would have failed before. Bugfixes get a regression test.
- CI green.
.github/workflows/ci.ymlruns Rust build + test, fmt, clippy, web type-check + build, and docs type-check + build on every PR. PRs don't merge with red CI. - The whole workspace builds locally too.
cargo build+cargo testfrom the workspace root. - No new TypeScript / Svelte errors.
npm run checkfor whichever apps you touched. - Style matches the surrounding code. Rust uses
rustfmt; JS uses Prettier (runnpm run lint). - One commit per PR. Yes, even when developing this tool we eat our own dog food. Use
giff publish+giff commit --amendto keep the layer clean.
Releases
Pushes to main trigger .github/workflows/release.yml, which auto-publishes
any of giff-core, giff-git, giff-github, giffstack
whose Cargo.toml version is newer than the version on crates.io. Pushes without a
version bump are no-ops. The workflow tags the repo v<giffstack-version> and
creates a GitHub release when giffstack publishes.
The publish job is scoped to a GitHub environment named crates-io. Setup:
- Repo Settings → Environments → New environment, name it
crates-io. - Add a secret
CARGO_REGISTRY_TOKENto that environment (token from crates.io/me). - (Optional but recommended) under "Deployment branches" restrict to
main, and tick "Required reviewers" with one or more maintainers.
With reviewers enabled, every push to main that bumps a version pauses the workflow
with an "Approve deployment" prompt before the secret is injected and cargo publish
runs — a manual gate against accidental publishes.
To cut a release: bump the version in the relevant Cargo.toml(s), commit, push to main, approve the deployment.
What we look for in reviews
- Validation invariants. Adding a stack mutation? Add a
stack.validate()call after the mutation. Adding a new error path on an external call? Hook into the retry queue if the operation is retry-safe. - Idempotent operations. The runner re-tries everything, the CLI re-runs after crashes. Operations that hit GitHub need to handle "already in target state" cleanly.
- Error messages with a fix in them. "could not derive a branch name from
!!!" is OK; "ParseError(InvalidInput)" is not. Tell the user what they can do about it. - Don't leak abstractions across crates. If
giffstackneeds something fromgiff-corethat isn't there, add it togiff-corewith a test. Don't reach in and re-implement.
What we don't want
- Dependencies-as-features. Adding
tokiotogiffstack"because it'd be nicer with async" is a no. The CLI staying sync keeps binary size and startup time predictable. - Premature multi-tenant work. The runner is single-tenant by design. Don't add
tenant_idcolumns or auth scaffolding until Phase 2 lights up. - SaaS-only features in the open-source repo. If a feature only makes sense behind paid SaaS billing, it doesn't belong here.
- Refactors without a tied feature. "Reorganising for clarity" PRs are politely declined unless they're paired with new work that benefits from the reorg.
Filing issues
What to include:
- The command you ran (full
giff ...invocation). giff --versionoutput.giff log --allif relevant — shows the stack state.- What you expected to happen vs what did.
- If it's a runner issue, the relevant lines from
docker compose logsand the contents of/data/state.db'seventstable around the time of the issue.
Where to start
Good first PRs:
- Pick a P3 from
GAPS.md— they're small, scoped, and won't conflict with anything in flight. - Improve a CLI error message you found unhelpful.
- Add a test for a code path that doesn't have one.
- Fix a typo in these docs.
Bigger work — discuss in an issue first so we can sanity-check the direction.