Every project you have built so far in this course lived in one folder, on one computer, edited by one person: you. The moment a second person joins the project, or you just want to undo a change from three days ago without undoing everything since, "one folder" stops being enough. You need a system that remembers every version of every file on purpose, knows who changed what and why, and can combine two people's work automatically when it is safe to. That system is called version control. This chapter covers the two you will actually meet at a studio: Git, the tool almost every programmer uses for source code, and Perforce, the tool almost every studio uses for the giant binary files — textures, models, scenes — that Git was never built to handle well. By the end of this chapter you will have made real commits, created a real merge conflict and fixed it by hand, tracked a large file with Git LFS, and understand exactly why an artist at a AAA studio never has to touch a command line.
Picture a solo game project with no version control at all. Every time you make a meaningfully different version of your main scene, you do the only thing that feels safe: save a copy under a new name before you touch anything risky.
This is not a joke exaggerated for a lesson — it is the default state of any folder edited by hand over enough weeks, on a solo project or a team one. It fails in several concrete ways, not just an aesthetic one:
Main_v2.unity and Main_v2_fixed.unity without opening both and eyeballing every object by hand.A version control system (VCS) is software that solves all five problems at once: it records every change to a set of files over time, tags each change with who made it, when, and a message saying why, and lets you inspect, compare, restore, or combine any earlier version — without you ever renaming a single file. The "current" version is just whatever the tool currently has checked out; there is no need for a filename to encode "which one is final," because the tool already knows.
There are two broad shapes a VCS can take, and this chapter covers one tool of each shape:
Both solve the "final_v2_REAL_final" problem completely. Where they differ — and why a studio ends up using both at once — is exactly the subject of the second half of this chapter, once you understand how Git itself actually works.
Git organizes every file you are tracking into three areas, and almost every Git command is really about moving something from one of these areas to the next.
The working directory is just your project folder, exactly as you see it in a file explorer. The staging area (also called the index) is a holding pen: a list of exactly which changes you have decided belong in the next commit. It exists so you can build a commit out of only part of what you changed — edit five files, but stage and commit only two of them as one focused change, leaving the other three for a separate commit later. The repository is the permanent history, stored inside a hidden .git folder that Git creates once, right at the root of your project.
A commit is the core unit of that history. It is easy to think of a commit as "the changes since last time," but that is not what Git actually stores — every commit is a full snapshot: the complete state of every tracked file at that moment, not a list of edits. Git is smart about not literally duplicating unchanged files' bytes on disk, but conceptually, and for everything you will do with it, a commit is a complete picture, not a diff.
Every commit also stores a pointer to its parent commit — the commit that came immediately before it. That single detail is what turns a pile of snapshots into a connected history: a chain you can walk backward through, one parent at a time, all the way to the very first commit, which has no parent at all.
Every commit is also given a unique fingerprint: a long hexadecimal (base-16) ID, computed automatically from the snapshot's exact content, its parent, its author, and its message. You will see these written two ways: the full 40-character ID, or a shortened first-seven-characters version like a1b2c3d, which is what most commands print by default because it is almost always unique enough to identify one commit.
Time to actually run these commands. Everything below happens in a terminal, inside an empty project folder called MoonJump.
$ cd MoonJump
$ git init
Initialized empty Git repository in /Users/alex/MoonJump/.git/
git init creates that hidden .git folder mentioned in Section 2. That single folder is the repository — it holds the entire history, every commit that will ever be made here. Delete .git and you delete the project's whole history; the working files remain, but every past version is gone.
master instead of main. They mean exactly the same thing — it is just a name. This chapter uses main, matching GitHub's modern default. Rename yours with git branch -m main if you want to match.Create one plain text file to track — a design notes file is enough to learn every core command before Section 8 brings in an actual Unity project.
$ echo "Player can jump." > design.txt
$ git status
On branch main
No commits yet
Untracked files:
(use "git add <file>..." to include in what will be committed)
design.txt
nothing added to commit but untracked files present (use "git add" to track)
git status is the single most-used Git command — run it constantly, it never changes anything, it only reports. Right now it says design.txt is untracked: Git sees the file exists in the working directory, but nothing has told Git to pay attention to it yet.
$ git add design.txt
$ git status
On branch main
No commits yet
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: design.txt
git add is the "working directory to staging area" arrow from Section 2's diagram. design.txt now sits under Changes to be committed — it is staged, meaning it is queued up for the next commit, but no commit has actually happened yet. Nothing is permanent until you commit.
$ git commit -m "Add initial design notes"
[main (root-commit) a1b2c3d] Add initial design notes
1 file changed, 1 insertion(+)
create mode 100644 design.txt
The -m flag supplies the commit message inline. This is the "staging area to repository" arrow: Git takes exactly what was staged, wraps it up with your message, author name, and timestamp, links it to the previous commit as its parent (here, none exists yet, so Git labels it root-commit), and gives it the ID a1b2c3d.
"fixed stuff" or "wip" is exactly the same failure as final_v2_REAL_final.unity from Section 1, just moved one layer down. The whole point of a message is to answer "why" for a future reader — often you, in six months. Section 13 comes back to this with concrete habits.$ git log
commit a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0 (HEAD -> main)
Author: Alex <alex@moonjump.dev>
Date: Mon Feb 2 09:14:22 2026 +0700
Add initial design notes
git log walks the parent chain from Section 2's diagram, printing one entry per commit: its full ID, author, timestamp, and message. (HEAD -> main) means two things point at this commit right now: HEAD (the commit you currently have checked out) and the branch main. Section 4 makes both of those precise.
Make a second commit so git log has more than one entry to show. Edit design.txt to add a second line:
Player can jump.
Player can double jump.
$ git add design.txt
$ git commit -m "Note double jump idea"
[main 9f8e7d6] Note double jump idea
1 file changed, 1 insertion(+)
$ git log --oneline
9f8e7d6 (HEAD -> main) Note double jump idea
a1b2c3d Add initial design notes
git log --oneline is the compressed view you will reach for constantly: one line per commit, newest first, exactly matching the parent chain from Section 2 — 9f8e7d6's parent is a1b2c3d, printed directly underneath it.
Edit design.txt one more time, adding a third line, but this time do not commit yet:
Player can jump.
Player can double jump.
Player can wall-jump.
$ git diff
diff --git a/design.txt b/design.txt
index e69de29..1234567 100644
--- a/design.txt
+++ b/design.txt
@@ -1,2 +1,3 @@
Player can jump.
Player can double jump.
+Player can wall-jump.
git diff with no arguments compares the working directory against the staging area (or the last commit, if nothing is staged) and prints only the lines that changed. A leading + means a line was added; a leading - would mean a line was removed; a line with no prefix is unchanged context, shown so the change makes sense on its own. This is the tool you reach for before every commit, to double-check exactly what you are about to record.
git diff compares working directory against staging area. git diff --staged compares staging area against the last commit. Once you have run git add, plain git diff will show nothing until you change the file again — the change moved into the staging area, so check --staged instead.Right now every commit sits on one line, one after another. A branch is nothing more exotic than a movable, named pointer to one specific commit. main is just a branch like any other — there is nothing structurally special about it except convention. HEAD is a second pointer, tracking whichever branch (or, rarely, commit) you currently have checked out.
Creating a branch is instant and cheap precisely because of that last line: it does not copy any files, it only creates a new named pointer at the current commit. The point of a branch is to try an idea — a new mechanic, a risky refactor — without touching main at all until you are sure the idea works.
$ git branch feature/wall-jump
$ git switch feature/wall-jump
Switched to branch 'feature/wall-jump'
git branch <name> creates the pointer; git switch <name> moves HEAD onto it, which also updates every file in the working directory to match that branch's commit (here, no change yet, since both branches point at the same commit). Now commit on this branch:
$ git add design.txt
$ git commit -m "Add wall-jump idea"
[feature/wall-jump 3c2b1a0] Add wall-jump idea
1 file changed, 1 insertion(+)
$ git log --oneline --all
3c2b1a0 (HEAD -> feature/wall-jump) Add wall-jump idea
9f8e7d6 (main) Note double jump idea
a1b2c3d Add initial design notes
Now the branches genuinely differ: feature/wall-jump points at 3c2b1a0, while main is still one commit behind, at 9f8e7d6. Switch back to main and Git rewrites your working directory to match — the third line in design.txt actually disappears from what you see on disk, because main's commit never included it:
$ git switch main
Switched to branch 'main'
Merging is bringing a branch's commits into another branch. There are two shapes it can take, and Git picks automatically based on the history.
main has not moved since feature/wall-jump was created — every commit on the feature branch is simply ahead of main on the same straight line. In that case, merging is trivial: Git just slides main's pointer forward to match.
$ git merge feature/wall-jump
Updating 9f8e7d6..3c2b1a0
Fast-forward
design.txt | 1 +
1 file changed, 1 insertion(+)
Fast-forward only works while one branch is a straight-line ahead of the other. Once both branches gain new commits since they diverged, Git cannot just slide a pointer — it has to actually combine the two histories, so it creates a new merge commit with two parents instead of the usual one.
$ git switch -c feature/ui
Switched to a new branch 'feature/ui'
(-c creates and switches in one step.) Edit design.txt on this branch, changing the first line from Player can jump. to Player can jump higher with a running start., then commit:
$ git add design.txt
$ git commit -m "Adjust jump wording for UI copy"
[feature/ui 7d6c5b4] Adjust jump wording for UI copy
1 file changed, 1 insertion(+), 1 deletion(-)
Now switch back to main and make an unrelated edit to the same first line, changing it instead to Player can jump twice as high.:
$ git switch main
Switched to branch 'main'
$ git add design.txt
$ git commit -m "Buff jump height"
[main 5e4d3c2] Buff jump height
1 file changed, 1 insertion(+), 1 deletion(-)
main and feature/ui have now genuinely diverged — each has one commit the other lacks, both editing the exact same line of the same file, two different ways:
Section 6 is what happens when you try to merge these two.
A merge conflict happens when Git tries a three-way merge and finds that both sides changed the same part of the same file, in different ways. Git can automatically combine changes to different lines of a file without any trouble — that happens silently, all the time. It genuinely cannot decide, on its own, which of two conflicting edits to the same line is correct. So it stops, and asks you.
$ git merge feature/ui
Auto-merging design.txt
CONFLICT (content): Merge conflict in design.txt
Automatic merge failed; fix conflicts and then commit the result.
Open design.txt. Git has not picked a side — it has written both versions into the file, wrapped in conflict markers, so you can decide by hand:
<<<<<<< HEAD
Player can jump twice as high.
=======
Player can jump higher with a running start.
>>>>>>> feature/ui
Player can double jump.
Player can wall-jump.
Read the markers left to right, top to bottom: <<<<<<< HEAD starts the block that shows what your current branch (main, since that is where HEAD is) has; ======= is the divider; everything down to >>>>>>> feature/ui shows what the other branch has. Nothing below >>>>>>> feature/ui was in conflict, so Git left it untouched.
$ git status
On branch main
You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: design.txt
no changes added to commit (use "git add" and/or "git commit -a")
Resolving a conflict is a plain text edit: delete all three marker lines, and keep, combine, or rewrite the content in between however actually makes sense. Here, combining both ideas reads fine as one sentence:
Player can jump higher, and twice as high with a running start.
Player can double jump.
Player can wall-jump.
Once the file looks the way you want, staging it tells Git "this conflict is resolved," and committing finishes the merge — note there is no -m message needed here, since Git already prepared one describing the merge:
$ git add design.txt
$ git commit -m "Merge feature/ui: resolve jump-height wording conflict"
Merge made by the 'ort' strategy.
design.txt | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
$ git log --oneline
8b7a695 (HEAD -> main) Merge feature/ui: resolve jump-height wording conflict
7d6c5b4 (feature/ui) Adjust jump wording for UI copy
5e4d3c2 Buff jump height
3c2b1a0 (feature/wall-jump) Add wall-jump idea
9f8e7d6 Note double jump idea
a1b2c3d Add initial design notes
<<<<<<< HEAD is a mistake and not intentional text. This produces a build with literal marker garbage in a script or design file. Always re-read the whole file after resolving, not just the conflicted lines, before staging it.Everything so far happened on one machine. A remote is another copy of the repository, almost always sitting on a server (GitHub, GitLab, or a company's own Git server), that you and your teammates all synchronize with instead of emailing files to each other.
$ git remote add origin https://github.com/alex/MoonJump.git
$ git push -u origin main
Enumerating objects: 21, done.
Counting objects: 100% (21/21), done.
Writing objects: 100% (21/21), 2.14 KiB | 2.14 MiB/s, done.
To https://github.com/alex/MoonJump.git
* [new branch] main -> main
branch 'main' set up to track 'origin/main'.
origin is just a conventional nickname for "the remote I cloned from or set up first" — nothing forces that exact name, but almost every project uses it. git push uploads your local commits that the remote does not have yet; -u (short for --set-upstream) remembers the link between local main and origin/main, so future pushes and pulls on this branch need no extra arguments.
A teammate, Priya, gets the whole project with one command:
$ git clone https://github.com/alex/MoonJump.git
Cloning into 'MoonJump'...
remote: Enumerating objects: 21, done.
remote: Counting objects: 100% (21/21), done.
Receiving objects: 100% (21/21), 2.14 KiB | 1.07 MiB/s, done.
git clone downloads the entire repository — every commit in its full history, not just the latest snapshot — and automatically sets up origin pointing back at the server. This is the "distributed" property from Section 1 made concrete: Priya's machine now has a complete, independent copy of the whole project's history, usable offline.
When Priya finishes some work and wants your latest changes too:
$ git pull
remote: Enumerating objects: 6, done.
Unpacking objects: 100% (6/6), 812 bytes | 812.00 KiB/s, done.
From https://github.com/alex/MoonJump.git
3c2b1a0..8b7a695 main -> origin/main
Updating 3c2b1a0..8b7a695
Fast-forward
design.txt | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
git pull is really two steps run back to back: git fetch (download the remote's new commits, but do not touch your working directory yet) followed by git merge (combine them into your current branch, exactly as Sections 5 and 6 described — including the possibility of a conflict, if you and the remote both changed the same lines).
main ever merges them. The underlying merge, once approved, is exactly Section 5's mechanics.Open a real Unity project folder and most of it was never authored by a person. Library/ is Unity's imported-asset cache, rebuilt automatically from your source assets every time the editor opens the project. Temp/ and Obj/ hold scratch files from the current editor session and the C# compiler. Build/ or Builds/ holds the actual game executable once you build it. None of these belong in version control — they are generated from files that already are tracked, exactly the way a compiled .o file in the C chapters was never something you version-controlled, only the .c source it came from.
A .gitignore file, placed at the root of the project, tells Git which paths to never track, even if they exist in the working directory:
# Unity generated
Library/
Temp/
Obj/
Build/
Builds/
Logs/
UserSettings/
# IDE generated
.vs/
.idea/
*.csproj
*.sln
*.user
Each line is a pattern: a trailing / matches a whole folder and everything inside it; a leading * is a wildcard matching any filename ending that way. Save this as .gitignore and Git simply stops offering these paths in git status, and refuses to stage them even with an explicit git add Library/, unless you force it.
Library/ before adding a .gitignore. This folder is regenerated differently per machine and per Unity version, so every teammate's next pull drags in thousands of meaningless changed files, produces enormous, unreviewable diffs, and turns nearly every merge into a conflict — over files nobody actually wrote by hand. This is the single most common way a new team's Git repository becomes unusable within its first week.If Library/ is already committed, adding .gitignore alone does not fix it — Git keeps tracking files it already knows about. You have to explicitly untrack them, which leaves the files on disk but removes them from Git's bookkeeping:
$ git rm -r --cached Library/
rm 'Library/BuildPlayerData/Player/000000000000000000000000000000.info'
rm 'Library/ScriptAssemblies/Assembly-CSharp.dll'
... (thousands of similar lines)
$ git commit -m "Stop tracking Library/, now covered by .gitignore"
-r makes the removal recursive (the whole folder tree); --cached is the important part — it removes the files only from Git's tracking, leaving them untouched on your disk, since Unity still needs them to actually run the project.
Every merge and every conflict resolution so far worked because design.txt is text: a sequence of lines Git can compare and combine line by line, the way git diff and the conflict markers in Section 6 both relied on. A .png, a .fbx, a .wav file is binary: a sequence of bytes with no concept of "lines" at all. Git cannot show you a meaningful line-by-line diff of two textures, and it cannot combine two edits to the same texture the way it combined two edits to different lines of design.txt.
$ git diff
diff --git a/Assets/Models/Player.fbx b/Assets/Models/Player.fbx
index 4a1f2b3..8c9d0e1 100644
Binary files a/Assets/Models/Player.fbx and b/Assets/Models/Player.fbx differ
That is the entire diff Git can offer for a binary file: "they differ." Not which vertices moved, not which bone weights changed — just that the bytes are not identical. Now picture the scenario Section 6 walked through, but for this file instead of a text one: you adjust the character's rig on main while Priya adjusts its mesh topology on her own branch, both starting from the same version of Player.fbx.
$ git merge priya/mesh-cleanup
Auto-merging Assets/Models/Player.fbx
CONFLICT (content): Merge conflict in Assets/Models/Player.fbx
Automatic merge failed; fix conflicts and then commit the result.
Section 6 told you to open the conflicted file and edit it by hand. That is impossible here — there is no text editor that lets you meaningfully "combine" two versions of a 3D mesh's raw byte layout. Git can only offer you one whole file or the other:
Whichever side you pick, the other person's hours of work on that same file vanish from the merge, silently, unless they happen to still have it on their own branch and manually redo it on top of the winning version. This is not a bug in Git — it is a direct, unavoidable consequence of what "binary" means. The next three sections cover the three real tools studios use to keep this from happening in the first place.
Git LFS (Large File Storage) is an official Git extension that solves one specific piece of the binary-asset problem: repository size. Recall from Section 2 that every commit is a full snapshot. Commit a 50 MB texture, tweak it slightly, commit again — Git's history now holds two nearly-identical 50 MB blobs, forever, because history is never silently deleted. A game project with years of texture and audio iteration can bloat a plain Git repository into many gigabytes that every single clone has to download in full, per Section 7's git clone.
Git LFS's trick: instead of storing the actual binary content inside the Git repository, it stores a tiny text pointer file in its place, and keeps the real content on a separate LFS server, downloaded only when actually needed.
$ git lfs install
Updated git hooks.
Git LFS initialized.
$ git lfs track "*.psd" "*.fbx" "*.png" "*.wav"
Tracking "*.psd"
Tracking "*.fbx"
Tracking "*.png"
Tracking "*.wav"
git lfs track writes its patterns into a new file, .gitattributes, which you commit like any other file — it is what tells every clone of the repository which patterns to treat specially:
*.psd filter=lfs diff=lfs merge=lfs -text
*.fbx filter=lfs diff=lfs merge=lfs -text
*.png filter=lfs diff=lfs merge=lfs -text
*.wav filter=lfs diff=lfs merge=lfs -text
$ git add .gitattributes Assets/Models/Player.fbx
$ git commit -m "Track binary assets with Git LFS"
What actually lands in the Git repository's history for Player.fbx is not the model at all — it is a plain text pointer, only a few lines long:
version https://git-lfs.github.com/spec/v1
oid sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e239
size 2148031
This genuinely fixes repository bloat and slow clones. It does not fix Section 9's actual conflict problem. A pointer file is still plain text, so two people changing the same binary asset still produces a conflict — just now it is a conflict between two tiny pointer files instead of two multi-megabyte blobs. Git still cannot tell you which textures or meshes to keep; it can only tell you two different oid hashes exist for the same path. The actual concurrent-editing problem needs a different fix entirely, covered next.
Every fix so far has been reactive: let two people edit the same file, then figure out afterward how to combine their work. File locking takes the opposite approach entirely — prevent the conflict from ever happening, by reserving exclusive write access to one file to one person at a time. It trades a small amount of friction (you sometimes have to wait, or ask a teammate to release a file) for a guarantee that Section 9's "someone's binary edits silently vanish" scenario simply cannot occur.
$ git lfs lock Assets/Scenes/Main.unity
Locked Assets/Scenes/Main.unity
$ git lfs locks
Path ID Lock Owner
Assets/Scenes/Main.unity 142 Alex
If Priya now tries to push a change to that same file while the lock is held, the LFS server rejects the push outright, telling her exactly who holds the lock. Once you finish your edit and commit it, you release the lock so someone else can take it:
$ git lfs unlock Assets/Scenes/Main.unity
Unlocked Assets/Scenes/Main.unity
Git LFS locking works, but it is a bolt-on feature added to a tool that was fundamentally designed around "everyone has a full independent copy and we merge later." Perforce (officially Helix Core) takes the opposite design as its starting assumption, which is exactly why most mid-size-and-larger studios run it for their art and design content, often alongside Git for programmer-only source code.
git clone works), a Perforce depot can hold terabytes of historical texture and audio data without every artist's machine needing local disk space for all of it.Section 9 lumped every binary asset together, but Unity scenes, prefabs, materials, and other .asset files are actually a special case. By default (under Edit > Project Settings > Editor > Asset Serialization > Mode, set to Force Text), Unity writes these as plain YAML — a human-readable text format — not as opaque binary. That means, unlike a .fbx or a .png, Git technically can diff and merge a .unity scene file line by line. Here is a small slice of what a single GameObject looks like inside one:
--- !u!1 &1234567890123456
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1234567890123457}
m_Layer: 0
m_Name: Player
m_TagString: Untagged
m_IsActive: 1
Notice &1234567890123456 right after the object type tag — that is a YAML anchor, and it is doing the same job as a pointer address from the C chapters. Every GameObject and every component in the scene gets one of these numeric fileID anchors, and every reference between objects — "this Transform's parent is that Transform," "this component lives on that GameObject" — is stored as a raw fileID number pointing at one of them. The scene file is really a graph of objects wearing a text costume.
That graph structure is exactly why a plain line-based merge, the kind that worked perfectly for design.txt in Section 6, is genuinely dangerous here. Git's default text merge has no idea that fileID: 1234567890123457 on one line needs to stay consistent with a matching object defined somewhere else entirely in the file. Two people restructuring the same part of a scene's hierarchy can produce a merge that Git reports as fully successful — no conflict markers, clean commit — while the resulting file is a corrupted graph: a dangling reference to a fileID that no longer exists, or two objects that were meant to be the same one now duplicated. Unity may refuse to open a scene like that, or worse, open it with objects silently missing.
Unity ships a dedicated tool for exactly this problem: UnityYAMLMerge, commonly called Smart Merge. Instead of merging line by line, it parses both versions of the file back into the object graph they represent, merges at the level of "this GameObject," "this component's fields," and re-serializes a consistent result. You wire it in as Git's merge tool for these file types, typically inside .gitconfig:
[merge]
tool = unityyamlmerge
[mergetool "unityyamlmerge"]
trustExitCode = false
cmd = '/Applications/Unity/Unity.app/Contents/Tools/UnityYAMLMerge' merge -p "$BASE" "$REMOTE" "$LOCAL" "$MERGED"
Smart Merge meaningfully raises the odds of a clean, correct scene merge, and it is worth enabling on every Unity project under Git. It is not a complete substitute for Section 11's locking, though: when both people restructure the exact same GameObjects — not just different ones in the same file — Smart Merge runs out of safe options and falls back to asking a human, the same way a text conflict did in Section 6, except now you are hand-editing a graph of numeric IDs instead of a sentence. That is precisely why studios commonly lock a shared main scene or a heavily-touched prefab even when Smart Merge is enabled — the tool reduces how often you need a lock, it does not remove the need for one.
Everything in this chapter comes together into a small set of habits that matter far more day to day than any single command.
git diff already shows what changed; a message like "Cap wall-jump velocity to prevent clipping through the ceiling collider" tells a future reader something the diff alone never could.main is exactly how you end up doing Section 6's conflict resolution unnecessarily often..gitignore before your very first commit. Fixing it later means Section 8's git rm -r --cached cleanup, which is avoidable entirely by getting it right on day one.git push is rejected if the remote has commits you do not have locally — that rejection is a safety check. git push --force overrides it, rewriting the remote's history to match yours, which silently deletes any commits a teammate already pushed that you never pulled. On a personal branch nobody else uses, force-pushing is sometimes fine. On main, or any branch someone else is actively working from, it can erase real work with no warning to the person who loses it..git folder.git add.<<<<<<< / ======= / >>>>>>> lines Git inserts to show both conflicting versions.Library/.git push --force; overwrites a remote's history to match your local one, potentially deleting commits others already pushed.notes.txt containing the single line Enemy has 100 HP., you run these commands in order: echo "Enemy has 120 HP." > notes.txt (overwriting the file), then git add notes.txt, then you edit the file again by hand so it now reads Enemy has 150 HP., and you do not run git add a second time. Predict exactly what git status will report, and predict what plain git diff (no arguments) will show versus what git diff --staged will show. Explain why the two diffs are different.git status reports the file in two sections at once: under "Changes to be committed" (because the 100 → 120 edit was staged) and also under "Changes not staged for commit" (because the 120 → 150 edit happened after staging and was never added). This is exactly what the three-area model from Section 2 predicts: staging a change copies it into the index at that moment, and a later edit to the working directory does not retroactively update what is already sitting in the index.
$ git status
On branch main
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: notes.txt
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: notes.txt
git diff (working directory vs. staging area) shows only the second edit, the one git add never saw:
$ git diff
diff --git a/notes.txt b/notes.txt
--- a/notes.txt
+++ b/notes.txt
@@ -1 +1 @@
-Enemy has 120 HP.
+Enemy has 150 HP.
git diff --staged (staging area vs. last commit) shows only the first edit, the one that was staged:
$ git diff --staged
diff --git a/notes.txt b/notes.txt
--- a/notes.txt
+++ b/notes.txt
@@ -1 +1 @@
-Enemy has 100 HP.
+Enemy has 120 HP.
Neither command alone shows the full 100 → 150 change, because each one compares a different pair of the three areas from Section 2's diagram — and committing right now, without staging again, would record the file as 120 HP, not 150, since a commit only ever takes what is currently staged.
credits.txt. Merging produces this file:
Lead Programmer: Alex
<<<<<<< HEAD
Lead Artist: Priya
=======
Art Director: Priya
>>>>>>> feature/credits-update
Sound Designer: Jae
Write the final, resolved content of credits.txt (deciding sensibly between the two titles, or combining them), and write the exact two commands needed after your edit to finish the merge.One reasonable resolution keeps the title that seems more accurate for a director-level credit, removing all three marker lines entirely:
Lead Programmer: Alex
Art Director: Priya
Sound Designer: Jae
Any resolution is acceptable as long as every <<<<<<<, =======, and >>>>>>> line is gone — leaving one in place would commit literal marker text as if it were a real credit, exactly the mistake Section 6's warning box calls out. Finishing the merge needs exactly the two commands from Section 6:
$ git add credits.txt
$ git commit -m "Merge feature/credits-update: settle on Art Director title"
No -m message is strictly required on the commit — Git pre-fills one describing the merge — but supplying your own that mentions how the conflict was resolved is more useful to a future reader than the generic default.
git init, immediately ran git add . and git commit -m "Initial commit", and only afterward created a .gitignore containing just Temp/ and Build/. A week later, every single git pull anyone runs produces dozens of merge conflicts inside a folder called Library/, even though nobody on the team has ever intentionally opened or edited a file in that folder. Explain exactly what went wrong, in terms of Section 8's rules, and give the sequence of commands that fixes it going forward.Two separate mistakes stacked on top of each other. First, Library/ was never added to the .gitignore at all — only Temp/ and Build/ were listed, so Git was never told to ignore Unity's imported-asset cache. Second, and worse, .gitignore was created after the first commit, and that first commit already used git add ., which stages everything in the working directory with no exceptions — so Library/ got committed and is now permanently part of the tracked history, regardless of what any later .gitignore says. A .gitignore only stops Git from tracking new, currently-untracked paths; it has no effect on files Git is already tracking. That is exactly why every teammate's regenerated, machine-specific Library/ contents keep showing up as "changed" and colliding on every pull, precisely the failure mode Section 8's warning box describes.
# Step 1: fix the .gitignore itself
Library/
Temp/
Obj/
Build/
Builds/
Logs/
UserSettings/
# Step 2: stop tracking Library/ without deleting it from disk
$ git rm -r --cached Library/
$ git add .gitignore
$ git commit -m "Stop tracking Library/, cover it properly in .gitignore"
# Step 3: everyone pulls this commit once, and their local Library/
# goes back to being untracked, generated content -- no more conflicts
$ git pull
The key lesson: a .gitignore must exist, and must be correct, before the first commit that would otherwise sweep generated files in — or if that ship has sailed, git rm -r --cached is the explicit "forget this, even though you already know about it" command that a plain .gitignore edit can never substitute for.