RSMultiGit - Rust Multi-Git
A fast CLI tool for managing multiple git repositories at once, written in Rust. RSMultiGit is a rewrite of pymultigit with native performance.
Features
- Repository discovery — automatically finds git repos via glob patterns or explicit folder lists
- Status inspection — count dirty, untracked, or non-synchronized repos using libgit2 (no subprocess overhead)
- Bulk operations — pull, clean, diff, grep, and branch inspection across all repos
- Build orchestration — run make, pydmt, rsconstruct, or bootstrap across all projects
- Flexible output — terse mode, statistics, inverted selection, and suppress-output options
- Error control — stop on first error or continue through all projects with
--no-stop
Philosophy
RSMultiGit follows the Unix philosophy: do one thing well. It discovers git repositories in the current directory tree and runs a single operation across all of them. No configuration files needed — everything is controlled via CLI flags.
Installation
Install from crates.io
cargo install rsmultigit
This downloads, compiles, and installs the latest published version into ~/.cargo/bin/.
Build from source
git clone https://github.com/veltzer/rsmultigit.git
cd rsmultigit
cargo build --release
The binary will be at target/release/rsmultigit.
To install it system-wide:
sudo cp target/release/rsmultigit /usr/local/bin/
Dependencies
RSMultiGit links against libgit2 (via the git2 crate) for native git repo inspection. The C library is compiled from source during the build, so no system packages are required beyond a C compiler and CMake (provided by your Rust toolchain).
Release profile
For an optimized binary, add the following to Cargo.toml:
[profile.release]
strip = true # Remove debug symbols
lto = true # Link-time optimization across all crates
codegen-units = 1 # Single codegen unit for better optimization
Getting Started
Basic usage
Navigate to a directory that contains git repositories as subdirectories, then run any rsmultigit command:
cd ~/git/myorg
rsmultigit list-projects
RSMultiGit will automatically discover git repos by looking for directories containing a .git folder. It searches both immediate subdirectories (*) and two levels deep (*/*).
Checking repository status
See which repos have uncommitted changes:
rsmultigit status
Count dirty repos with statistics:
rsmultigit --stats count-dirty
Find repos with untracked files:
rsmultigit --stats untracked
Pulling all repos
rsmultigit pull
Or quietly:
rsmultigit pull --quiet
Searching across repos
Grep for a pattern across all repositories:
rsmultigit grep "TODO"
Show only filenames:
rsmultigit grep --files "TODO"
Building all projects
Run make across all repos:
rsmultigit build-make
Run rsconstruct build on projects that have an rsconstruct.toml:
rsmultigit build-rsconstruct
Filtering projects
Only operate on specific folders:
rsmultigit --folders projectA,projectB status
Use a custom glob pattern:
rsmultigit --glob "python-*" status
Error handling
By default, rsmultigit stops on the first error. To continue through all projects:
rsmultigit --no-stop pull
Command Reference
Global Flags
These flags can be used with any subcommand and must appear before the subcommand name:
| Flag | Description |
|---|---|
--terse | Terse output — suppress project headers |
--stats | Print match count as N/total after count commands |
--no-output | Suppress command output (only print project names) |
--print-not | Invert selection — print repos that do NOT match |
--git-verbose | Pass --verbose to git commands |
--git-quiet | Pass --quiet to git commands |
--no-sort | Do not sort the project list |
--glob <PATTERN> | Glob pattern for project discovery (default: */*) |
--no-glob | Disable glob — check immediate subdirectories only |
--folders <LIST> | Comma-separated list of folders to operate on |
--no-stop | Do not stop on errors — continue to next project |
--no-print-no-projects | Suppress the “no projects found” message |
Example:
rsmultigit --stats --terse count-dirty # Just print "3/50"
rsmultigit --no-stop pull # Pull all, skip failures
rsmultigit --glob "python-*" status # Only match python-* dirs
rsmultigit --folders a,b,c list-projects # Operate on specific folders
Count Commands
These commands test each discovered repo and print matching projects.
rsmultigit count-dirty
Count repositories with dirty working trees (modified, deleted, or staged files). Uses libgit2 for fast native inspection.
rsmultigit count-dirty
rsmultigit --stats count-dirty # Print count as N/total
rsmultigit --terse --stats count-dirty # Print only the count line
rsmultigit untracked
Count repositories that have untracked files.
rsmultigit untracked
rsmultigit --stats untracked
rsmultigit synchronized
Count repositories that are not synchronized with their upstream (ahead or behind origin/<branch>).
rsmultigit synchronized
rsmultigit --stats synchronized
rsmultigit --print-not synchronized # Show repos that ARE synchronized
Status Commands
These commands inspect each repo and print output only for repos that have data.
rsmultigit status
Show git status -s output for repositories that are not clean.
rsmultigit status
rsmultigit dirty
Show git diff --stat output for repositories with modifications.
rsmultigit dirty
rsmultigit list-projects
List all discovered projects.
rsmultigit list-projects
rsmultigit age
Show the age of the last commit for each repo as a human-readable relative date.
rsmultigit age
rsmultigit authors
Show unique commit authors for each repo, sorted by number of commits.
rsmultigit authors
rsmultigit config <KEY>
Show a git config value across all repos. Repos where the key is not set are skipped.
rsmultigit config user.email
rsmultigit config remote.origin.url
rsmultigit size
Show the size of the .git directory for each repo. Useful for finding bloated repos.
rsmultigit size
rsmultigit last-tag
Show the most recent tag for each repo. Repos without tags are skipped.
rsmultigit last-tag
Action Commands
These commands run an action in each project directory.
rsmultigit branch-local
Show local branches for each repo.
rsmultigit branch-local
rsmultigit branch-remote
Show remote branches for each repo.
rsmultigit branch-remote
rsmultigit branch-github
Show the GitHub default branch for each repo (requires gh CLI).
rsmultigit branch-github
rsmultigit pull
Pull the current branch from origin.
rsmultigit pull
rsmultigit pull --quiet
rsmultigit push
Push the current branch to origin.
rsmultigit push
rsmultigit fetch
Fetch from origin without merging.
rsmultigit fetch
rsmultigit stash push
Stash working-tree changes in each repo.
rsmultigit stash push
rsmultigit stash pop
Pop the most recent stash in each repo.
rsmultigit stash pop
rsmultigit reset hard|soft|mixed
Reset HEAD across all repos.
rsmultigit reset hard # Discard all changes
rsmultigit reset soft # Keep changes staged
rsmultigit reset mixed # Unstage changes (default git behavior)
rsmultigit log
Show recent commits for each repo.
rsmultigit log # Show last 10 commits
rsmultigit log -n 5 # Show last 5 commits
rsmultigit tag local
List local tags for each repo.
rsmultigit tag local
rsmultigit tag remote
List remote tags for each repo.
rsmultigit tag remote
rsmultigit tag has-local
Show repos that have local tags (prints only the project header).
rsmultigit tag has-local
rsmultigit tag has-remote
Show repos that have remote tags (prints only the project header).
rsmultigit tag has-remote
rsmultigit remote
Show remote URLs for each repo.
rsmultigit remote
rsmultigit prune
Prune stale remote-tracking branches (git remote prune origin).
rsmultigit prune
rsmultigit gc
Run git garbage collection on each repo.
rsmultigit gc
rsmultigit checkout <BRANCH>
Checkout a branch across all repos.
rsmultigit checkout main
rsmultigit checkout develop
rsmultigit commit -m <MESSAGE>
Stage all changes and commit across all repos with a shared message.
rsmultigit commit -m "bump version"
rsmultigit submodule-update
Update submodules recursively (git submodule update --init --recursive).
rsmultigit submodule-update
rsmultigit blame <FILE>
Run git blame on a specific file across all repos. Repos where the file does not exist are skipped.
rsmultigit blame README.md
rsmultigit blame Makefile
rsmultigit clean-hard
Hard-clean each repository with git clean -ffxd. Warning: this removes all untracked and ignored files.
rsmultigit clean-hard
rsmultigit diff
Show git diff for each repository.
rsmultigit diff
rsmultigit grep <REGEXP>
Grep across all repositories. Output lines are prefixed with the project name.
rsmultigit grep "TODO"
rsmultigit grep --files "TODO" # Only show filenames
Build Commands
These commands run build tools in each project directory. Projects with a .disable file are skipped.
rsmultigit build-bootstrap
Run python bootstrap.py in each project.
rsmultigit build-pydmt
Run pydmt build in each project.
rsmultigit build-make
Run make in each project.
rsmultigit build-venv-make
Run make inside the project’s virtualenv (.venv/bin/make).
rsmultigit build-venv-pydmt
Run pydmt build inside the project’s virtualenv.
rsmultigit build-pydmt-build-venv
Run pydmt build_venv in each project.
rsmultigit build-rsconstruct
Run rsconstruct build on projects that have an rsconstruct.toml file. Projects without rsconstruct.toml are skipped.
rsmultigit build-rsconstruct
Utility Commands
rsmultigit version
Print detailed version information including git commit, branch, dirty status, and Rust compiler version.
rsmultigit version
Short version via flag:
rsmultigit --version
Configuration
RSMultiGit does not use a configuration file. All behavior is controlled via CLI flags passed before the subcommand.
Output control
| Flag | Default | Description |
|---|---|---|
--terse | false | Suppress project headers (=== name ===) |
--stats | false | Print match count (N/total) for count commands |
--no-output | false | Suppress command output in print-if-data commands |
--print-not | false | Invert selection — print non-matching repos |
Debug
| Flag | Default | Description |
|---|---|---|
--git-verbose | false | Pass --verbose to git commands |
--git-quiet | false | Pass --quiet to git commands |
Project discovery
| Flag | Default | Description |
|---|---|---|
--glob <PATTERN> | */* | Glob pattern for finding projects |
--no-glob | false | Disable glob, scan immediate subdirectories only |
--folders <LIST> | (none) | Comma-separated explicit folder list |
--no-sort | false | Preserve discovery order instead of sorting |
Error handling
| Flag | Default | Description |
|---|---|---|
--no-stop | false | Continue on errors instead of stopping |
--no-print-no-projects | false | Suppress “no projects found” message |
Build command skipping
Build commands (build-*) automatically skip projects that contain a .disable file in their root directory. The build-rsconstruct command additionally skips projects that do not have an rsconstruct.toml file.
Project Discovery
RSMultiGit discovers git repositories by searching for directories that contain a .git subdirectory.
Discovery modes
There are three ways RSMultiGit finds projects, checked in this order:
1. Explicit folders (--folders)
When --folders is provided, only those directories are considered. Non-git directories are silently skipped.
rsmultigit --folders /path/to/repoA,/path/to/repoB status
2. No-glob mode (--no-glob)
When --no-glob is set, RSMultiGit scans immediate subdirectories of the current directory:
rsmultigit --no-glob list-projects
3. Glob-based discovery (default)
By default, RSMultiGit uses the glob pattern */* to find projects two levels deep (e.g., org/repo). If no projects are found with */*, it automatically falls back to * to handle the common case where immediate subdirectories are git repos.
# Works from ~/git/veltzer (repos are at */*)
cd ~/git
rsmultigit list-projects
# Also works from ~/git/veltzer (repos are at *)
cd ~/git/veltzer
rsmultigit list-projects
A custom glob can be provided:
rsmultigit --glob "python-*" list-projects
rsmultigit --glob "org/team-*" list-projects
Sorting
By default, discovered projects are sorted alphabetically. Use --no-sort to preserve the filesystem discovery order.
How it works
- Collect candidate paths using the selected mode
- Filter to directories that contain
.git/ - Sort alphabetically (unless
--no-sort) - Pass the list to the selected subcommand’s runner
Architecture
Overview
RSMultiGit follows a simple pipeline: discover projects → run command → collect results.
Module structure
src/
main.rs Entry point, CLI dispatch
cli.rs Clap derive definitions (Cli + Commands)
config.rs AppConfig runtime struct
discovery.rs Project discovery via glob or folder list
runner.rs Three execution patterns
subprocess_utils.rs Shell command helpers
commands/
mod.rs Module declarations
count.rs git2-based repo inspection (dirty, untracked, synchronized)
status.rs git status / diff via subprocess
branch.rs Branch listing (local, remote, github)
pull.rs git pull
clean.rs git clean -ffxd
diff.rs git diff
grep.rs git grep with project-name prefix
build.rs Build commands (make, pydmt, rsconstruct, bootstrap)
Runner patterns
All subcommands use one of three runner functions:
do_count
For count commands (count-dirty, untracked, synchronized). Calls a test function on each project path (using libgit2, no subprocess), counts matches, optionally prints statistics.
do_for_all_projects
For action commands (pull, clean-hard, diff, grep, branch-*, build-*). Changes into each project directory, runs the action, prints a header. Respects --no-stop for error handling.
print_if_data
For status commands (status, dirty, list-projects). Changes into each project directory, calls a data function. If it returns Some(text), prints the project name and data. If None, the project is silently skipped.
Git inspection
The count commands (count-dirty, untracked, synchronized) use the git2 crate for direct repository inspection. This avoids forking git subprocesses and is significantly faster for large numbers of repos.
All other git operations use std::process::Command to run the git CLI, which provides familiar output formatting and handles edge cases that libgit2 may not cover.
Error handling
All functions return anyhow::Result. The --no-stop flag controls whether errors in individual projects are fatal (default) or logged and skipped.
Build script
The build.rs script embeds git metadata (commit SHA, branch, dirty status, describe) and the Rust compiler version at compile time. These are accessible via env!() macros and displayed by rsmultigit version.
Testing
RSMultiGit has both unit tests and integration tests.
Running tests
cargo test
Unit tests
Unit tests are defined as #[cfg(test)] modules inside each source file:
| Module | Tests | What’s tested |
|---|---|---|
cli | 11 | Subcommand parsing, global flags, argument validation |
commands::count | 7 | is_dirty, has_untracked, non_synchronized with temp git repos |
discovery | 6 | Folder, glob, and no-glob discovery modes |
runner | 11 | All three runner patterns with mock closures |
subprocess_utils | 6 | capture_output, check_call, check_call_ve |
Tests that change the working directory use serial_test::serial to avoid conflicts with parallel test execution.
Integration tests
Integration tests are in tests/ and run the compiled rsmultigit binary as a subprocess against temporary git repositories:
tests/
main.rs Test entry point, loads modules
common/mod.rs Shared helpers
tests_mod/
cli.rs Help, unknown subcommand, missing args
count.rs Count-dirty, untracked, terse, print-not
discovery.rs Immediate subdirs, nested, glob, folders
status.rs Status and dirty output
version.rs Version subcommand and --version flag
Test helpers (tests/common/mod.rs)
| Function | Description |
|---|---|
run_rsmultigit(dir, args) | Run the rsmultigit binary with given args in a directory |
stdout_str(output) | Extract trimmed stdout from command output |
stderr_str(output) | Extract trimmed stderr from command output |
setup_git_repos(names) | Create a temp dir with initialized git repos |
init_git_repo(path) | Initialize a single git repo with one commit |
Writing new tests
- Create a new file in
tests/tests_mod/ - Add a
#[path]module entry intests/main.rs - Use
setup_git_repos()to create test fixtures - Use
run_rsmultigit()to execute commands and assert on output
Example:
#![allow(unused)]
fn main() {
use crate::common::{run_rsmultigit, stdout_str, setup_git_repos};
#[test]
fn my_new_test() {
let tmp = setup_git_repos(&["repo1", "repo2"]);
let output = run_rsmultigit(tmp.path(), &["list-projects"]);
assert!(output.status.success());
let stdout = stdout_str(&output);
assert!(stdout.contains("repo1"));
}
}