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

Command Reference

Global Flags

These flags are accepted by every subcommand and may appear before or after the subcommand name.

FlagDescription
-v, --verbosePrint every repo, even when no action is taken (adds the header for skipped repos; git status switches to per-file output)
--terseMinimal, machine-readable output: repo names only for data commands, just the N/total line for count commands, failing rule names only for the check commands
--no-headerSuppress the [repo] header line printed before per-repo output
--no-outputSuppress command output, keeping only the [repo] headers. Action commands run their subprocesses with output captured and discarded; a failing repo’s output is still attached to its error
--print-notInvert selection: print the repos that do NOT match
--no-stopOn error, print error in <repo>: ... to stderr and continue with the next repo instead of stopping. The run still exits non-zero (N of M repos failed) if any repo failed
--short-circuitStop at the first failing rule. Off by default; honoured by check same and check exists
-j, --jobs <N>Run up to N repos in parallel (default 1; 0 means one worker per CPU). Output is still printed in repo order
--venvActivate each repo’s local .venv (prepend .venv/bin to PATH, set VIRTUAL_ENV) before running tool subprocesses. On by default; honoured by run, build, cargo and npm; not by uv
--no-venvTurn --venv off: run tool subprocesses with the ambient environment

Example:

rsmultigit --terse git count dirty               # Just print "3/50"
rsmultigit --no-stop git pull                # Pull all, report failures, keep going
rsmultigit -j 8 git fetch                    # Fetch eight repos at a time
rsmultigit --short-circuit check same        # Stop at the first broken rule

Output conventions

Most commands print a [repo] header followed by that repo’s output, and print nothing for repos where nothing happened (no data, nothing to do, or the repo was skipped by a precondition such as a missing Cargo.toml). --verbose prints the header for those repos too, --no-header drops the header everywhere, and --terse reduces the output to bare repo paths.

Commands that take an operation word (git count dirty, gh clean-all, cargo clippy, …) print their help, with every operation and its description, when the word is left out - rsmultigit gh is the quickest way to see what gh can do.

Git Commands

rsmultigit git <command>

Everything that inspects or operates on the repositories as git repositories. rsmultigit git alone lists the subcommands. They come in two kinds: the reports below (counts and per-repo data, computed with libgit2) and the operations after them, each of which runs the git command of the same name in every repo.

Counts

rsmultigit git count <what> tests each repo with libgit2 (no subprocess), prints the path of every matching repo, then a final matched/total line.

rsmultigit git count dirty

Repos with changes in the working tree or index: modified, deleted, renamed, type-changed, newly staged, or conflicted (mid-merge) files. Untracked files do not count.

rsmultigit git count dirty
rsmultigit --terse git count dirty               # Only the "N/total" line
rsmultigit --print-not git count dirty           # Clean repos instead

rsmultigit git count untracked

Repos that have untracked files.

rsmultigit git count unsynchronized

Repos that are not synchronized with their upstream: ahead of or behind it. The upstream is the branch’s configured tracking branch (as set by clone or push -u); when none is configured, origin/<branch> is used. A repo with no upstream at all has nothing to be out of sync with and is not counted, which matches how git status and git push treat it.

rsmultigit git count unsynchronized
rsmultigit --print-not git count unsynchronized    # Repos that ARE in sync

rsmultigit git count ahead / rsmultigit git count behind

The two halves of unsynchronized: repos with local commits not yet pushed (ahead) or upstream commits not yet pulled (behind), against the same upstream. The same tests as list ahead / list behind.

Reports

These commands compute a value per repo and print it under the [repo] header. Repos with nothing to report are skipped unless --verbose is set. With --terse, only the repo path is printed.

rsmultigit git status / rsmultigit status

One-line summary of every repo that needs attention: counts of conflicted, staged, modified, deleted and untracked files, plus ahead N / behind N when the branch has diverged from its upstream (see git count unsynchronized for how the upstream is chosen). Clean, in-sync repos are skipped. Computed with libgit2. rsmultigit status is a top-level shortcut for the same command.

