Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

FlagDescription
--terseTerse output — suppress project headers
--statsPrint match count as N/total after count commands
--no-outputSuppress command output (only print project names)
--print-notInvert selection — print repos that do NOT match
--git-verbosePass --verbose to git commands
--git-quietPass --quiet to git commands
--no-sortDo not sort the project list
--glob <PATTERN>Glob pattern for project discovery (default: */*)
--no-globDisable glob — check immediate subdirectories only
--folders <LIST>Comma-separated list of folders to operate on
--no-stopDo not stop on errors — continue to next project
--no-print-no-projectsSuppress 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

FlagDefaultDescription
--tersefalseSuppress project headers (=== name ===)
--statsfalsePrint match count (N/total) for count commands
--no-outputfalseSuppress command output in print-if-data commands
--print-notfalseInvert selection — print non-matching repos

Debug

FlagDefaultDescription
--git-verbosefalsePass --verbose to git commands
--git-quietfalsePass --quiet to git commands

Project discovery

FlagDefaultDescription
--glob <PATTERN>*/*Glob pattern for finding projects
--no-globfalseDisable glob, scan immediate subdirectories only
--folders <LIST>(none)Comma-separated explicit folder list
--no-sortfalsePreserve discovery order instead of sorting

Error handling

FlagDefaultDescription
--no-stopfalseContinue on errors instead of stopping
--no-print-no-projectsfalseSuppress “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

  1. Collect candidate paths using the selected mode
  2. Filter to directories that contain .git/
  3. Sort alphabetically (unless --no-sort)
  4. Pass the list to the selected subcommand’s runner

Architecture

Overview

RSMultiGit follows a simple pipeline: discover projectsrun commandcollect 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.

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:

ModuleTestsWhat’s tested
cli11Subcommand parsing, global flags, argument validation
commands::count7is_dirty, has_untracked, non_synchronized with temp git repos
discovery6Folder, glob, and no-glob discovery modes
runner11All three runner patterns with mock closures
subprocess_utils6capture_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)

FunctionDescription
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

  1. Create a new file in tests/tests_mod/
  2. Add a #[path] module entry in tests/main.rs
  3. Use setup_git_repos() to create test fixtures
  4. 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"));
}
}