gg_multi
gg_multi is the multi-repository workspace engine of the Gg Multi
Suite. It manages a master workspace of registered repositories
and organisations, lets you create ticket workspaces that scope a
subset of those repos to a single feature or bugfix, and orchestrates
cross-repo actions (commit, push, review, publish, …) in dependency
order.
gg_multi is normally driven via the gg CLI (which auto-detects
workspace vs. single-package mode), but it also ships its own
executable for direct use and in CI/CD pipelines.
A complete German hands-on walkthrough is available in
handbook.md— recommended reading for new users.
What gg_multi gives you
- A persistent master workspace under
.master/containing every registered repo and organisation. - Per-ticket workspaces under
tickets/<id>/that hold scoped clones of the repos you need for one feature. - Automatic dependency resolution: every cross-repo command runs in dependency order so downstream packages see consistent upstream state.
- Path localisation: while you work on a ticket, intra-workspace
pubspec.yamlreferences point to local paths; on review they are re-localised to Git refs. - A single review pipeline (
do review) that brings every repo of a ticket into a state ready for merge or publish.
Installation
git clone https://github.com/ggsuite/gg_multi.git
cd gg_multi
./install # or install.bat on Windows
This installs the gg_multi executable globally. In most day-to-day
work you will use the gg CLI instead (dart pub global activate gg), which routes its shared can/did/do commands to gg_multi
whenever you are inside a workspace.
Command Hierarchy
gg_multi
├── ls repos | organizations | deps <target> | tickets
├── can commit | push | publish | review
├── did commit | push
└── do commit | push | publish | review | cancel-review
add | add-deps | rm | create ticket
init | code | claude
execute | install-git-hooks | install-gitattributes
All cross-repo commands run inside a ticket directory
(tickets/<id>/) and iterate over the ticket's repos in dependency
order.
gg_multi ls
| Command | Purpose |
|---|---|
gg_multi ls repos |
list every repo in the master workspace, sorted by name |
gg_multi ls organizations |
list every GitHub organisation represented in the master workspace |
gg_multi ls deps <target> |
list dependencies / dev_dependencies of <target> |
gg_multi ls tickets |
list every ticket workspace under tickets/ |
gg_multi do — workspace setup
| Command | Purpose |
|---|---|
gg_multi do init |
initialise the master workspace in the current directory |
| `gg_multi do add | --force]` |
gg_multi do rm <target> |
remove a repo from the master workspace or delete a ticket workspace |
gg_multi do create ticket <id> [-m <description>] |
create tickets/<id>/ with a .ticket file |
gg_multi do add-deps <target> |
add every dependencies / dev_dependencies of <target> to the master workspace |
gg_multi do code |
open the current ticket in VS Code |
gg_multi do claude |
aggregate each repo's CLAUDE.md into one ticket-level CLAUDE.md |
gg_multi do install-git-hooks |
install gg's git hooks in every ticket repo |
gg_multi do install-gitattributes |
install a shared .gitattributes in every ticket repo |
gg_multi do execute <cmd> |
run a shell command in every ticket repo |
gg_multi do add is context-aware:
- run from the workspace root: the repo is cloned into
.master/, - run from inside a ticket (
tickets/<id>/): the repo is also copied into the ticket and its local dependencies are pulled in.
gg_multi can — preflight checks
| Command | Purpose |
|---|---|
gg_multi can commit |
run gg can commit in every ticket repo (analyze + format + tests) |
gg_multi can push |
check that every ticket repo is push-ready |
gg_multi can publish |
check that every publishable repo is publish-ready |
gg_multi can review |
check that every repo is localized and has no uncommitted changes |
Each can command aborts on the first failure so you find out early
when a repo is in a bad state.
gg_multi do — execute across ticket repos
| Command | Purpose |
|---|---|
gg_multi do commit -m <message> |
commit every ticket repo with the same message |
gg_multi do push [--force] |
push every ticket repo |
gg_multi do review |
unlocalise → localise as Git refs → pub upgrade → commit → push, for every repo |
gg_multi do cancel-review |
revert a review and return to local working mode |
gg_multi do publish |
publish every publishable package of the ticket |
gg_multi did — reporting
| Command | Purpose |
|---|---|
gg_multi did commit |
report which repos have new commits since the last reference |
gg_multi did push |
report which repos have new pushed commits |
Folder layout
my_project/
├── .master/ # every registered repo (managed by gg_multi)
│ ├── gg/
│ ├── gg_multi/
│ └── …
└── tickets/
└── PROJ-123/ # one ticket workspace
├── .ticket # JSON with id + description
├── app_core/ # ticket-scoped clone
└── ui_core/
WorkspaceUtils.detectTicketPath() walks up the directory tree from
wherever you invoke gg_multi to find the matching workspace, so the
commands work from any sub-directory inside it.
Org-prefixed repo folders
Repos newly added to .master/ are cloned into a folder named after
their organization: <org>_<repo> for Dart repos (for example
ggsuite_gg_test) and <org>-<repo> for TypeScript repos. This lets
same-named repos from different organizations coexist on disk. Folders
created before this convention keep their plain repo name and continue
to work: commands resolve a repo by exact folder name first, then by
the package name in its manifest, then by its git remote URL. Ticket
copies always reuse the folder name of the master clone. Note that two
packages with the same manifest name still collide in the dependency
graph — the prefix only solves the on-disk collision.
Step-by-step: working on a ticket end-to-end
0. One-time project setup
mkdir my_project
cd my_project
gg_multi do init
gg_multi do add https://github.com/my-org # pull in every repo of an org
1. Create a ticket workspace
gg_multi do create ticket PROJ-123 -m 'Simplify login flow'
cd tickets/PROJ-123
2. Add the repos you need
gg_multi do add app_core ui_core
Local dependencies are pulled in automatically and pubspec.yaml
references are localised to relative paths inside the ticket.
3. Open the ticket (optional)
gg_multi do code
4. Develop and iterate locally
Work in each repo as you normally would. Inside a single repo you can
run gg one check for the full single-repo check pipeline.
5. Commit across all ticket repos
gg_multi can commit
gg_multi do commit -m 'Simplify login flow'
can commit runs the per-repo check pipeline in dependency order and
aborts on the first failure; do commit then commits each repo with
the same message.
6. Push
gg_multi can push
gg_multi do push
7. Review
gg_multi do review
For every ticket repo this runs:
- Unlocalise references (back to original form via
gg_localize_refs), status →unlocalized. - Re-localise as Git references, status →
git-localized. dart pub upgrade(ifpubspec.yamlexists).gg do commitwith a default review message.gg do push.
Need to keep working after starting a review?
gg_multi do cancel-review
8. Publish (when approved)
gg_multi can publish
gg_multi do publish
Publish is meant to be triggered manually by a human after review approval.
Non-interactive publish via --config
gg_multi do publish is interactive by default — it asks for a merge
message and a version increment (patch / minor / major) for every
publishable repo. In scripted runs, CI pipelines or release tooling
this is awkward. Pass --config <path> to load the merge messages and
increments from a JSON file instead:
gg_multi do publish --config .gg-publish.json
gg_multi looks for the file at <configArg> first (relative to the
current directory, or absolute), then under the ticket directory.
Missing fields cause a hard FormatException — no silent fall-back to
an interactive prompt.
.gg-publish.json schema
{
// Top-level defaults applied to every repo of the ticket.
"version_increment": "patch", // "patch" | "minor" | "major"
"merge_message": "Default merge message",
// Optional: per-repo overrides. Keyed by the repo's directory name
// inside the ticket. Each field is independent — anything missing
// falls back to the top-level value.
"repos": {
"<repoName>": {
"version_increment": "minor",
"merge_message": "Custom message for this repo"
},
"<otherRepo>": {
"merge_message": "Only override the message, keep the default increment"
}
}
}
Resolution order per repo:
repos.<repoName>.version_increment/merge_message, then- top-level
version_increment/merge_message.
If neither layer supplies a value for a repo that is about to publish, the run aborts with an error naming the missing field.
Example
// tickets/PROJ-123/.gg-publish.json
{
"version_increment": "patch",
"merge_message": "PROJ-123: simplify login flow",
"repos": {
"app_core": {
"version_increment": "minor",
"merge_message": "PROJ-123: new public login API"
}
}
}
Then:
cd tickets/PROJ-123
gg_multi do publish --config .gg-publish.json
→ app_core gets a minor bump with the custom message; every other
ticket repo gets a patch bump with the top-level message.
The same flag exists on the single-repo gg one do publish (it reads
the same schema but only uses the top-level fields). See the
gg_one README for details.
Running tests
dart test
A coverage report can be generated with:
dart run coverage:format_coverage --lcov --in=coverage --out=coverage/lcov.info
gg_multi is held at 100 % test coverage.
Getting help
gg_multi -h
gg_multi do -h
gg_multi do add -h
gg_multi can -h
gg_multi ls -h
Further reading
handbook.md— full hands-on walkthrough (German).- The sibling
ggpackage — unified CLI that auto-routes shared commands betweengg_one(single repo) andgg_multi(workspace).
License
gg_multi is licensed under the terms specified in the LICENSE
file.