rsmultigit status                        # Same as `rsmultigit git status`
# [/home/me/git/myrepo]
# 2 modified, 1 untracked, ahead 1
rsmultigit --verbose git status              # Full per-file `git status -s` instead

rsmultigit git dirty

git diff --stat for repos with unstaged modifications; falls back to git diff --cached --stat when only staged changes exist.

rsmultigit git age

The age of the last commit as a relative date (3 days ago).

rsmultigit git authors

git shortlog -sne HEAD: authors with commit counts and emails.

rsmultigit git size

Size of the .git directory per repo, in human-readable units. Symlinks are not followed. A linked worktree, whose .git is only a pointer file, has no git directory of its own and is skipped.

rsmultigit git last-tag

The most recent tag reachable from HEAD (git describe --tags --abbrev=0). Repos without tags are skipped.

Operations

Action commands print the [repo] header for repos where the action ran; with --verbose it is printed for skipped repos too.

rsmultigit git config <KEY>

A git config value per repo. Repos where the key is unset are skipped.

rsmultigit git config user.email
rsmultigit git config remote.origin.url

rsmultigit git pull [--quiet]

git pull in every repo. --quiet is forwarded to git.

rsmultigit git push

git push in every repo that is ahead of its upstream (the configured tracking branch, else origin/<branch>). Repos with nothing to push, or with neither, are skipped. A branch measured against origin/<branch> for want of a configured upstream is pushed there explicitly (git push origin HEAD), since plain git push refuses a branch without an upstream.

rsmultigit git fetch

git fetch in every repo.

rsmultigit git commit -m <MESSAGE>

git add -A then git commit -m <MESSAGE> in every repo that has changes. Clean repos are skipped.

rsmultigit git checkout <BRANCH>

git checkout <BRANCH> in every repo.

rsmultigit git stash push / rsmultigit git stash pop

push runs git stash push -m "rsmultigit stash" in every repo that has changes to tracked files; clean repos are skipped. pop restores the most recent stash carrying that message, wherever it sits in the stash list, and skips repos that have none. A stash made by hand is never popped, so push followed by pop round-trips exactly what push saved.

These three throw work away, so each one skips the repos where it would change nothing (no [repo] header unless --verbose), and each takes --dry-run, which lists per repo what would go and changes nothing. Run the dry run first: across a fleet, one command can discard a lot.

rsmultigit git reset hard|mixed [--dry-run]

git reset --<mode> HEAD in every repo that has something to reset.

WhatRunsSelects repos with--dry-run shows
hardgit reset --hard HEADany change to a tracked file, staged or notgit status --short --untracked-files=no
mixedgit reset --mixed HEADstaged changesgit diff --cached --name-status

There is no soft: git reset --soft HEAD changes nothing, and a soft reset to an older commit is not a fleet-wide operation.

rsmultigit git reset hard --dry-run   # What would be discarded, per repo
rsmultigit git reset hard             # Discard all changes to tracked files
rsmultigit git reset mixed            # Unstage changes

rsmultigit git restore [--dry-run]

git restore . in every repo with unstaged changes to tracked files: discards them, leaving staged changes and untracked files alone. --dry-run shows git diff --name-status instead. (Formerly clean git; it removes nothing untracked, so it was never a clean.)

rsmultigit git clean <what> [--dry-run]

WhatRunsNotes
hardgit clean -ffxdRemoves untracked and ignored files (.venv/, target/, .env, …)
softgit clean -fdRemoves untracked files only

A repo is selected when git’s own dry run (git clean -nxd / -nd) lists something, and --dry-run prints exactly that list.

cargo clean lives under the cargo command: rsmultigit cargo clean.

rsmultigit git diff

git diff in every repo.

rsmultigit git log [--count N]

git log --oneline -n N in every repo (default 10).

rsmultigit git log
rsmultigit git log --count 5

rsmultigit git blame <FILE>

git blame <FILE> in every repo that contains the file; others are skipped.

