Summary

A branch is an independent line of development. In game development, branches let you work on a new feature — an enemy AI, a new level, a combat mechanic — without touching the stable version of your project. If the feature works, you merge it in. If it breaks things, you discard it. Because Git branches are extremely lightweight (just a pointer to a commit), creating and switching branches is instant and has no performance cost.


Key ideas

What a branch actually is

When you make commits, Git builds a chain of snapshots. A branch is simply a named pointer that tracks the most recent commit on that line of work. The special pointer HEAD always points to the branch you are currently on — it tells Git “this is where my next commit should go”.

main ──────────────────────────────► commit-C ← HEAD

When you create a new branch from main and make commits on it, the history diverges:

main  ─────────────────────────────► commit-C
                                           ↓
feature-enemy-ai  ────────────────► commit-D → commit-E ← HEAD

Creating and switching branches

git branch feature-enemy-ai        # create a branch (stay on current branch)
git checkout feature-enemy-ai      # switch to it

Or create and switch in one step:

git checkout -b feature-enemy-ai   # classic syntax
git switch -c feature-enemy-ai     # modern syntax (Git 2.23+)

Unity works fine while you switch branches. Git updates the files in your working tree to match the branch you switch to. Unity will detect the changes and reload.

Listing and deleting branches

git branch                      # list all local branches (* = current)
git branch --merged             # branches already merged into current (safe to delete)
git branch --no-merged          # branches with unmerged work
git branch -d feature-enemy-ai  # delete (safe — refuses if unmerged work remains)
git branch -D feature-enemy-ai  # force delete (even if unmerged)

Merging

When a feature is ready, merge it back into main:

git checkout main               # switch to the branch you are merging INTO
git merge feature-enemy-ai      # merge the feature branch in

Fast-forward merge

If main has not moved since the branch was created, Git can simply move the main pointer forward to the tip of the feature branch. No new commit is created.

Before:
main ────────────────────────► commit-C
feature ─────────────────────► commit-C → commit-D → commit-E

After fast-forward:
main ────────────────────────────────────────────────► commit-E

Git will report: Fast-forward.

Three-way merge

If both main and the feature branch have new commits since they diverged, Git performs a three-way merge: it compares both branch tips against their common ancestor and creates a new merge commit combining both lines.

git merge feature-combat
# Git creates a new merge commit automatically
# You will be prompted to write a commit message

Resolving merge conflicts

A merge conflict happens when both branches changed the same lines of the same file in different ways. Git cannot automatically decide which version is correct — it needs you to choose.

What a conflict looks like in a file

<<<<<<< HEAD
float moveSpeed = 5f;
=======
float moveSpeed = 8f;
>>>>>>> feature-movement-overhaul
  • <<<<<<< HEAD — the version from your current branch (the one you merged into)
  • ======= — separator
  • >>>>>>> feature-movement-overhaul — the version from the incoming branch

Resolving the conflict

  1. Open the conflicted file in your editor
  2. Choose which version to keep (or write a combined version)
  3. Delete all the conflict markers (<<<<<<<, =======, >>>>>>>)
  4. Save the file
  5. Stage the resolved file and commit:
git add PlayerMovement.cs
git commit -m "Merge feature-movement-overhaul: use 8f move speed"

Example resolution

// Chose the feature branch value after discussion with team:
float moveSpeed = 8f;

Tips for avoiding conflicts:

  • Communicate with teammates before editing the same scripts
  • Keep each feature branch focused — smaller branches conflict less
  • Pull from main frequently when working on long-running features

Typical feature branch workflow

This is the standard workflow for Unity game teams:

# 1. Make sure main is up to date
git checkout main
git pull
 
# 2. Create a branch for the new feature
git checkout -b feature-inventory-system
 
# 3. Work on the feature — commit as you go
git add Assets/Scripts/Inventory/
git commit -m "Add item pickup and storage logic"
git commit -m "Add inventory UI panel with slots"
 
# 4. When ready, merge back to main
git checkout main
git pull                              # get any new team changes first
git merge feature-inventory-system
 
