CLI tool
lindell/multi-gitter avatar
lindell/multi-gitter

multi-gitter: one script, many repositories, one pull request each

Update multiple repositories in with one command

1,231 stars93 forksGoApache-2.0

At a glance

What is it?
multi-gitter runs a script inside many cloned repositories and opens a pull request in each one that changed. It is a good fit for mechanical edits across an organisation, and a poor fit for anything that needs per-repository judgement.
Who is it for?
Adopt multi-gitter when the change is identical in every repository and can be expressed as a script that exits cleanly, for example a template file sync or a lint autofix. Do not adopt it when the edit needs per-repository decisions, when a change touches large binary files, or when your platform is neither GitHub, GitLab, Gitea, Bitbucket nor Gerrit.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 10 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 15, 2026, and from our analysis. They are not legal advice.

DEEP OPEN-SOURCE ANALYSIS

The problem multi-gitter solves: the same edit in fifty repositories

A change that is trivial in one repository becomes a coordination problem in an organisation. Adding a PR template, bumping a dependency, fixing a linter rule across every service: each repository needs a clone, a branch, a commit and a pull request, and each pull request needs a reviewer. The work is not hard, it is repetitive, and repetition is where mistakes accumulate. multi-gitter exists to collapse that loop into a single command. You write a script that makes the change in one repository, and the tool runs that script in the context of many repositories, committing and opening pull requests wherever the script actually modified something. The README states the intent directly: if you can script it to run in one place, you can run it in all your repositories with one command. The audience is whoever already automates in shell, Python, Node or Go and wants that automation to fan out. It is not aimed at people who want a GUI, and it is not a code review tool. It sits between your script and the hosting platform's API, and it is opinionated about one thing only: that a change worth making in one repository is worth proposing in all of them.

How a run actually works: clone, execute, diff, pull request

The mechanism is deliberately plain. multi-gitter selects a set of repositories from the flags you give it (an organisation with -O, a user with -U, individual repositories with -R, or a code search on GitHub with code-search), clones each one into a temporary directory, and runs your script with the repository as the working directory. The clone location defaults to the operating system temporary directory and can be moved with the clone-dir config key. If the script leaves the working tree unchanged, nothing happens for that repository. If it changed files, multi-gitter creates the branch named by the branch key (the default in the documented config is multi-gitter-branch), commits with the message from -m or the commit-message key, and opens a pull request. Concurrency is capped by the concurrent key, which the config example shows defaulting to 1, so a large organisation is processed one repository at a time unless you raise it. Two details follow from this design. First, the script runs inside a fresh clone, so it sees that repository's files and nothing else; any state you need must be passed in as arguments or environment. Second, the tool only knows whether files changed, not whether the change is correct. It has no model of your intent. That is the whole contract, and it explains both the flexibility and the failure modes.

Getting it running: install, token, and the run command

