The Daily Git Terminal Workflow
This is the workflow every developer at the studio follows, from a fresh machine to a merged pull request, using only the terminal. If you are new to Git, read it top to bottom once; after that, use the numbered steps as a reference.
Step 1: One-Time Machine Setup
Install Git, then tell it who you are and how to behave. You only do this once per machine. Check whether Git is already installed with git --version; if it prints a version of 2.38 or newer, skip the install.
macOS
brew install git
No Homebrew? Run xcode-select --install instead to get Apple's Git.
Linux
sudo apt update && sudo apt install git
Fedora: sudo dnf install git
Windows
winget install --id Git.Git -e --source winget
This installs Git for Windows, which includes Git Bash. Restart your terminal afterwards.
Next, set your identity and the studio defaults. Use the same email address as your GitHub account so your commits are linked to your profile.
git config --global user.name "Your Name"
git config --global user.email "you@shatteredvales.studio"
git config --global init.defaultBranch main
git config --global pull.rebase true
git config --global rebase.autoStash true
git config --global push.autoSetupRemote true
git config --global fetch.prune true
git config --global merge.conflictStyle zdiff3
git config --global rerere.enabled true
- pull.rebase:
git pullreplays your local commits on top of the remote instead of creating a "merge commit" every time you pull. - rebase.autoStash: uncommitted changes are set aside automatically during a rebase and restored afterwards.
- push.autoSetupRemote: the first
git pushon a new branch sets up tracking for you (see Step 5). - fetch.prune: removes your local references to branches that were deleted on GitHub.
- merge.conflictStyle zdiff3: conflict markers also show the original version of the code, which makes conflicts much easier to read (see Step 7).
- rerere.enabled: "reuse recorded resolution". Once you resolve a conflict, Git remembers how and resolves the same conflict automatically if it comes up again.
Finally, set your line-ending behavior so files don't show as changed on every line when Windows and macOS developers work on the same repository, and choose the editor Git opens for commit messages.
macOS
git config --global core.autocrlf input
git config --global core.editor "nano"
Linux
git config --global core.autocrlf input
git config --global core.editor "nano"
Windows
git config --global core.autocrlf true
git config --global core.editor "notepad"
Prefer VS Code? Use git config --global core.editor "code --wait" on any platform. Review everything with git config --global --list.
Step 2: Know the Branch Model
A branch is a movable label pointing at a line of commits. Your computer has local branches; GitHub has remote branches, which Git shows with an origin/ prefix (for example origin/Dev). They only change when you fetch, pull, or push.
| Branch | Purpose | Who changes it |
|---|---|---|
main | Production. What customers and players see. | Maintainers, by promoting QA |
QA | Release candidate under test. | Maintainers, by promoting Dev |
Dev | Integration branch. All finished work lands here first. | Anyone, through an approved pull request |
feature/…, fix/… | Your work in progress, one branch per issue. | You |
main, QA, and Dev are protected: you never commit to them directly. Branch names are case-sensitive, so type Dev, not dev. Name your branches <type>/issue-<number>-<short-description>, for example feature/issue-42-inventory-sort or fix/issue-57-save-crash.
Step 3: Start the Day: Sync and Branch
Always start new work from an up-to-date Dev. Starting from a stale branch is the most common cause of painful conflicts later.
git switch Dev
git pull
git switch -c feature/issue-42-inventory-sort
git switch -c creates the branch and moves you onto it. (Older tutorials use git checkout -b, which does the same thing.) Already have a branch in progress? Switch back to it with git switch <branch-name> and bring it up to date with Step 6.
Step 4: Stage and Commit with Conventional Commits
Git commits in two stages. First you stage changes (choose what goes into the next commit), then you commit them with a message. Review before you stage, and review again before you commit.
git status # what changed?
git diff # line-by-line changes not yet staged
git add -p # stage changes hunk by hunk (y = yes, n = no, s = split, q = quit)
git add path/to/file.cs # or stage a whole file
git diff --staged # exactly what will be committed
git commit # opens your editor for the message
Avoid git add . until you have checked git status: it stages everything, including debug files and anything you forgot to ignore. Commit small, focused units of work several times a day rather than one huge commit at the end.
The commit message format
The studio uses Conventional Commits (opens in a new tab). Every message starts with a type, an optional scope in parentheses, and a short summary:
<type>(<scope>): <summary>
<body: what changed and why, wrapped at 72 characters>
<footer: issue references, breaking changes>
| Type | Use it for |
|---|---|
feat | A new feature or behavior a user or player can notice. |
fix | A bug fix. |
docs | Documentation only (README, comments, guides). |
style | Formatting, whitespace, or naming with no change in behavior. |
refactor | Restructuring code without changing what it does. |
perf | A change that makes code faster or use less memory. |
test | Adding or fixing tests. |
build | Build system, packages, or dependencies. |
ci | CI/CD pipelines and GitHub Actions. |
chore | Maintenance that fits nowhere else (version bumps, config). |
revert | Undoing an earlier commit. |
- Write the summary in the imperative mood, as if completing "This commit will…":
add, notaddedoradds. - Keep the summary under 72 characters, lowercase, with no trailing period.
- The scope is the area of the codebase:
inventory,auth,billing,ui. - Use the body to explain why. The diff already shows what.
- Reference the issue in the footer with
Refs #42. - Mark breaking changes with
!after the type and aBREAKING CHANGE:footer.
Good examples:
feat(inventory): sort items by rarity, then name
fix(save): prevent crash when the save folder is read-only
perf(tilemap): cache chunk meshes instead of rebuilding each frame
docs(readme): add local database setup steps
Poor examples:
fixed stuff
WIP
Updated PlayerController.cs
feat: changes
A full message with body, footer, and breaking change:
feat(api)!: require tenant ID on all report endpoints
Reports were resolving the tenant from the session cookie, which
broke for API-key clients that have no session. Every report
endpoint now takes an explicit tenantId route parameter.
BREAKING CHANGE: /reports/{id} is now /tenants/{tenantId}/reports/{id}
Refs #118
Step 5: Push and Set Upstream Tracking
A new local branch doesn't exist on GitHub until you push it. The first push should also set the branch's upstream: the remote branch it tracks. Once that's set, plain git push and git pull know where to go, and git status can tell you whether you are ahead of or behind GitHub.
git push -u origin feature/issue-42-inventory-sort
-u is short for --set-upstream. If you set push.autoSetupRemote in Step 1, a plain git push does the same thing on the first push. To check or fix tracking:
git branch -vv # every branch with its upstream and ahead/behind counts
git branch --set-upstream-to=origin/feature/issue-42-inventory-sort # fix tracking on the current branch
git branch --unset-upstream # stop tracking
In git status, "ahead 2" means you have 2 commits GitHub doesn't have yet (push them); "behind 3" means GitHub has 3 commits you don't (pull them). "Diverged" means both are true; after a rebase (Step 6) that is expected.
Step 6: Rebase onto the Latest Dev, Step by Step
While you work, other pull requests merge into Dev. Rebasing takes your commits off your branch, moves the branch's starting point to the newest Dev, and replays your commits on top, one at a time. The result is a clean, straight history, and a pull request that shows only your changes. Rebase at least daily and always before you open a pull request.
- Make sure your working tree is clean (
git statussays "nothing to commit"). Commit or stash anything in progress. - Optionally, create a safety bookmark of your branch as it is now.
- Download the latest state of GitHub without changing any of your files.
- Replay your commits onto the newest
Dev. If Git stops with a conflict, go to Step 7, then come back. - Check the history looks right, then build and run the tests.
- Push. Because the commits were rewritten, a normal push is rejected; use
--force-with-lease.
git status # 1. must be clean
git branch backup/issue-42 # 2. optional safety bookmark
git fetch origin # 3. update origin/* references
git rebase origin/Dev # 4. replay your commits on top of Dev
git log --oneline --graph -15 # 5. your commits should sit on top of Dev
git push --force-with-lease # 6. update your branch on GitHub
--force-with-lease overwrites your remote branch only if nobody else has pushed to it since you last fetched. Plain --force overwrites unconditionally and can silently delete a teammate's work, so don't use it. If a rebase goes wrong at any point, git rebase --abort puts everything back exactly as it was before you started. Delete the bookmark when you're done with git branch -D backup/issue-42.
Tidy your commits before review (interactive rebase)
Before you open a pull request, you can squash "fix typo" and "WIP" commits into meaningful ones. Interactive rebase opens your editor with a list of your commits; change the word at the start of each line, save, and close.
git rebase -i origin/Dev
| Word | Effect |
|---|---|
pick | Keep the commit as is. |
reword | Keep the commit, but edit its message. |
squash | Merge into the commit above and combine both messages. |
fixup | Merge into the commit above and discard this message. |
drop | Delete the commit entirely. |
Shortcut: when you notice a mistake in an earlier commit, commit the fix with git commit --fixup <commit-id>. Later, git rebase -i --autosquash origin/Dev lines the fixups up under the right commits for you.
Step 7: Resolve Merge Conflicts Cleanly
A conflict happens when your commit and someone else's both changed the same lines, and Git can't decide which version wins. It isn't an error, and nothing is lost: Git pauses and asks you to decide. You will see something like:
CONFLICT (content): Merge conflict in src/Inventory/ItemSorter.cs
error: could not apply 3f2a9c1... feat(inventory): sort items by rarity, then name
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
- Run
git status. Conflicted files are listed under "Unmerged paths" as both modified. - Open each conflicted file and search for
<<<<<<<. Each conflict looks like the block below. - Edit the block until it contains the code that should exist after both changes. This is often a combination of both sides, not one or the other. Delete all the marker lines.
- Check you didn't leave any markers behind, then stage the file to mark it resolved.
- Continue the rebase. Git moves on to your next commit and may stop again; repeat until it finishes.
- Build and run the tests. A conflict-free result can still be broken code.
Anatomy of a conflict (zdiff3 style):
<<<<<<< HEAD
items.Sort((a, b) => a.Value.CompareTo(b.Value));
||||||| parent of 3f2a9c1
items.Sort();
=======
items.Sort((a, b) => a.Rarity.CompareTo(b.Rarity));
>>>>>>> 3f2a9c1 (feat(inventory): sort items by rarity, then name)
Mark resolved and continue:
git diff --check # reports any leftover conflict markers
git add src/Inventory/ItemSorter.cs
git rebase --continue # during a merge instead: git merge --continue
- Top section (
HEAD): the code already on the branch you are rebasing onto, that is, the latestDev. - Middle section (
|||||||): the original code before either change. Compare each side with it to see what each person actually changed. - Bottom section: your commit's version.
In the example above, Dev changed the sort to use Value and you changed it to use Rarity. The right answer depends on what both changes were for, so read both commits (and ask their author) before choosing. If you're unsure, stop and ask; a wrong resolution is a bug that nobody reviews, because it looks like normal code.
Generated files (package lockfiles, compiled assets) should not be hand-merged: take either side, then regenerate them with the project's tool. Unity scenes and prefabs need special care; see Managing Game Assets with Git LFS. To give up and return to where you started, run git rebase --abort (or git merge --abort).
Step 8: Open the Pull Request and Clean Up
When your branch is rebased, tested, and pushed, open a pull request into Dev. You can do this from the terminal with the GitHub CLI. The code review guide covers what makes a good pull request.
gh pr create --base Dev --fill
While the pull request is in review, push fixes as new commits so reviewers can see what changed since their last look. After it merges, delete your local branch and sync:
git switch Dev
git pull
git branch -d feature/issue-42-inventory-sort # -d refuses if the work isn't merged; that's a safety check
Step 9: Recover from Common Mistakes
Almost nothing in Git is truly lost once it has been committed. Before trying anything drastic, find the command for your situation below, and ask in the team channel if you're unsure.
| Situation | Command |
|---|---|
| Discard your unsaved edits to a file | git restore <file> |
| Unstage a file (keep the edits) | git restore --staged <file> |
| Fix the last commit's message or add a forgotten file (not yet pushed) | git commit --amend |
| Undo the last commit but keep its changes staged (not yet pushed) | git reset --soft HEAD~1 |
| Undo a commit that's already on a shared branch | git revert <commit-id> |
| Set work aside temporarily | git stash, later git stash pop |
| Committed to the wrong branch | git switch -c <new-branch>, then ask a lead to help clean up the original |
| "I lost my commits" after a reset or rebase | git reflog, then git branch rescue <commit-id> |
git reflog lists every commit your branch has pointed at recently, including ones that no longer appear in git log. Treat git reset --hard and git clean -fd with care: they permanently delete uncommitted work, and nothing can bring it back.