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.
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:
.gitignore keeps generated folders like Unity's Library out of the repository:
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
- Contact the person holding it. Usually that is the whole story.
- If it is unclear who that is, contact whoever owns that asset or that type of asset.
- 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.metaand 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
.metaalways travels with its asset in the same commit. A commit holding the.fbxbut not
its.metagives 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 |
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:
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.
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