rsmultigit git grep [-l|--files] <REGEXP>

git grep -n -e <REGEXP> in every repo, so a pattern starting with - is searched for rather than read as an option. Each output line is prefixed with the repo name; -l prints matching filenames only. Repos with no match print nothing; --terse lists just the repos that matched and --print-not the ones that did not.

rsmultigit git grep "TODO"
rsmultigit git grep -l "TODO"

rsmultigit git branch local|remote|github

git branch, git branch -r, or the GitHub default branch via gh repo view (requires the gh CLI).

rsmultigit git tag local|remote|has-local|has-remote

local and remote list tags (git tag, git ls-remote --tags origin). has-local and has-remote behave like count commands: they print the repos that have any tags, then a matched/total line.

rsmultigit git remote

git remote -v in every repo.

rsmultigit git remote-prune

git remote prune origin in every repo: drops remote-tracking branches whose upstream branch is gone. Not called prune, since git prune is git’s unrelated object pruning.

rsmultigit git gc

git gc in every repo.

rsmultigit git submodule-update

git submodule update --init --recursive in every repo.

Repo Lists

The list commands print the absolute path of every repo they select, one per line, with no header and no summary line, so the output feeds straight into xargs or a shell loop. --print-not inverts the selection of every filter (list dirty --print-not prints the clean repos); -j inspects repos in parallel without changing the output order. A bare rsmultigit list lists the choices.

for repo in $(rsmultigit list dirty); do git -C "$repo" diff --stat; done
rsmultigit list ahead                  # the repos `git push` would send something from
rsmultigit list dirty --print-not      # the clean repos

rsmultigit list repos

Every configured repo. --verbose adds the [repo] header for each entry.

rsmultigit list dirty

Repos with modified, deleted, staged or conflicted files (untracked files do not count; see list untracked). The same test as git count dirty.

rsmultigit list untracked

Repos with untracked files. The same test as git count untracked.

rsmultigit list unsynchronized

Repos whose checked-out branch is ahead of or behind its upstream. A repo with no upstream (or a detached HEAD) has nothing to be out of sync with and is never listed. The same test as git count unsynchronized.

rsmultigit list ahead

Repos with local commits not yet pushed to the upstream.

rsmultigit list behind

Repos whose upstream has commits not yet pulled. This compares against the remote-tracking refs as they are; run rsmultigit git fetch first for a current answer.

Running Arbitrary Commands

rsmultigit run <COMMAND...> (alias: rsmultigit exec)

Run an arbitrary command in every repo. A single argument is run through the shell (sh -c), so pipes, redirects and quoting work; several arguments are executed directly without a shell.

By default (--venv), a repo that has a local .venv gets it activated first, so the command and anything it spawns resolve from the repo’s own venv. Pass --no-venv to run with the ambient environment everywhere.

rsmultigit run touch marker.txt
rsmultigit run "echo hello > greeting.txt"
rsmultigit exec cargo check
rsmultigit run pytest                  # each repo's own pytest, via its .venv
rsmultigit --no-venv run which python  # ambient python everywhere

Consistency Checks

The check subcommands evaluate the [[check-same]], [[check-exists]] and [[check-submodules]] rules in the config file; rsmultigit check alone lists them. See Configuration for the rule fields. Unlike every other command, output is organised by rule, not by repo: each rule prints a header line followed by its verdict. The header is [kind rule-name], the kind being the stanza the rule came from and the command that runs it (check-same, check-exists, check-submodules), followed by the rule’s properties as key=value pairs (path and select always; exclude, marker, marker_absent when set; enabled=false and must_have=true only when they differ from the default), so a failure can be read against its rule without opening the config. --no-header drops it.

rsmultigit check same

For each enabled [[check-same]] rule, hash path in every selected repo and group the repos by content. A rule passes when there is at most one group and, for must_have = true rules, no selected repo lacks the file.

