A modern Go CLI for managing multi-repository projects. Execute commands across multiple git repositories simultaneously.
Reimplementation of gogo-meta — originally written in TypeScript and now rewritten in Go with near-identical CLI behavior (see Differences from the TypeScript version).
Upgrading from the TypeScript version (v1.x / v2.x)? See the upgrade guide for step-by-step migration instructions and the behavior changes in v3.
- Clone entire project ecosystems with one command
- Converge the working copy on the configuration with one command (
gogo update) - Execute arbitrary commands across all repositories
- Parallel or sequential execution modes
- Flexible filtering (include/exclude by name or pattern)
- NPM operations across all projects
- Symlink projects for local development
- JSON and YAML configuration formats
- Multiple config files with
-fflag (like Docker Compose)
The Go rewrite is behavior-compatible with the TypeScript original for all valid inputs. A few intentional divergences harden security and robustness:
- Built-in commands run without a shell. Internal
git/npm/ssh-keyscaninvocations execute as argument vectors, not through/bin/sh -c. The TypeScript version interpolated values (including project URLs read from.gogo) into a shell string, which allowed command injection / remote code execution when cloning an untrusted meta repository.gogo execandgogo runstill run shell commands — that is their purpose. - Project paths are validated. Project keys in
.gogo(and thefolderargument ofgogo project create/gogo project import) must be relative and stay within the repository; absolute paths and..traversal are rejected. The TypeScript version performed no such check, so a malicious.gogocould create or move directories outside the repository. gogo inithas no-fshorthand. Use--force. (The global-f/--fileoverlay flag is unchanged.) The TypeScript CLI accepted-ffor both, which the Go CLI framework cannot express without a flag collision.
Get the most recent release from GitHub.
docker pull ghcr.io/dafish/gogo-metaWhen using Docker, mount your working directory and SSH keys so gogo can access your repositories:
docker run -it --rm \
-v "$PWD":/workspace \
-v "$HOME/.ssh":/root/.ssh:ro \
-w /workspace \
ghcr.io/dafish/gogo-meta <command>Any gogo command shown in this README can be run via Docker by replacing gogo with the docker run call above. For convenience, you can create a shell alias:
alias gogo='docker run -it --rm -v "$PWD":/workspace -v "$HOME/.ssh":/root/.ssh:ro -w /workspace ghcr.io/dafish/gogo-meta'git clone https://github.com/daFish/gogo-meta.git
cd gogo-meta
make buildThe binary is built to dist/gogo. Add it to your $PATH or move it to a directory in your $PATH:
mv dist/gogo /usr/local/bin/# Initialize a new meta repository
gogo init
# Import existing repositories
gogo project import api git@github.com:org/api.git
gogo project import web git@github.com:org/web.git
# Clone a meta repository (includes all children)
gogo git clone git@github.com:org/meta-repo.git
# Bring every project into the state .gogo describes:
# move renamed ones, clone missing ones, fast-forward the rest
gogo update
# Run commands across all projects
gogo exec "npm install"
gogo exec "git status" --parallelThe config file defines child repositories, ignore patterns, and predefined commands. Both JSON and YAML formats are supported.
gogo looks for config files in the following order of precedence: .gogo (JSON) > .gogo.yaml > .gogo.yml. The first file found is used.
{
"projects": {
"api": "git@github.com:org/api.git",
"web": "git@github.com:org/web.git",
"libs/shared": "git@github.com:org/shared.git"
},
"ignore": [".git", "node_modules", ".vagrant", ".vscode"],
"groups": {
"frontend": ["web"],
"backend": ["api", "libs/shared"]
},
"commands": {
"build": "npm run build",
"test": {
"cmd": "npm test",
"parallel": true,
"description": "Run tests in all projects"
},
"deploy": {
"cmd": "npm run deploy",
"parallel": true,
"concurrency": 2,
"includeOnly": ["api", "web"]
},
"migrate": {
"cmd": "npm run migrate",
"groups": ["backend"]
}
}
}# Main services
projects:
api: git@github.com:org/api.git
web: git@github.com:org/web.git
libs/shared: git@github.com:org/shared.git
ignore:
- .git
- node_modules
- .vagrant
- .vscode
# Named sets of projects, targetable with --group
groups:
frontend:
- web
backend:
- api
- libs/shared
# Predefined commands
commands:
build: npm run build
test:
cmd: npm test
parallel: true
description: Run tests in all projects
deploy:
cmd: npm run deploy
parallel: true
concurrency: 2
includeOnly:
- api
- web
migrate:
cmd: npm run migrate
groups:
- backendA group is a named set of projects. Define groups in the config file and target
them with --group instead of spelling out directories every time:
# Run in every project of group "backend"
gogo exec --group backend "npm test"
# Several groups at once — the union of their projects
gogo exec --group backend,frontend "npm test"
# Works for every command that takes filters
gogo git status --group backend
gogo npm install --group frontend --parallelRules:
- Group members are project paths exactly as they appear under
projects. - Multiple groups are combined as a union; duplicates are collapsed.
--groupcombines with the other filters via AND, so--group backend --exclude-only libs/sharedruns in the backend projects exceptlibs/shared.- Predefined commands can name their groups via
groups(seemigrateabove). A--groupflag on the command line replaces the groups from the config. - An unknown group name, or a group listing a project that is not configured,
is reported when the group is used, so a group referring to a project from an
overlay that is not loaded does not get in the way of unrelated commands.
gogo validatechecks every group up front and resolves the members against every config file it finds — in the current directory and next to the primary config — whether or not they were loaded.
For large projects you can split configuration across multiple files and merge them at runtime using the -f, --file flag:
# Load primary .gogo plus additional projects from .gogo.devops
gogo -f .gogo.devops exec "npm test"
# Multiple overlays are applied in order
gogo -f .gogo.devops -f .gogo.extra git statusOverlay files follow the same format as the primary config (JSON or YAML). When merging:
- Projects: overlay entries are added; on key conflict the overlay wins
- Ignore: arrays are concatenated and deduplicated
- Groups: overlay entries are added; on name conflict the overlay wins (the overlay's project list replaces the base one)
- Commands: overlay entries are added; on key conflict the overlay wins
A group in an overlay may list projects defined in the base config, and vice
versa. At runtime group members are checked against the merged configuration;
gogo validate checks them against every config file it finds, so a split that
only resolves with -f still validates without it.
Overlay paths are resolved relative to the directory containing the primary config file.
Write commands (project create, project import) only modify the primary config file — overlay projects are never absorbed into it.
A .gogo.local file sitting next to the primary config is auto-merged on every normal command (no -f needed) — think of it like docker-compose.override.yml. Its filename follows the primary's format: .gogo.local beside a JSON .gogo, .gogo.local.yaml beside a .gogo.yaml. Merge order is primary → .gogo.local → -f overlays.
Use it for personal additions: extra repos only you work with, or a shared template you'd rather not pass with -f every time. It is not merged by write commands (project create, project import, git clone), so your personal projects never leak into the shared .gogo.
The ignore destination mirrors the config layer, so personal choices never pollute the shared repo:
| What | Ignored via |
|---|---|
The .gogo.local file itself |
shared .gitignore (like .env; each dev keeps their own uncommitted) — added automatically by gogo init, project import, and migrate |
Project dirs from the shared config (.gogo) |
shared, tracked .gitignore |
Project dirs from .gogo.local |
.git/info/exclude — git's per-repo, uncommitted ignore; never shared |
gogo git update auto-adds local-only project directories to .git/info/exclude, so a personal repo cloned into the umbrella stays out of git status without touching the shared .gitignore.
If a
.gogo.local.*file exists in a format that doesn't match the primary (e.g..gogo.local.yamlbeside a JSON.gogo), gogo prints a warning that it will not be merged.
Note: the merged config is the effective config — a
.gogo.localwith invalid JSON/YAML makes every gogo command fail until you fix or remove the file. gogo fails loudly here on purpose: silently ignoring a broken overlay would run commands against a different project set than you think you have.
Because gogo finds .gogo.local on its own, it gets the same ownership check as the primary config: a .gogo.local owned by another user is refused rather than merged. See SECURITY.md.
These options are available for most commands:
| Option | Description |
|---|---|
-f, --file <path> |
Additional config file to merge (repeatable) |
--group <names> |
Only target projects of the named group(s) (comma-separated) |
--include-only <dirs> |
Only target specified directories (comma-separated) |
--exclude-only <dirs> |
Exclude specified directories (comma-separated) |
--include-pattern <regex> |
Include directories matching regex pattern |
--exclude-pattern <regex> |
Exclude directories matching regex pattern |
--parallel |
Execute commands concurrently |
--concurrency <n> |
Maximum parallel processes (default: 4) |
Initialize a new gogo-meta repository by creating a config file.
gogo init # Create .gogo (JSON, default)
gogo init --format yaml # Create .gogo.yaml (YAML)
gogo init --force # Overwrite existing config file| Option | Description |
|---|---|
-f, --force |
Overwrite existing config file |
--format <format> |
Config file format: json (default) or yaml |
Execute an arbitrary command in all project directories.
# Run in each project sequentially
gogo exec "npm test"
# Run in parallel
gogo exec "npm install" --parallel
# Run with limited concurrency
gogo exec "npm run build" --parallel --concurrency 2
# Filter to specific projects
gogo exec "git status" --include-only api,web
# Target a group defined in the config
gogo exec "echo hello" --group backend
# Exclude projects
gogo exec "npm install" --exclude-only docs
# Use regex patterns
gogo exec "npm test" --include-pattern "^libs/"| Option | Description |
|---|---|
--group <names> |
Only target projects of the named group(s) |
--include-only <dirs> |
Only run in specified directories |
--exclude-only <dirs> |
Skip specified directories |
--include-pattern <regex> |
Include directories matching pattern |
--exclude-pattern <regex> |
Exclude directories matching pattern |
--parallel |
Run commands concurrently |
--concurrency <n> |
Max parallel processes |
Run a predefined command from the .gogo file.
# List available commands
gogo run
gogo run --list
# Run a predefined command
gogo run build
# Override config options with CLI flags
gogo run test --parallel
gogo run deploy --include-only api
# Commands can define defaults in .gogo:
# - parallel: true/false
# - concurrency: number
# - groups: array of group names
# - includeOnly/excludeOnly: array of directories
# - includePattern/excludePattern: regex patterns
# CLI flags override these config defaults| Option | Description |
|---|---|
-l, --list |
List all available commands |
--group <names> |
Only target projects of the named group(s) (overrides config) |
--include-only <dirs> |
Only run in specified directories (overrides config) |
--exclude-only <dirs> |
Skip specified directories (overrides config) |
--include-pattern <regex> |
Include directories matching pattern (overrides config) |
--exclude-pattern <regex> |
Exclude directories matching pattern (overrides config) |
--parallel |
Run commands concurrently (overrides config) |
--concurrency <n> |
Max parallel processes (overrides config) |
Clone a meta repository and all its child repositories.
# Clone to directory matching repo name
gogo git clone git@github.com:org/meta-repo.git
# Clone to custom directory
gogo git clone git@github.com:org/meta-repo.git -d my-project| Option | Description |
|---|---|
-d, --directory <dir> |
Target directory name |
Clone any child repositories defined in .gogo that don't exist locally.
gogo git update
gogo git update --parallel
gogo git update --include-only api,web| Option | Description |
|---|---|
--group <names> |
Only target projects of the named group(s) |
--include-only <dirs> |
Only update specified projects |
--exclude-only <dirs> |
Skip specified projects |
--parallel |
Clone in parallel |
--concurrency <n> |
Max parallel clones |
Show git status across all repositories.
gogo git status
gogo git status --parallel
gogo git status --include-only api| Option | Description |
|---|---|
--group <names> |
Only target projects of the named group(s) |
--include-only <dirs> |
Only check specified projects |
--exclude-only <dirs> |
Skip specified projects |
--parallel |
Run in parallel |
Pull changes in all repositories.
gogo git pull
gogo git pull --parallel| Option | Description |
|---|---|
--group <names> |
Only target projects of the named group(s) |
--include-only <dirs> |
Only pull specified projects |
--exclude-only <dirs> |
Skip specified projects |
--parallel |
Pull in parallel |
--concurrency <n> |
Max parallel pulls |
Push changes in all repositories.
gogo git push
gogo git push --include-only api,web| Option | Description |
|---|---|
--group <names> |
Only target projects of the named group(s) |
--include-only <dirs> |
Only push specified projects |
--exclude-only <dirs> |
Skip specified projects |
--parallel |
Push in parallel |
List, create, or delete branches across all repositories.
# List branches
gogo git branch
# List all branches (including remote)
gogo git branch --all
# Create a new branch
gogo git branch feature/new-feature
# Delete a branch
gogo git branch feature/old-feature --delete| Option | Description |
|---|---|
-d, --delete |
Delete the specified branch |
-a, --all |
List all branches (local and remote) |
--group <names> |
Only target projects of the named group(s) |
--include-only <dirs> |
Only target specified projects |
--exclude-only <dirs> |
Skip specified projects |
--parallel |
Run in parallel |
Checkout a branch in all repositories.
# Checkout existing branch
gogo git checkout main
# Create and checkout new branch
gogo git checkout -b feature/new-feature| Option | Description |
|---|---|
-b, --create |
Create the branch if it doesn't exist |
--group <names> |
Only target projects of the named group(s) |
--include-only <dirs> |
Only target specified projects |
--exclude-only <dirs> |
Skip specified projects |
--parallel |
Run in parallel |
Commit changes in all repositories with the same message.
gogo git commit -m "Update dependencies"
gogo git commit -m "Fix bug" --include-only api| Option | Description |
|---|---|
-m, --message <msg> |
Commit message (required) |
--group <names> |
Only target projects of the named group(s) |
--include-only <dirs> |
Only commit in specified projects |
--exclude-only <dirs> |
Skip specified projects |
Create and initialize a new child repository.
gogo project create libs/new-lib git@github.com:org/new-lib.gitThis will:
- Create the directory
- Initialize git
- Add the remote origin
- Add the project to
.gogo - Add the path to
.gitignore
Import an existing repository as a child project.
# Clone and import a remote repository
gogo project import api git@github.com:org/api.git
# Import an existing local directory (reads remote from git)
gogo project import existing-folder
# Register without cloning (clone later with gogo git update)
gogo project import api git@github.com:org/api.git --no-clone| Option | Description |
|---|---|
--no-clone |
Register project in .gogo without cloning |
This will:
- Clone the repository (if URL provided and directory doesn't exist, unless
--no-clone) - Add the project to
.gogo - Add the path to
.gitignore(unless--no-clone)
Run npm install in all projects.
gogo npm install
gogo npm i # Alias
gogo npm install --parallel| Option | Description |
|---|---|
--group <names> |
Only target projects of the named group(s) |
--include-only <dirs> |
Only install in specified projects |
--exclude-only <dirs> |
Skip specified projects |
--parallel |
Run in parallel |
--concurrency <n> |
Max parallel installs |
Run npm ci in all projects (clean install from lockfile).
gogo npm ci
gogo npm ci --parallel| Option | Description |
|---|---|
--group <names> |
Only target projects of the named group(s) |
--include-only <dirs> |
Only run in specified projects |
--exclude-only <dirs> |
Skip specified projects |
--parallel |
Run in parallel |
--concurrency <n> |
Max parallel processes |
Create npm links between projects for local development.
# Create global npm links for all projects
gogo npm link
# Create symlinks between all interdependent projects
gogo npm link --all| Option | Description |
|---|---|
--all |
Link all projects bidirectionally (symlink dependencies) |
--group <names> |
Only target projects of the named group(s) |
--include-only <dirs> |
Only link specified projects |
--exclude-only <dirs> |
Skip specified projects |
Run an npm script in all projects.
# Run in all projects
gogo npm run build
# Run in parallel
gogo npm run test --parallel
# Only run if script exists
gogo npm run lint --if-present| Option | Description |
|---|---|
--if-present |
Only run if the script exists in package.json |
--group <names> |
Only target projects of the named group(s) |
--include-only <dirs> |
Only run in specified projects |
--exclude-only <dirs> |
Skip specified projects |
--parallel |
Run in parallel |
--concurrency <n> |
Max parallel processes |
Validate all config files in the current directory.
gogo validateChecks .gogo, .gogo.yaml, and .gogo.yml files for valid syntax and structure.
Converge the working copy on the configuration in one run: move projects that are checked out at another path, clone the ones that are missing, and fast-forward the rest.
gogo update # move, clone and fast-forward everything
gogo update --dry-run # show what would happen, change nothing
gogo update --parallel # clone and fast-forward concurrently
gogo update --no-pull # only move and clone
gogo update --group backend # restrict to a group| Option | Description |
|---|---|
--dry-run |
Show what would be done without changing anything |
--no-migrate |
Do not move projects checked out at another path |
--no-clone |
Do not clone projects that are missing |
--no-pull |
Do not fast-forward projects that are present |
--parallel |
Clone and fast-forward concurrently |
--concurrency <n> |
Max parallel processes (default: 4) |
--group <names> |
Only target projects of the named group(s) |
--include-only <dirs> |
Only update specified projects |
--exclude-only <dirs> |
Skip specified projects |
--include-pattern <re> |
Include projects matching a regex |
--exclude-pattern <re> |
Exclude projects matching a regex |
What it never does. gogo update does not merge, rebase, stash, or switch
branches. Each present project is fetched and then advanced with
git merge --ff-only, so a project that has diverged from its upstream is
reported and left untouched for you to resolve.
A dirty working tree is not a reason to skip a project: the fast-forward is attempted, and git itself refuses it when your local changes would be overwritten. Work in progress that does not overlap the incoming changes survives.
| Situation | Result |
|---|---|
| Behind upstream | updated (N commit(s)) |
| Up to date, or ahead only | up to date — nothing is pushed for you |
| Diverged from upstream | failed — resolve it manually |
| Detached HEAD, or no upstream | skipped, and the run still succeeds |
| Occupied by a different repository | failed — no move is applied for any project |
Exit code is 1 when any project failed, 0 otherwise. Projects that were
skipped never cause a non-zero exit.
Passing --no-migrate --no-clone --no-pull together is an error: there would be
nothing left to do.
Move/rename working-copy directories to match the configuration.
gogo migrate # Rename directories to match .gogo config paths
gogo migrate --dry-run # Show what would be moved without making any changes| Option | Description |
|---|---|
--dry-run |
Show what would be moved without changing anything |
mkdir my-project && cd my-project
gogo init
gogo project import backend git@github.com:org/backend.git
gogo project import frontend git@github.com:org/frontend.git
gogo project import shared git@github.com:org/shared.git
gogo npm install --parallel# Start of day: pull all changes
gogo git pull --parallel
# Check status across all repos
gogo git status
# Create feature branch everywhere
gogo git checkout -b feature/my-feature
# Run tests
gogo npm run test --parallel
# Commit changes
gogo git commit -m "Add feature"
# Push changes
gogo git push# Only work with API and shared libs
gogo exec "npm test" --include-only api,libs/shared
# Exclude documentation from builds
gogo npm run build --exclude-only docs
# Target all libs
gogo git status --include-pattern "^libs/"- Go 1.24 or higher
- Git
make help # Show all available targets
make build # Build the gogo binary to dist/gogo
make docker # Build the gogo container image locally (Dockerfile.local)
make fmt # Run go fmt
make lint # Run golangci-lint
make test # Run all tests
make test-coverage # Run tests and generate coverage report (coverage/)
make clean # Remove build artifacts (dist/, coverage/)
make all # Clean, lint, test-coverage, then build./
├── cmd/gogo/ # Entry point
├── internal/
│ ├── cli/ # Cobra command definitions
│ ├── config/ # Config parsing, merging, validation
│ ├── executor/ # Shell command execution
│ ├── filter/ # Include/exclude filtering
│ ├── loop/ # Multi-repo orchestration
│ ├── output/ # Terminal formatting
│ └── ssh/ # SSH host key management
├── Makefile
├── .golangci.yml
└── go.mod
gogo runs arbitrary shell commands from the .gogo file (gogo run) and from the command line (gogo exec). Only run gogo in meta-repositories you trust. See SECURITY.md for the full trust model and the built-in hardening.
- Go 1.24 or higher
- Git
MIT