Versions & Releases

One Repo, Many Products: Versioning Without Copying Folders

Keep dozens of apps, games and books in a few repos without version chaos: per-product git tags, build files out of git, and clear signs it's time to split.

By Tu Nguyen · · 6 min read.md

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 main branch, 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:

  1. A snapshot of the code, exactly as it was when you submitted 1.0 to the store. That is git’s job: a tag.
  2. 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).
  3. 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:

  1. GitHub Releases attached to the tag. gh release create froggy-jump@1.0.0 build/froggy.aab --notes-file CHANGELOG.md gives each version a page, its files and its notes, and the repo stays small.
  2. Git LFS for heavy source assets you still want versioned: layered artwork, original PNGs, cover files. git lfs track "*.psd".
  3. .gitignore every dist/, build/ and export/ 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:

  1. read the version from the product’s status file;
  2. check that the plan for that version is done;
  3. create and push the tag <product>@<version>;
  4. 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.

FAQ

Should each product have its own git repository?

Not by default. One repo per kind of product (games, apps, extensions, books) keeps shared scripts in one place. Split a product out only when it needs its own collaborators, CI, license or storage.

How do I tag versions for several products in one repo?

Put the product name in the tag, for example froggy-jump@1.0.1 or tab-tamer@1.2.0. git tag -l 'froggy-jump@*' then lists one product's releases, and git log froggy-jump@1.0.0..HEAD -- games/froggy-jump shows what changed since.

Should I keep old versions in separate folders like v1 and v2?

No. Copying folders duplicates every bug fix and doubles the repo size. Git already stores every version; a tag is enough to get any of them back. Keep per-version folders only for plans and notes.

Where should build files like APKs, ZIPs and PDFs go?

Attach them to a GitHub Release for the matching tag and ignore the build folders in git. Every rebuild otherwise adds another binary copy that git can never shrink.