[check-same gitignore] path=.gitignore select=*
ok (212 files)
[check-same workflow-build-yml] path=.github/workflows/build.yml select=* marker_absent=.noci must_have=true
34 files, 2 groups (1 missing, 3 skipped)
  group A (31 files):
    /home/me/git/alpha/.github/workflows/build.yml
    ...
  group B (3 files):
    /home/me/git/beta/.github/workflows/build.yml
    ...
  missing in:
    /home/me/git/gamma

Groups are ordered largest first, so group A is the presumed canonical version. missing counts must_have violations; skipped counts repos that lack the file under a rule without must_have.

Exit code is 1 when any rule fails, 0 otherwise. A rule that matches no files at all is reported as no files matched and, after all rules have run, turns into a hard error on stderr naming every such rule, since an empty rule almost always means a stale select or path.

FlagEffect
--checks <NAME>...Run only these rules, in the given order. Named rules run even if enabled = false. An unknown name is an error
--checks-re <REGEX>...Also run every rule whose name matches a regex (unanchored). A regex matching nothing is an error. Combines with --checks
--only-failedDrop the ok (N files) lines; print failing rules only
--allow-emptyLet a rule that matches no files pass as ok (0 files) instead of failing
--diffAfter reporting a failing rule, print a unified diff between representatives of the differing groups. With exactly two groups this is automatic; with more, it prompts for the pair and offers to diff another
--copyAfter reporting a failing rule, prompt for a “from” group and a “to” group, confirm, then overwrite every file in the “to” group with the “from” representative (preserving each destination’s file mode). Exit code is 0 even when rules still differ; a file that could not be written makes it non-zero
--fix-missingFor rules with must_have violations, prompt for a group to seed from, confirm, then create the file in each violating repo (creating parent directories as needed). Exit code is 0 even when rules still differ; a file that could not be created makes it non-zero

The interactive prompts accept a group letter (A, B, …), s to skip the rule, or q to quit the whole run; confirmations default to no. EOF on stdin is treated as quit, so the command never hangs when stdin is closed.

--terse prints only the names of failing rules, one per line, and implies --only-failed. --short-circuit stops after the first failing rule.

rsmultigit check same                         # Report every rule
rsmultigit --terse --no-stop check same       # Names of broken rules only, for scripts
rsmultigit check same --checks gitignore editorconfig
rsmultigit check same --checks-re '^rs-'      # Every rule whose name starts with rs-
rsmultigit check same --diff --only-failed    # Show what differs
rsmultigit check same --copy                  # Fix drift interactively
rsmultigit check same --fix-missing           # Create files missing from must_have repos

rsmultigit check exists

For each enabled [[check-exists]] rule, assert that every selected repo contains path. Content is never compared, which makes this the right rule type for files that must exist but legitimately differ per repo (a README, say).

[check-exists readme-present] path=README.md select=*
ok (212 repos)
[check-exists license-present] path=LICENSE select=*
212 repos, 2 missing LICENSE
  missing in:
    /home/me/git/alpha
    /home/me/git/beta

Exit code is 1 when any rule fails. A rule that selects no repos at all fails with no repos selected and becomes a hard error at the end, unless --allow-empty is passed.

--checks, --checks-re, --only-failed, --allow-empty, --terse and --short-circuit behave exactly as for check same. There are no interactive modes.

rsmultigit check submodules

For each enabled [[check-submodules]] rule, find the submodule the rule’s url names in every selected repo and assert that all of them have it checked out at the same commit. A submodule is the fleet’s way of sharing a repo, and the pin is the part that drifts when one consumer bumps it and the others do not. Only submodules with a rule are checked: one without a rule is free to differ per repo, by decision rather than by accident.

The URL is matched after normalisation on both sides, so the rule may say github.com/owner/repo while the repos added it over ssh or https, with or without .git. The commit compared is the one checked out in the submodule’s working directory, the same view check same takes of ordinary files: what is on disk and what a build uses. A submodule that is not checked out falls back to the commit recorded in the superproject’s index, then in HEAD. A selected repo that does not carry the submodule is skipped, not failed: the rule constrains the pin, not which repos use the submodule.

