A few months into running several small products at once, a new kind of mess shows up. The game you launched in spring is on 1.0.1, the browser extension went from 1.1 to 1.2, another extension is preparing 1.6, and the cookbook is on its second edition. They all live in a handful of repos, and you start to wonder: should every product get its own repo before this gets out of hand?
Before answering, look at what git actually knows. In a typical solo studio setup (one repo per kind of product: games, apps, extensions, books) you often find the same two things:
- Zero tags. One
mainbranch, a few hundred commits, and no record of which commit became which store release. The version numbers live only in markdown files. - One repo that keeps growing. Usually the one holding exported PDFs, EPUBs, ZIPs or APKs. Every rebuild adds another full binary to history.
Neither of those is solved by splitting repos. This post is about what does solve them.
A “version” is really three things
Most of the confusion comes from using one word for three different things:
- A snapshot of the code, exactly as it was when you submitted 1.0 to the store. That is git’s job: a tag.
- The plan and the changelog: what 1.0.1 was supposed to do and what it actually did. These are text files that pile up version after version (see ship small versions).
- The build: the
.apk, the extension.zip, the.epub. It is produced from (1) and does not belong in git history.
Once those three are separated, “too many versions in one repo” mostly stops being a problem: a tag weighs nothing, the plans are a few markdown files, and the builds go somewhere else.
Patterns that look tidy but hurt
Folders per version (froggy-jump-v1/, froggy-jump-v2/). The most intuitive and the worst. Every fix
has to be made twice, git blame loses the trail, and the repo doubles with each release. It is doing git’s
job by hand.
A long-lived branch per product in a shared repo. main stops being the truth, a fix to a shared script
has to be merged into every branch, and on your own you will soon forget which branch is current.
Git submodules (one repo per product, glued into a parent). Clean on paper; in practice every clone
needs --recursive, every branch switch needs submodule update, and every change is committed twice.
Useful for large teams with per-repo permissions, rarely worth it for one person.
Note that per-version folders are fine for plans (that’s how phases and plans stay readable), just not for code.
One trunk, one tag per product release
This is what big multi-package repositories do when packages release on their own schedule: one main
branch, and every release gets a tag that starts with the product name.
git tag -a froggy-jump@1.0.0 -m "Froggy Jump 1.0.0, live on the App Store"
git tag -a tab-tamer@1.1.0 -m "Tab Tamer 1.1.0, Chrome Web Store"
git push origin --tags
Every version question now has a plain git answer:
# Which versions of Froggy Jump exist? (newest first, 1.0.10 sorts after 1.0.2)
git tag -l 'froggy-jump@*' --sort=-v:refname
# What changed in this product since the last live release?
git log froggy-jump@1.0.0..HEAD -- games/froggy-jump
# Which release is the current code closest to?
git describe --tags --match 'froggy-jump@*'
# Get the exact code that was submitted as 1.0.0
git switch --detach froggy-jump@1.0.0
Tags cost nothing, don’t change your folders and don’t touch your workflow. A few hundred of them is nothing to git.
Create a branch only when you need one. The classic case: the live 1.0 crashes, while main is already
halfway into 1.1.
git switch -c release/froggy-jump-1.0 froggy-jump@1.0.0
# fix, tag froggy-jump@1.0.1, cherry-pick the fix into main, delete the branch
The branch lives for a day and has one purpose. The tag stays forever.
Get build files out of git history
Binaries you can rebuild should not be versioned by git. In order of preference:
- GitHub Releases attached to the tag.
gh release create froggy-jump@1.0.0 build/froggy.aab --notes-file CHANGELOG.mdgives each version a page, its files and its notes, and the repo stays small. - Git LFS for heavy source assets you still want versioned: layered artwork, original PNGs, cover
files.
git lfs track "*.psd". .gitignoreeverydist/,build/andexport/folder. If it can be generated, don’t commit it.
History that is already bloated can only be cleaned by rewriting it (git filter-repo,
git lfs migrate import). That changes every commit hash, so do it once, deliberately, after a backup.
Not on a Friday afternoon.
Automate the tag, not the thinking
There are good release tools. release-please reads conventional commit messages and opens a release PR
per package, Changesets records a small “bump intent” file with each change, and semantic-release
can tag each package with a name@version format. All three come from the npm world.
A solo studio usually mixes a game engine, native mobile code, browser extensions and ebooks, and it often already has its own source of truth: a status file per product with the version being built and the version that is live. Adding a tool with a second source of truth just gives you two files that disagree.
The simpler route is a small shared release script that runs when a store approves a version:
- read the version from the product’s status file;
- check that the plan for that version is done;
- create and push the tag
<product>@<version>; - optionally create a GitHub Release with the build attached.
Same idea as release-please (versions come from data, not typing), but the data is the file you already keep.
When to actually split a repo
A shared repo is a default, not a rule. Move a product into its own repo when at least one of these is true:
| Sign | Example |
|---|---|
| Someone else works on it and needs their own access | a freelancer building one game |
| It becomes open source | an extension you want public on GitHub |
| It needs its own slow CI that shouldn’t run for other products | a 20-minute iOS build pipeline |
| It dwarfs everything else in size | one game is most of the repo |
| It no longer shares code or workflow with the rest | a product you sold or handed over |
If none apply, stay together: shared scripts are fixed once, one git pull updates everything, and one
overview reads one place (more in running 10 projects in parallel).
When you do split, don’t copy the folder. Take its history:
# Built into git
git subtree split --prefix=games/froggy-jump -b split/froggy-jump
# Or cleaner, on a fresh clone
git filter-repo --subdirectory-filter games/froggy-jump
Rename the old froggy-jump@* tags to plain v* in the new repo, and leave a short README in the old
location pointing to it.
Two small tricks for big repos
Fix an old version without stashing your current work. git worktree gives you a second folder on the
same repository:
git worktree add ../froggy-hotfix froggy-jump@1.0.0
# ...fix, tag, then:
git worktree remove ../froggy-hotfix
Check out only one product. On a second machine or in CI:
git clone --filter=blob:none --sparse <url>
git sparse-checkout set games/froggy-jump scripts
With these two, “the repo is too big” stops being a reason to split.
A rollout in order of risk
| Step | Change | Risk |
|---|---|---|
| 1 | Agree on <product>@<X.Y.Z> and back-fill tags for versions already live |
very low |
| 2 | A shared release script: status file → tag (+ GitHub Release) | low |
| 3 | Ignore build folders; builds go to GitHub Releases | low |
| 4 | Git LFS for heavy source assets | medium |
| 5 | Rewrite old history to drop binaries | high (hashes change) |
| 6 | Split out products that meet the signs above | case by case |
The short answer to the original question: many products in one repo is fine; missing tags is the problem. Folders sort products, tags remember versions, and builds live outside git.
Where ShotMatic fits
ShotMatic reads the status file in each product folder, so every line on a board shows the version being built and the version that is live, across all your repos on one screen. Its GIT strip shows what is still uncommitted or unpushed before you tag, and push always shows the file list first. The tagging itself stays a plain git command you run, or a script of your own.