Installation is available through Homebrew with brew install --cask lindell/multi-gitter/multi-gitter, through a release binary, through an install script (curl -s https://raw.githubusercontent.com/lindell/multi-gitter/master/install.sh | sh), or from source with go install github.com/lindell/multi-gitter@latest, which the README says is not recommended for most cases. You then need a token that can list repositories and create pull requests. It can come from the GITHUB_TOKEN, GITLAB_TOKEN or GITEA_TOKEN environment variable, or from the --token flag. On GitHub the README asks for repo permissions on a classic personal access token; on GitLab it asks for the api permission; on Gitea tokens are generated under Settings, then Applications, then Manage Access Tokens. The basic invocation is multi-gitter run ./my-script.sh -O my-org -m "Commit message" -B branch-name, and the README notes the script needs execution permissions first, via chmod +x. For interpreted languages the path must be absolute, because the script runs in each repository's directory rather than yours, so the documented form is multi-gitter run "python $PWD/run.py" -O my-org -m "Commit message" -B branch-name, with equivalent examples for node and go run. Before committing anything, multi-gitter run ./script.sh --dry-run --log-level=debug -O my-org -m "Commit message" -B branch-name shows what would change without making changes. That combination is the single most useful thing in the tool's surface area, and skipping it is how people end up opening dozens of identical bad pull requests.

Configuration precedence and the flags that decide scope

Everything configurable by flag is also configurable by file. Passing --config=./path/to/config.yaml loads a file, and the flag can be repeated, with later files overriding earlier ones. Separately, multi-gitter reads ~/.multi-gitter/config automatically. The documented priority is flags first, then an explicitly defined config file, then the static config file. That ordering matters in practice: a value sitting in ~/.multi-gitter/config will be silently overridden by a project config file, and both will be overridden by a flag, so a run that behaves unexpectedly is usually a precedence question rather than a bug. The run options list is long and mostly concerns scope and pull request metadata: assignees, author-name and author-email for the commit identity (falling back to global git config when unset), base-branch for what the change is based on, and code-search for GitHub, which the config comments describe as ignoring repeated results from a given repository and excluding forks unless fork:true is specified. There is also api-push, which pushes changes through the API instead of git. The comments are explicit about the trade-off: it is GitHub only, it produces verified commits automatically, and it is slower and not suited for changes to large files. If your change touches anything sizeable, the git path is the one to use.

Where it breaks: mechanical edits, wrong tool for judgement calls

multi-gitter commits whatever your script leaves behind. It cannot tell a correct refactor from a subtly broken one, and it has no notion of a repository that should be excluded because its structure differs. The README's own framing gives this away: the tool is for changes you can script to run in one place, and the examples (syncing a file, updating a dependency, fixing linting issues, search and replace) are all cases where the same transformation applies everywhere. Point it at a migration that needs a human decision per repository and you will get a stack of pull requests that each need individual correction, which is more work than doing them by hand. The branch collision behaviour is a second sharp edge. The config documents a setting for what happens when the branch already exists, with skip and replace among the available values, which means the default outcome of a re-run is a decision you should make deliberately rather than discover. Third, the concurrency default of 1 in the documented config means a first run against a large organisation will be slow unless you raise it, and raising it multiplies the load you place on the hosting API and on whatever your script calls. Finally, api-push is GitHub only and the comments warn it is slower and unsuitable for large files, so a workflow built around verified commits does not transfer to GitLab, Gitea or Bitbucket. None of these are defects in the design. They are the boundaries of a tool that automates one specific loop.

Alternatives: multi-gitter versus a custom loop over the platform API

The obvious alternative is a script of your own: list repositories with the GitHub or GitLab API, clone each, run your transformation, push a branch, open a pull request through the API. That approach gives you complete control over filtering, error handling and retry behaviour, and it is the right answer when your selection logic is unusual or when you need to coordinate with other systems mid-run. The difference in approach is where the complexity lives. A custom loop puts repository selection, cloning, change detection, committing and pull request creation in your code, and you maintain all of it as the platform APIs move. multi-gitter puts those five steps behind one command and exposes them as flags and config keys, which is less code to own but also less room to intervene. If your transformation is genuinely mechanical and your target is a whole organisation, the built-in path is the shorter one. If your transformation needs a decision per repository, or if you want to record why each repository was included or skipped, writing the loop yourself will cost less than bending multi-gitter around the exception. There is no middle ground in the tool's surface as documented: the selection inputs are organisation, user, explicit repository list and GitHub code search.

Maintenance cost, licence and what to check before a first run

multi-gitter is written in Go, is not archived, and its release history shows a steady cadence: v0.62.0, v0.63.0 and v0.63.1 between February and May 2026, with the last push to the default branch in September 2026. The version numbers are still in the 0.x range, so minor releases can carry behaviour changes, and pinning a version in CI is more defensible than tracking latest. Installation from source is explicitly not recommended for most cases, which suggests the maintainers expect binary distribution to be the normal path; that also means upgrades are a binary swap rather than a dependency resolution problem. The licence is Apache-2.0, which permits commercial and private use and includes an explicit patent grant, with the usual obligations around retaining notices and stating changes. That is a summary of the identifier, not legal advice; if you redistribute a modified binary, read the licence text. Before the first live run, confirm three things: the token scope (repo on GitHub, api on GitLab), that your script behaves when invoked through an absolute path from an unfamiliar working directory, and that --dry-run --log-level=debug shows the file changes you expect. The dry run is the cheapest verification the tool offers, and it is the step that separates a useful fan-out from fifty pull requests you have to close by hand.

Editorial conclusion

Adopt multi-gitter when the change is identical in every repository and can be expressed as a script that exits cleanly, for example a template file sync or a lint autofix. Do not adopt it when the edit needs per-repository decisions, when a change touches large binary files, or when your platform is neither GitHub, GitLab, Gitea, Bitbucket nor Gerrit. Verify first that your token has the repo scope on GitHub or the api scope on GitLab, that your script works when invoked with an absolute path, and that a run with --dry-run --log-level=debug produces the file changes you expect before you let it commit.

Official sources

  1. Issues
  2. License: Apache-2.0
  3. lindell/multi-gitter on GitHub
  4. README
  5. Releases
Community notes

Community notes