[check-submodules shared-themes] url=github.com/veltzer/shared-themes select=*
ok (4 repos @ aa5e688, 208 skipped)
[check-submodules shared-tags] url=github.com/veltzer/shared-tags select=teaching-*
3 repos, 2 commits
  1f2e3d4 (2 repos):
    /home/me/git/teaching-slides (shared/shared-tags)
    /home/me/git/teaching-syllabi (shared/shared-tags)
  9a8b7c6 (1 repo):
    /home/me/git/teaching-animations (shared/shared-tags)

Groups are ordered largest first. Exit code is 1 when any rule’s submodule is pinned to more than one commit, or to none. A rule whose submodule no selected repo carries fails with no repos have this submodule and becomes a hard error at the end, unless --allow-empty is passed, since finding it nowhere almost always means a stale select or a misspelled url.

--checks, --checks-re, --only-failed, --allow-empty, --terse and --short-circuit behave exactly as for check same. There are no interactive modes.

rsmultigit check all [--only-failed] [--allow-empty]

Run every enabled [[check-same]] rule, then every enabled [[check-exists]] rule, then every enabled [[check-submodules]] rule. All three always run, so a failure in one never hides the state of the others. Exit code is non-zero if any of them fails.

rsmultigit check all --only-failed

rsmultigit check list [same|exists|submodules]

Print the name of every [[check-same]] rule (the default), every [[check-exists]] rule or every [[check-submodules]] rule, one per line, including disabled ones. The kind is named after the subcommand that runs it. The bash and zsh completion scripts call this to complete check same --checks <TAB>, check exists --checks <TAB> and check submodules --checks <TAB>.

Build Commands

rsmultigit build [<method>]

Run a build tool in each project directory. Projects with a .disable file in their root are always skipped.

By default (the global --venv flag), a project that has a local .venv gets it activated before the build tool runs, so the tools the build spawns (pytest, mypy, ruff, …) resolve from the repo’s own venv, including the build tool itself when the venv provides it. Projects without a .venv build with the ambient environment. Pass --no-venv to disable the activation everywhere.

MethodWhat runsWhich projects
bootstrappython bootstrap.pyall
makemakeall
rsconstructrsconstruct --quiet buildthose with rsconstruct.toml
cargocargo build --profile dev then cargo build --releasethose with Cargo.toml

Publishing is not a build method: see rsmultigit cargo publish below.

Default build method

The method is optional when the config file sets default_build_method:

# ~/.config/rsmultigit/config.toml
default_build_method = "rsconstruct"

With that in place a bare rsmultigit build means rsmultigit build rsconstruct. The key accepts exactly the spellings the command line does (rsconstruct, not Rsconstruct), and a method given on the command line always wins over the config file. Without the key, a bare rsmultigit build is an error that says how to fix it.

rsmultigit build rsconstruct
rsmultigit build                       # same, given default_build_method = "rsconstruct"
rsmultigit build make                  # explicit method overrides the default
rsmultigit --no-venv build rsconstruct

Cargo Commands

These commands operate only on repositories that have a Cargo.toml file and no .disable file. Other repositories are skipped.

rsmultigit cargo <what> [--release | --profile <name>] [--check]

Runs one cargo operation in each Rust project. All of them honour --venv.

whatRunsNotes
buildcargo build --profile dev then cargo build --releaseEvery profile, like cargo_build.sh; --release or --profile builds just that one
checkcargo checkType-check only, no artifacts
clippycargo clippy --all-targets -- -D warningsSame invocation as the fleet’s ci.yml: warnings fail the run
fmtcargo fmt --all--check only verifies and fails on unformatted code
testcargo testcargo’s built-in runner
nextestcargo nextest runRequires cargo-nextest
doccargo doc --no-depsAPI docs for the crate itself
denycargo deny checkRequires cargo-deny
fetchcargo fetchDownload dependencies without building
updatecargo updateRefreshes Cargo.lock
cleancargo cleanRemoves the target directory
publishcargo publishUploads the crate to crates.io; release is the full release flow
releasecargo release <type> --execute --no-confirmThe full release flow, see below; --type picks the bump