# 5. Push to GitHub
git push origin main
 
# 6. Delete the feature branch (optional but tidy)
git branch -d feature-inventory-system

Commit as you go, even for scenes and prefabs. Commits on your feature branch are local until you push them, and once pushed they still affect nobody else’s work until the branch is merged through a pull request. So frequent small commits are safe, and each one is a point you can return to if a change to a scene or prefab goes wrong. Two Unity-specific habits make them useful:

  • Commit a scene together with the prefabs and assets it depends on, and their .meta files. The .meta file holds the asset’s unique ID; “if an asset loses its .meta file, any reference to that asset is broken” (Unity Technologies, Unity Manual: Asset metadata). A commit that contains a scene but not the new prefab it references, or a prefab without its .meta, will not open cleanly for a teammate or for your future self.
  • Push your branch at least daily. A pushed branch is a backup on GitHub, and it still does not touch develop or main (the scene-merging advice below still applies: agree who is editing which scene).

This local merge is fine for a solo project. In a team, steps 4 and 5 are usually replaced by pushing the feature branch and opening a pull request, so that a teammate and an automated build check the work before it lands (see git-github-unity and the next section). If main is protected, a direct git push origin main will be rejected.


Team branching models

A branching model (or branching strategy) is the team’s agreement about which long-lived branches exist, where new work branches from, and where it merges back. Three models dominate.

GitHub Flow

GitHub describes GitHub Flow as “a lightweight, branch-based workflow” with six steps: create a branch, make changes, create a pull request, address review comments, merge the pull request, delete the branch (GitHub, GitHub flow). There is one long-lived branch, main. Every feature or fix gets a short-lived branch from main and returns to main through a pull request; GitHub notes that branch protection settings may block the merge until the pull request meets the repository’s requirements.

main      ──●───────────●───────────●──►
             \         /  \        /
feature-a     ●──●──●─      \      /
feature-b                    ●──●─

Git Flow

Git Flow was proposed by Vincent Driessen in 2010 (Driessen, A successful Git branching model). It has two long-lived branches:

  • main (called master in the original): every commit is a production-ready release. Driessen states that each merge into it is “by definition” a new release.
  • develop: the integration branch, holding “the latest delivered development changes for the next release”. Nightly or test builds come from here.

It adds three kinds of short-lived supporting branch:

BranchBranches fromMerges intoPurpose
feature/*developdevelopOne feature or task
release/*developmain and developStabilise a release: bug fixes and version numbers only, no new features
hotfix/*mainmain and developUrgent fix to the released build

Driessen merges with git merge --no-ff so that each feature remains visible as a group of commits and can be reverted as a unit. In a 2020 “note of reflection” on the same page, he says Git Flow has become “a dogma or panacea” for many teams; teams practising continuous delivery should consider something simpler such as GitHub Flow, while Git Flow still suits explicitly versioned software or projects that support several released versions at once.

Trunk-based development

In trunk-based development, “developers collaborate on code in a single branch called ‘trunk’” and “resist any pressure to create other long-lived development branches” (Hammant, Trunk Based Development). Work is integrated in very small steps; short-lived feature branches are used only for code review and CI checks. Unfinished features are hidden behind feature flags so that the trunk stays releasable at all times.

Comparison

GitHub FlowGit FlowTrunk-based
Long-lived branchesmain onlymain + developmain (trunk) only
Features merge intomain via PRdevelop via PRtrunk, in small and frequent merges
Where tested releases livemain, plus tagsmain only receives release and hotfix mergestrunk, plus tags or release branches
CeremonyLowHighLow process, but needs strong CI and discipline
Best fitSmall teams, continuous integrationVersioned releases, several supported versionsExperienced teams with good automated tests

Which one for a student Unity team?

The following is a recommendation for small (2–6 person) student teams rather than a rule from the sources.

  • Default: GitHub Flow with a protected main. It has the fewest moving parts. Every change goes through a pull request, a teammate reviews it, and an automated test and build must pass before it can merge. main therefore always opens and builds.
  • Add a develop branch (a simplified Git Flow) when you need a separate “stable” build, for example when main must hold only the build you submit for assessment, show at a playtest, or upload to itch.io. Release and hotfix branches are usually unnecessary at this scale: merge develop into main through a pull request when a milestone build has been tested, and tag it (git tag v0.3-alpha).
  • Keep feature branches short-lived in either model. Unity scenes and prefabs merge badly, so a branch that lives for three weeks accumulates conflicts that are painful to untangle. Merge or rebase the latest integration branch into your feature branch at least every few days.

“Should we merge through a QA branch, or straight to main if it works on my machine?” A “QA branch” or “staging branch” in a student team is normally what Git Flow calls develop (the integration branch where features are combined and tested together), or a short-lived release/* branch cut from it to stabilise a build. Either way the answer is the same: do not merge into main just because your local tests pass. Local testing only shows that your branch works on your machine with your Library folder and your scenes; the pull request’s CI build and test run checks it on a clean machine against the current integration branch, and the reviewer checks what the tests cannot. Before merging develop into main for a milestone, run a quick smoke test of the built game (does it start, reach the main loop, save and load?; see qa-and-bug-triage).

Rule of thumb. A feature branch never merges straight into main “to protect the build”. It merges into the integration branch through a pull request that must pass the CI checks: main in GitHub Flow, develop in Git Flow. If you use Git Flow, main only receives tested release merges from develop (and hotfixes). Merging first into main would put untested work into the very branch you are trying to protect.


Protecting branches on GitHub

GitHub can enforce the model so that nobody accidentally pushes untested work to main or develop. By default, administrators can bypass classic branch protection; tick Do not allow bypassing the above settings (or leave the ruleset bypass list empty) if the rules should apply to everyone (GitHub, About protected branches). There are two mechanisms, and both can apply at once; where they overlap, the most restrictive version of a rule wins (GitHub, About rulesets):

  • Rulesets (Settings → under “Code, planning, and automation”, Rules → Rulesets → New ruleset → New branch ruleset). The newer option: several rulesets can target the same branch, they can be switched between Active and Disabled without deleting them, and anyone with read access can see them (GitHub, Creating rulesets for a repository).
  • Classic branch protection rules (Settings → Branches → Add classic branch protection rule). Only one rule can apply to a branch at a time (GitHub, About protected branches).

The settings a student team needs, with their ruleset names:

RuleEffect
Restrict deletionsThe branch cannot be deleted
Block force pushesHistory on the branch cannot be rewritten
Require a pull request before merging, with Required approvals set to 1Changes arrive only via a PR approved by a teammate
Require status checks to pass before mergingNamed CI checks (for example the GameCI test-and-build job) must succeed before the merge button is enabled
↳ Require branches to be up to date before merging (an option of the status-check rule)The pull request branch “must be up to date with the base branch before merging”, so the checks have run against the latest develop or main. GitHub notes the cost: “more builds may be required”, because every PR has to be updated whenever someone else merges first. Without it, “status checks may fail after you merge your branch if there are incompatible changes with the base branch”
↳ Dismiss stale pull request approvals when new commits are pushed (an option of the pull-request rule)An approval no longer counts once new commits change the diff, so a teammate re-approves what actually merges

(Rule names and quotations from GitHub, Available rules for rulesets and About protected branches; the classic equivalents are “Require a pull request before merging”, “Required number of approvals before merging” and “Require status checks to pass before merging”, GitHub, Managing a branch protection rule.)

A pull request routine that leaves no conflict work for others

The settings above enforce that a pull request is reviewed and tested. A team also needs a habit for who deals with conflicts. The following is a recommended team rule rather than a GitHub requirement:

  1. The PR author brings the branch up to date and resolves conflicts before asking for review. Merge the latest integration branch into your feature branch (git switch feature/x, git fetch, git merge origin/develop), fix any conflicts in your own branch, re-run the game, and push. Nobody else should have to untangle your conflicts after you merge. GitHub’s Update branch button on the pull request does this merge for you (or a rebase, via Update with rebase), but it only works “when there are no merge conflicts”; if there are conflicts you must “resolve the conflicts before updating the branch” (GitHub, Keeping your pull request in sync with the base branch).
  2. With “Require branches to be up to date” on, GitHub enforces step 1: if someone else merges first, your PR must be updated again before its merge button returns.
  3. Merge promptly after approval, so the branch does not drift out of date again.

PR checklist (paste into the pull request description, or into a .github/pull_request_template.md file):

  • The project opens and the game builds; the CI check is green.
  • Edit Mode and Play Mode tests pass, and I played the changed feature in the Editor.
  • I only changed scenes and prefabs I own this sprint (or agreed the change with their owner).
  • No stray changes to Library/, Temp/, Logs/, UserSettings/ or unrelated assets; every new asset has its .meta file.
  • Screenshots or a short GIF show the change, plus steps to test it.

A required status check is identified by its job name, so give each CI job a unique name across workflows; duplicate names can block merges with ambiguous results (GitHub, About protected branches). Note also that GitHub reports a skipped job as successful, so a skipped required check does not block a merge (GitHub, Status checks).

Plan availability. GitHub Free gives a “full feature set” on public repositories but only a “limited feature set” on private ones; protected branches and required pull-request reviewers in private repositories are listed as GitHub Pro and GitHub Team features (GitHub, GitHub’s plans). A team on Free can therefore either make the repository public or check whether their university’s GitHub Education or organisation plan provides these features. GitHub’s rulesets documentation names Team and Enterprise plans but does not clearly state which rules work on private Free repositories; check the repository’s Settings page, which will say if a feature is unavailable.


Continuous integration (CI)

Continuous integration means every push or pull request is automatically built and tested on a server, so that a broken build is caught before it reaches the integration branch. For Unity projects on GitHub, the usual free route is GameCI, a set of GitHub Actions that run Unity’s test runner and build the player on GitHub’s servers. The required status check in the table above is the result of such a workflow. See unity-ci-with-gameci for setup, Unity licence activation and an example workflow.

Which branches should CI build: develop or a release branch? For a small team, run the tests and a build on every pull request into the integration branch (develop or main) and on pushes to develop, so the integration branch is always known to work. Make release builds, the ones you submit, playtest or upload to itch.io, from main or from a tag on main, rather than maintaining a separate release branch. GitHub Actions supports this directly: a pull_request trigger with a branches filter runs only for pull requests that target those branches, and a push trigger with a tags filter runs “only when specific tags are pushed” (GitHub, Events that trigger workflows):

on:
  pull_request:
    branches: [main, develop]   # test every PR into the integration branches
  push:
    branches: [develop]         # re-check develop after each merge
    tags: ['v*']                # release build when you push a tag such as v0.3-alpha

A release/* branch only earns its place when a build needs days of fixes while other work carries on in develop; this is a recommendation for student teams, in line with Driessen’s 2020 note that simpler models suit teams that do not maintain several released versions.


Unity-specific notes

Scene files and merge conflicts: With Unity’s default Force Text asset serialisation, .unity scene files (and prefabs) are stored as YAML text, but they are long, machine-generated and full of cross-referencing IDs, so they remain difficult to merge by hand (Unity Documentation, Editor settings: Asset Serialization). Best practices to reduce scene conflicts:

  • Work on different scenes in different branches where possible
  • Keep Unity’s YAML text serialisation (Force Text, the default) — it at least makes scene conflicts readable and lets Smart Merge (UnityYAMLMerge) help
  • Coordinate with teammates before making large scene changes — “I’m editing Level3 scene today”

Prefabs: Similar to scenes, prefab conflicts can be complex. Assign ownership of specific prefabs to specific team members during a sprint.

Meta files: Every asset in Unity has a .meta file storing the GUID. These must be tracked alongside the asset — never add *.meta to .gitignore. If meta files conflict, the version with the correct GUID for your project should be kept.


External references