# 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.

- URL: https://shotmatic.app/blog/one-repo-many-products-versioning
- Category: Versions & Releases
- Published: 2026-09-27
- Author: Tu Nguyen

## Key takeaways

- Keeping many products in one repo is rarely the real problem; the problem is that git has no record of which product shipped which version.
- Tag every release with the product name in front, like froggy-jump@1.0.1, and let git answer every version question.
- A version is three different things: a code snapshot (a tag), a plan and changelog (text files), and a build (which does not belong in git).
- Only split a product into its own repo when it has its own people, its own CI, its own license or its own size problem, and take its history with you.

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](https://shotmatic.app/blog/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](https://shotmatic.app/blog/phases-not-todo-lists) 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.**

```bash
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:

```bash
# 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.

```bash
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](https://shotmatic.app/blog/run-10-projects-in-parallel)).

When you do split, don't copy the folder. Take its history:

```bash
# 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:

```bash
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:

```bash
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.