--release appends --release and --profile <name> appends --profile <name> (dev, release, or any custom profile the crate’s Cargo.toml defines). Both are accepted by the operations that compile under a profile: build, check, clippy, test, nextest and doc. With neither, build compiles every profile the fleet ships, dev then release, and the other operations keep cargo’s own default. --check is accepted by fmt only, and --type by release only. A flag on any other operation is an error, and --release, --profile and --check are mutually exclusive.

rsmultigit cargo build                   # Every Rust repo, dev and release
rsmultigit cargo build --profile dev     # Only the default (dev) profile
rsmultigit cargo build --release         # Only release
rsmultigit cargo fmt --check             # Is every Rust repo formatted?
rsmultigit cargo clippy                  # Lint the fleet the way CI does
rsmultigit -j 0 cargo nextest --release  # Release-mode tests, all cores
rsmultigit cargo update                  # Refresh every Cargo.lock

rsmultigit cargo release [--type <patch|minor|major>]

Release a new version of each Rust project by running cargo release <type> --execute --no-confirm, which bumps the version in Cargo.toml, commits, tags, pushes, and publishes to crates.io (the rest of the policy is the repo’s release.toml). The default release type is patch.

A repo is released only when its root Cargo.toml declares a [package] that cargo would publish. This is cargo’s own vocabulary, so no rsmultigit-specific marker is needed:

  • A virtual workspace (a root [workspace] listing members, with no [package]) has nothing to version or publish and is skipped. This is what a repo of examples and exercises looks like.
  • A package with publish = false (or publish = []) is a scratch crate that was never meant for crates.io; it is skipped here just as cargo publish itself would refuse it. publish.workspace = true defers to [workspace.package].publish, as in cargo.
  • A repo with no Cargo.toml is not a rust project and is skipped.

Skipped repos are listed under --verbose, like every other skip.

Before the first repo is touched, two things are checked once for the whole run:

  • cargo-release must be on PATH. It is a separate crate, not part of the toolchain, so a rebuilt CARGO_HOME loses it; the error names the fix (cargo install cargo-release) instead of cargo’s own “no such command: release”.
  • The crates.io token is fetched. If CARGO_REGISTRY_TOKEN is already set in the environment it is used as is. Otherwise the first line of pass show <entry> is taken, where <entry> is the config file’s crates_io_pass_entry (default keys/crates.io). The token is handed to every cargo release process through its environment only, so it never lands in ~/.cargo/credentials or any other file.
rsmultigit cargo release                 # Patch release (default)
rsmultigit cargo release --type minor    # Minor release
rsmultigit cargo release --type major    # Major release

uv Commands

Run uv operations across every repository that has a pyproject.toml at its root; other repositories are skipped.

rsmultigit uv lock [--upgrade|--check]

Run uv lock in each Python project, re-resolving uv.lock from pyproject.toml. Without --upgrade, versions that are already locked are kept and only added or removed dependencies change; with --upgrade, locked versions may move forward to the newest allowed releases.

With --check, nothing is written: uv lock --check only asserts that the lockfile is up to date with pyproject.toml, and a stale lockfile is an error (combine with the global --no-stop to survey all projects instead of stopping at the first stale one). --check and --upgrade are mutually exclusive.

rsmultigit uv lock                    # Bring every lockfile in sync with pyproject
rsmultigit uv lock --upgrade          # Deliberately upgrade all locked versions
rsmultigit uv lock --check --no-stop  # Report which lockfiles are stale

rsmultigit uv sync

Run uv sync in each Python project, syncing its environment from the lockfile. Projects without a .venv get one created.

