A Rust CLI tool for managing multiple Git repositories at once. Reads the list of target repos from ~/.config/rsmultigit/config.toml and runs bulk operations (status, pull, build, grep, check same, etc.) across all of them. Rewrite of pymultigit for native speed.
rsmultigit requires a config file at ~/.config/rsmultigit/config.toml. Tests override this via the RSMULTIGIT_CONFIG env var. Run rsmultigit setup config-sample to print a worked example; its source is assets/config-example.toml (embedded at compile time via include_str!). rsmultigit setup interactive (commands/setup.rs, prompts via inquire) writes a minimal config interactively; every question has a flag (--repos-dir, --build/--no-build, --overwrite) so tests drive it without a terminal.
repos = [...]— list of shell-expanded globs. Matches that aren't git repos are filtered out.default_build_method = "..."— optional; what a barersmultigit buildruns (same spellings as the CLI:bootstrap,make,rsconstruct,cargo). Deserialized straight intocli::BuildWhat, so the CLI enum and the config key can never drift. An explicit method on the command line wins.crates_io_pass_entry = "..."— optional, defaultkeys/crates.io; the pass(1) entrycargo releasereads the crates.io token from whenCARGO_REGISTRY_TOKENis unset. The token only ever travels through the child process environment.[[check-same]]blocks — consumed bycheck same(andcheck all). Fields:name,select,exclude?,marker?,marker_absent?(drop repos containing this file — the in-repo opt-out, e.g..noci),path,enabled?(default true),must_have?(default false; when true, in-scope repos missingpathare violations).[[check-submodules]]blocks — consumed bycheck submodules(andcheck all), implemented incommands/check_submodules.rs. Each names a submodule byurl(matched after normalising scheme, user,.gitand case on both sides) and fails when the selected repos that carry it have it checked out at different commits. Same selection fields as[[check-same]](name,select,exclude?,marker?,marker_absent?,enabled?). Only declared submodules are checked; a selected repo without the submodule is skipped.[[check-exists]]blocks — consumed bycheck exists(andcheck all). Presence-only: they assert every selected repo haspath, and never compare content, which is what makes them right for files that legitimately differ per repo (README.md). Same selection fields as[[check-same]](name,select,exclude?,marker?,marker_absent?,path,enabled?); nomust_have, since requiring the file is the whole rule.
cargo build # Debug build
cargo build --release # Release build
cargo nextest run # Run tests (preferred runner)
cargo nextest run --release # Run tests in release mode
cargo nt # Alias for nextest runTests use cargo-nextest (not cargo test). Config in .config/nextest.toml (4 threads, fail-level reporting).
src/
├── main.rs # Entry point, command dispatch
├── cli.rs # Clap derive CLI definitions (commands + global flags)
├── config.rs # AppConfig: the global flags (a clap Args struct flattened into Cli)
├── runner.rs # Three runner patterns for executing across repos
├── subprocess_utils.rs # The Cmd builder, check_call/capture_output, per-thread output capture
├── git_url.rs # Git remote URL parsing: identity, GitHub owner/name
├── github.rs # GitHub API client (octocrab + tokio), token from GH_TOKEN / `gh auth token`
└── commands/ # Command modules (one per operation); `check.rs` owns config-file
# parsing and rule evaluation, `check_run.rs` the check-* drivers
tests/
├── main.rs # Integration test entry
├── common/mod.rs # Test helpers (setup_git_repos, run_rsmultigit)
└── tests_mod/ # Integration test modules
docs/ # mdBook documentation
build.rs # Embeds git metadata at compile time
All commands use one of three patterns in runner.rs:
do_count— Boolean test per repo; prints the matching repos and a count summary. Used by:git count dirty/untracked/unsynchronized/ahead/behindandgit tag has-local(git2, no subprocess), andgit tag has-remote(spawnsgit ls-remote).do_for_all_projects— Runs an action (Result<()>) in each repo dir; the_with_checkvariant runs a cheap predicate first and skips repos where it is false. Skips are decided only there, never inside the action, because the serial path prints the header before the action runs. Used by:git pull/push/fetch/clean,build, etc.print_if_data— Calls data function returningOption<String>, prints only if Some. Used by:git status,git dirty,git grep,git age,git authors,git size,git last-tag.
- Edition 2024 Rust
- Error handling:
anyhow::Result<T>everywhere, with.context()for error messages - GitHub: through the API client in
github.rs(octocrab), never by spawning theghCLI; the onlyghcall left isgh auth token, once per run, when noGH_TOKEN/GITHUB_TOKENis set. Responses are deserialized into typed structs; GraphQL values (owner, name, cursors, node ids) travel as variables, never inside the query text. The rest of the program is synchronous: callgithub()?.block_on(...)from the per-repo function. - Git inspection: Prefer the
git2crate for everything libgit2 can do — subprocess startup times 260 repos dominates runtime (see "Git inspection" indocs/src/architecture.mdfor the recorded benchmark). Use thegitCLI subprocess only for network ops (pull/push/fetch) and commands whose value is git's own output formatting (log,blame,grep). - Command module pattern: Each command is a simple
pub fnreturningResult<()>(action),Result<bool>(predicate or count test) orResult<Option<String>>(data). A module that prints its own lines usessubprocess_utils::out_line, neverprintln!, so the parallel runner and--no-outputsee them - No rustfmt.toml or clippy.toml — uses Rust defaults
- Release profile:
strip = true,lto = true - Tests: Unit tests in
#[cfg(test)]modules within source files. Integration tests intests/. Usetempfile::TempDirfor isolation; tests take explicit repo paths and never change the working directory.
This repo uses the canonical .github/workflows/ci.yml shared byte-identically
by all rs* repos (canonical copy in rsconstruct — edit it there, not here; the
rs-workflow-ci rule in check same guards against drift). The same applies
to build.rs, release.toml, deny.toml, .config/nextest.toml,
scripts/ci-install-tools.sh and docs/src/release-info.md.
- Every branch push:
cargo fmt --check, build, clippy (-D warnings),cargo deny check,cargo nextest run. - Release: triggered by a
chore: Release ...commit (as written bycargo release) landing on the default branch — thev*tag push itself triggers nothing. Builds binaries for Linux x64/ARM64 and macOS x64/ARM64, namedrsmultigit-{linux,macos}-{x86_64,aarch64}, and attaches them to a GitHub release for the tag computed fromCargo.toml(openssl is vendored unconditionally via thegit2feature in Cargo.toml, so no--featuresflag is needed). - Docs: mdBook deployed to GitHub Pages on release and on manual dispatch.
The book lives in
docs/; keepdocs/src/commands.mdand README.md in step withcli.rswhen adding or renaming a subcommand or flag.
Runtime deps — keep it minimal:
clap(derive) — CLI parsingclap_complete— shell completionsgit2— native git operationsglob— pattern matchinganyhow— error handlingserde+toml— config-file parsingsha2— SHA-256 hashing forcheck sameshellexpand— tilde/env expansion in config pathsregex-lite—--checks-rerule matchingsimilar— unified diffs forcheck same --diffcamino— UTF-8 paths throughoutindicatif— progress bar in the parallel runneroctocrab(default features off: no JWT/aws-lc-rs) +tokio+futures-util— the GitHub API client, its runtime, and request concurrencyserde_json— GitHub request and response bodiesanstream— prints our own styled help (the bare-command listing) with clap's colour detection (tty,NO_COLOR,CLICOLOR_FORCE)
Licenses and advisories are gated by the fleet-shared deny.toml.