Version Control

Everything is in Git, on our self-hosted Gitea server, with Git LFS for binary assets.
Game projects, tools, packages, this vault: one system, one commit format.

We used to use Unity Version Control

UVC and the Gluon client are gone. If you find a page, a note or a colleague still describing
checkouts in Gluon, that is out of date. Please say so.

The concepts carried over almost unchanged, which is why this page reads similarly: binary files
are still locked while you hold them, generated files are still never merged by hand, and a
.meta still travels with its asset. Only the tool changed.

Setup

→ Gitea & Git LFS for installing the client, getting an account, and cloning your first project.

Two things are configured per repository and are worth understanding rather than just copying:

.gitattributes decides which files go through LFS and which are lockable. The lockable list
is binary or merge-hostile formats, where two people editing at once means one of them loses work:

lockExtensions

.blend · .psd · .unity · .prefab · .asset · .IMP · .flp · .clip · .shadergraph · .vfx · .controller · .anim · .mat · .mixer · .fbx · .png · .tga · .exr · .psb · .wav · .mp3 · .ogg

.gitignore keeps generated folders like Unity's Library out of the repository:

ignorePaths

Library/ · Temp/ · Obj/ · Build/ · Builds/ · Logs/ · UserSettings/ · MemoryCaptures/ · Recordings/ · *.csproj · *.sln · *.user · *.pidb · *.booproj · *.svd

Both lists are generated

From Asset Rules (Generated), so the list here and the list a tool checks against cannot drift.
Change them in the bubbly-asset-rules repo. A repository's .gitattributes and .gitignore
still have to be updated to match. The list above is what they should say.

Locking

Our project files are binary. Git can store them and version them; it cannot merge them. So for
anything on the lockable list, you take a lock, do the work, and release it.

Locking, unlocking and seeing who holds what are all in our own Git client (see
Gitea & Git LFS). You should not need a terminal for any of it.

Underneath it is Git LFS locking, so the equivalent commands exist if you ever want them:

git lfs lock "Assets/Source/Meshes/Prop/Furniture/SM_Prop_Furniture_Table_01.blend"
git lfs locks          # who holds what
git lfs unlock <path>  # when you are done

Lock before you edit, not after. A lock taken after you have already made changes tells you that
someone else was editing it, at the point where that information is least useful.

Release when you are done, not at the end of the week. A forgotten lock is invisible to you and
blocking to somebody else. This is the single most common friction in a locking workflow, in every
studio that has ever used one.

Unblocking a stale lock

  1. Contact the person holding it. Usually that is the whole story.
  2. If it is unclear who that is, contact whoever owns that asset or that type of asset.
  3. If they are unavailable, contact a studio lead. Force-unlocking is theirs to do, because it can
    destroy work that has not been pushed.

Branching

Art and content work happens directly on main. Files are locked while someone holds them, so
there is no risk of two people editing one at once, and branching binary assets only adds merge steps
that cannot be resolved automatically anyway.

Code work happens on task branches (task-<id>-<short-description>) and is merged back after review.

main is always buildable. Builds are made from main and tagged; see Builds.

.meta files

Two rules, and they cause more silent breakage between them than anything else in this document.

  • Never move, rename or delete an asset outside the Unity editor. Not in Explorer, not in your
    Git client. Doing so orphans the .meta and breaks every reference to that asset. This is the
    single most common way a Unity project gets silently broken, and it is tempting precisely because
    moving files in Explorer is faster.
  • A .meta always travels with its asset in the same commit. A commit holding the .fbx but not
    its .meta gives everyone else a re-imported asset with a fresh GUID and broken references.

VCS003 checks the second one. Nothing can check the first. It is a habit.

What is in version control

Path In version control?
/Unity Yes
/Assets/Source (.blend, .psd, .IMP, .clip, .flp) Yes, via LFS
/Assets/Exported Yes, generated, never hand-edited
/Assets/Raw (field recordings, gameplay footage) No, use Q-Dex / Bubbly Backup
/Documents, /Outreach No, use Q-Dex / Bubbly Backup
Exported assets are regenerated, not merged

If two people conflict on an .fbx, .png or anything else under /Exported, the answer is to
re-export it from source. Never resolve the diff by hand. The result will look plausible and
be wrong, which is the worst of both outcomes.

Commit Format

<type>[optional scope]: <description>

[optional body]

[optional footer(s)]

Type can be any of these:

commitFormat

fix · feat · build · chore · ci · docs · style · refactor · perf · test

Key Value
breakingMarker !

Pattern: ^(fix|feat|build|chore|ci|docs|style|refactor|perf|test)(\([^)]+\))?!?: .+

The type can be followed by a ! to mark breaking changes.

Scope: the area affected by the change.

Description: short, plain text, what changed.

Body: optional. Free text explaining why the change was made, not what it did; the description
already covers that. Worth writing whenever someone reading the history in six months would otherwise
have to guess.

Footers: : separated key-value pairs, e.g. Reviewed-by: Janis.

This now applies to game projects too

It used to apply only to the Git repositories holding tools and packages, because game projects
were in UVC and had no commit convention. Now everything is Git, so everything follows it.

It is not cosmetic: changelogs are generated from commit types. feat and fix are
shown to testers, everything else is hidden. A commit with no valid type is a change that silently
never reaches a tester.

Example

feat(api)!: New access points for getting comments

New api endpoints:
- Get Comments
- Get Commenter

BREAKING CHANGE: Removed old Comments endpoint.

Reviewed-by: Janis

Read more