uv selects its own target environment from the project directory, so rsmultigit runs it with VIRTUAL_ENV (and UV_PROJECT_ENVIRONMENT) unset and leaves the choice to uv. This matters when you invoke rsmultigit from an activated shell: the inherited VIRTUAL_ENV names your venv, never the repo being synced, and passing it through makes uv sync warn (does not match the project environment path, after which the value is ignored) while the uv pip interface would silently target the wrong environment. The global --venv/--no-venv flag therefore does not apply to uv; the other uv operations behave the same way.

rsmultigit uv build

Run uv build in each Python project, producing the sdist and wheel under dist/.

rsmultigit uv publish

Run uv publish in each Python project, uploading whatever uv build left in dist/ to the package index. Credentials and the target index come from uv’s own configuration (UV_PUBLISH_TOKEN, trusted publishing, keyring, [[tool.uv.index]]); rsmultigit passes nothing through.

rsmultigit uv build                   # Build every distribution
rsmultigit uv publish                 # ... then upload them

npm Commands

Run npm operations across every repository that has a package.json at its root; other repositories are skipped. All of them honour --venv, so package.json scripts that shell out to tooling living in the repo’s .venv find it on PATH.

rsmultigit npm <what> [--fix]

whatRunsNotes
installnpm installInstalls from package.json, writing package-lock.json
cinpm ciClean install of exactly what package-lock.json says; fails if it disagrees with package.json
updatenpm updateMoves dependencies to the newest versions their declared ranges allow and rewrites package-lock.json
auditnpm auditReports known vulnerabilities; exits non-zero when any are found. --fix runs npm audit fix, which rewrites package.json and package-lock.json
outdatednpm outdatedLists dependencies with newer releases; exits non-zero when any are outdated
testnpm testRuns the project’s test script
publishnpm publishPublishes the package to the registry

--fix is accepted by audit only; on any other operation it is an error. audit and outdated report through their exit status, so combine them with the global --no-stop to survey the whole fleet instead of stopping at the first repo with findings.

rsmultigit npm update                    # Refresh every package-lock.json within ranges
rsmultigit npm ci                        # Reproduce every lockfile exactly
rsmultigit --no-stop npm outdated        # Which repos have newer releases available?
rsmultigit npm audit --fix               # Apply the automatic vulnerability fixes

GitHub Commands

These commands talk to GitHub through the gh CLI (which must be installed and authenticated) and operate only on repositories that have a remote whose URL contains github.com. Other repositories are skipped.

rsmultigit gh clean-all [--keep <N>]

Clean up GitHub deployments, releases, and workflow runs for each repository. Keeps only the --keep (default 4) most recent non-failed of each and deletes the rest; failed deployments (latest status failure/error) and failed workflow runs (failure, cancelled, timed_out, startup_failure, action_required) are always deleted, even if recent.

Note that this deletes data on GitHub permanently: releases, deployment history, and workflow run logs are gone once removed.

rsmultigit gh clean-all              # Keep the 4 most recent of each
rsmultigit gh clean-all --keep 10    # Keep the 10 most recent of each

rsmultigit gh artifacts

List the assets attached to each repository’s latest GitHub release as a NAME / SIZE / DOWNLOADS table. A data command: repositories without a release, or whose latest release has no assets, print nothing.

rsmultigit gh artifacts              # Who is downloading which binaries?

rsmultigit gh last-workflow-state

Print the conclusion of each repository’s most recent workflow run (success, failure, cancelled, …). A run that has not finished yet shows its status instead (in_progress, queued). A data command: repositories with no workflow runs print nothing.

rsmultigit gh last-workflow-state                    # One line per repo
rsmultigit --terse gh last-workflow-state            # Just the repo names

rsmultigit gh open-site

Open each repository’s GitHub Pages site in the browser with xdg-open, printing the URL first. A repository without a Pages site says no GitHub Pages site and is otherwise left alone. Every repository with a site gets a browser tab, so point the config at the repos you mean before running it across a large fleet.

rsmultigit gh open-site

rsmultigit gh sync-metadata [--dry-run]

Sync each repository’s GitHub metadata from its config/project.lua, printing only what actually differs. Repositories without that file are skipped. The file is the source of truth for two fields:

project.luaGitHub field
DESCRIPTION_SHORTdescription
KEYWORDS (a lua list)topics (order-independent)

A field that is absent or empty locally is left alone on GitHub: “unset locally” is not the same as “clear it”. The repository features follow a fixed fleet policy that is not read from the file and is always enforced: wiki off, issues on, projects off.

The repository is addressed by its github.com remote, not by the file’s NAME; a NAME that disagrees with the GitHub repository name is reported but never acted on. Only the topics that differ are sent (--add-topic / --remove-topic), so a topic present on both sides is never touched.

Only the plain assignment subset of lua that project.lua uses is understood (X = "...", X = { "...", ... }, X = Y aliases, -- comments); anything else is an error naming the line.

rsmultigit gh sync-metadata --dry-run   # Show the differences, change nothing
rsmultigit gh sync-metadata             # Show them and update GitHub

Sample output for one repository with every field out of step:

[demos-os-linux]
description
  local:  demos-os-linux is a project to demo and explore the Linux API
  github: Linux demos
topics
  local:  api c demos linux
  github: c demos linux old-topic
features
  want:   wiki=false issues=true projects=false
  github: wiki=true issues=true projects=false

--dry-run is accepted by sync-metadata only, and --keep by clean-all only; on any other gh operation either is an error rather than a silently ignored flag.

Utility Commands

These need no config file: setup is how a fresh install bootstraps one.

rsmultigit setup <command>

The two ways to get a first config file. rsmultigit setup interactive alone lists them.

rsmultigit setup interactive

Write a first ~/.config/rsmultigit/config.toml by answering two questions in the terminal:

  1. Which directory holds your git repositories. A text prompt with tab completion over directories, defaulting to ~/git when it exists. The answer is checked before it is accepted: it must be a directory with at least one git repository directly under it, the same test the repos glob applies later.
  2. Which build tool a bare rsmultigit build runs. A menu of the supported methods (bootstrap, make, rsconstruct, cargo) plus none, each row showing how many of the repos found carry that tool’s file (bootstrap.py, Makefile, rsconstruct.toml, Cargo.toml). The cursor starts on the most common one.

The config is written with repos = ["<dir>/*"] (the home directory contracted to ~) and, unless none was picked, default_build_method. An existing file is replaced only after a confirmation.

Each question is skipped when its answer is given as an option, and with every answer given nothing is asked at all, so the command also works in scripts and without a terminal:

rsmultigit setup interactive                                  # fully interactive
rsmultigit setup interactive --repos-dir ~/src                # asks only the build tool
rsmultigit setup interactive --repos-dir ~/src --build cargo  # asks nothing
rsmultigit setup interactive --repos-dir ~/src --no-build     # no default_build_method
rsmultigit setup interactive --repos-dir ~/src --build make --overwrite  # replace without asking
OptionMeaning
--repos-dir <DIR>Directory whose direct subdirectories are the repositories
--build <METHOD>Set default_build_method; excludes --no-build
--no-buildWrite no default_build_method
--overwriteReplace an existing config file without asking

Without a terminal, a question that still needs asking is an error naming the option that answers it; nothing is written in that case.

rsmultigit setup config-sample

Print a fully commented sample config to stdout.

mkdir -p ~/.config/rsmultigit
rsmultigit setup config-sample > ~/.config/rsmultigit/config.toml

rsmultigit complete <bash|zsh|fish|elvish|powershell>

Print a shell completion script. The bash and zsh scripts additionally complete check same --checks <TAB> and check exists --checks <TAB> with the rule names from your config, by calling rsmultigit check list at completion time.

rsmultigit complete bash >> ~/.bash_completion
rsmultigit complete zsh > ~/.zfunc/_rsmultigit

rsmultigit version

Print detailed version information: crate version, git describe, commit, branch, dirty state, rustc version, edition, and build timestamp.

rsmultigit version
rsmultigit --version                 # One-line "rsmultigit x.y.z by Author"