In a world with , chapter 1.3.

THE PATH · SESSION 3 OF 66 · UNIT 1 · ACT I

Save Points and Respawns

Git Init, Commit, Branch, Merge

60 min read5 diagrams3 side quests9 terminals4,721 words11 code blocks

At a glance

PACE
Session 3 of 66 · suggested week 1. Continue when you are ready.
UNIT
Unit 1 · I Woke Up in a Terminal and All I Had Was a Blinking Cursor
ACT
Act I · Foundation
BEFORE
1.2 Your Status Screen
AFTER
1.4 Guild Sync
ARTIFACT
Three repositories on your machine: explorers-journal turned into a Git repo, branch-quest with a conflict you created and resolved yourself, and quest-log with two branches merged into main and a history you can read with git log --oneline --graph
READ
60 min · 5 diagrams · 11 code blocks

The hook#

Here is the nightmare. You spend three hours on something. It works. You feel incredible. Then you make one small change and everything breaks, and when you try to undo it you cannot remember what you changed, so you undo a bit more, and now it is worse. The work is gone. Rage quit.

In a game that scenario does not exist. Why? Save points. You save before the boss fight. You die, you respawn at the save, you try again with what you learned. Nobody loses three hours in a game with a save system, and nobody should lose three hours of code.

The adventurer had learned this the hard way. One wrong turn in the catacombs, one trap, and a whole evening of mapping vanished with him. The second time he found a save crystal he pressed his palm to it before every door. He died just as often after that. It stopped mattering.

Git is save points for your code. It is the most important tool in a developer's kit after the editor itself, and every company, every open-source project and every working developer on the planet uses it, every day, on everything. It is also, for most beginners, the first tool that feels genuinely alien: a vocabulary of commits and branches and heads, output that seems to shout at you, a conflict that looks like the file has been vandalised. By the end of this session all of it will be ordinary. You will have made a repository from nothing, saved into it a dozen times, split off a parallel timeline, merged it back, broken the merge on purpose and repaired it with your own hands.

We are covering a lot: initialising a repo, staging, committing, the log, ignoring files, branching, merging, and resolving a conflict. Every one of those maps onto something you already understand. A commit is a save. A branch is an alternate timeline. A merge is combining timelines. A conflict is the game asking which version you want to keep. Hold on to those four sentences and the rest is spelling.

One more thing before we start, because it is the most common confusion of the whole unit. Git is not GitHub. Git is a program on your machine; it works with no internet, no account, no website. GitHub is a website that hosts Git repositories so other people can see them. Today is Git only. Your work stays on your machine. Next session, the Guild Sync, we go online.

The idea#

The problem Git solves has a name you have already seen: final_project_v2_FINAL_actually_final_v3.docx. That is version control done by hand, and it fails in exactly the ways you expect. Which version is the one that worked? What changed between v2 and FINAL? Who changed it? Git answers all of those by tracking every change to every file over time, remembering who made it and why, and letting you go back to any point. Teams use it so five people can work on the same project without overwriting each other; solo developers use it because the five people are you on Monday, you on Tuesday and you at two in the morning.

A few words of vocabulary, and then the two pictures that make them real. A repository, repo for short, is a project folder that Git is watching. A commit is a snapshot, a save point: a record of what every file looked like at one moment, with your name, the time and a message. The staging area is a waiting room where you choose which changes go into the next save. A branch is an alternate timeline, a way to work on something without touching the main line. A merge joins two timelines back into one.

Every file in a repository is in one of three states. Modified: you changed it and Git has not been told. Staged: you told Git to include it in the next save. Committed: saved, locked in, safe. Two commands move a file between the rooms. git add takes it from modified to staged, into the waiting room. git commit takes everything in the waiting room and makes a save point of it. Nothing is saved until it has gone through both doors, and git status tells you at any moment which room every file is standing in.

Three cards side by side: working directory (the field) holding inventory.txt modified, secrets.txt untracked and README.md unchanged; staging area (the waiting room) holding inventory.txt staged; repository (.git) holding three commits; between them the two doors git add and git commit -m with the undo commands underneath, and under each card the lines git status prints for that state
Fig 1 · Three states, two doors. A file is modified in your folder, staged in the waiting room, or committed in .git; git add and git commit are the only ways through, and git status tells you which room every file is in.

Why a waiting room at all? Because a save should mean something. If you fixed a bug and also half-finished a new feature, you want two saves, not one with a vague message. The staging area lets you pick the bug fix, commit it with a message that says so, and leave the rest for the next commit. Early on you will mostly git add . and stage everything, and that is fine. The room is there for when you need it.

This also kills the second-biggest misconception: Git is not automatic. It does not save when you press Ctrl+S. Nothing is tracked until you add it and nothing is saved until you commit it. The upside is that every save is deliberate and has a message; the downside is that you have to remember to save. Save often. Small saves, each one doing one thing, with a message that says what and why.

What a commit actually is. Not a diff, not a list of changes: a whole snapshot of every tracked file, plus a little header. The header holds your name and email, the date, your message, and the hash of the commit that came before it, its parent. The name of the commit is a forty-character hexadecimal hash, like c0769461e5a3…, computed from everything inside it. Change one byte of one file and the hash changes, which is why a save point cannot be quietly tampered with. Git shows you the first seven characters most of the time, c076946, and that is enough to point at it.

Left, one commit opened: its forty-character hash, its parent df4829e, author, date, message and the snapshot of every tracked file; right, the four-commit chain as git log --oneline shows it, newest first, each commit pointing down to its parent, with HEAD pointing to main pointing to the newest commit
Fig 2 · A save is a snapshot plus a parent. Every commit holds a whole picture of the folder and the hash of the commit before it; branches are labels on commits, HEAD is the label you are standing on.

Because every commit holds its parent's hash, commits form a chain, and the chain runs backwards: the newest commit knows the one before it, which knows the one before that, back to the very first, which has no parent and is marked (root-commit). git log walks that chain and prints it newest first. A branch is nothing more than a label stuck on one commit, and HEAD is the label that says where you are standing. When you commit, the new save's parent is whatever HEAD pointed at, and the label you are on moves forward to the new save. That is all a branch is. Keep that picture; it makes everything in the second half of today obvious.

Branches, then, are timelines. You create one when you want to try something without risking the main line: a new feature, an experiment that might not work, a fix you are not sure about. The default branch is called main. git branch lists your branches with a star on the one you are on. git branch name creates one. git checkout name moves to it, and git checkout -b name creates and moves in one step. Newer Git also has git switch name and git switch -c name, which do the same jobs with a clearer name; both work, most tutorials still say checkout, use whichever you like. When you move to a branch Git rewrites your folder to match that branch's latest save, which is the part that surprises people the first time: files appear and disappear. Nothing is deleted. The folder is showing you a different timeline.

TEXT
main:     A --- B --- C                       \feature:                D --- E

Commits A, B and C are on main. You branched at C, made D and E on feature, and main has not moved. Merging brings a timeline back. If main has not moved since the fork, Git simply slides the main label forward to E; that is a fast-forward, no new commit needed. If both timelines moved, Git makes a merge commit with two parents that joins them. And if both timelines changed the same lines of the same file, Git stops, writes both versions into the file with markers around them, and asks you to choose. That is a merge conflict. It is not an error and nothing went wrong; it is the one decision a program cannot make for you.

Two more misconceptions to retire now. Deleting a branch after you merge it does not delete the commits: they are part of main's history, and the branch was only a label. And you do not need to commit everything at once; you should not. Small, focused saves with clear messages are what make the log worth reading.

Set up your identity, once. Every commit carries a name and an email, so before your first save Git needs to know yours. You also tell it to call the first branch main instead of the older default master, so your repositories look like everyone else's. Three commands, run once per machine, kept in a file called ~/.gitconfig that you can cat like any other:

SH
git config --global user.name "Your Name"git config --global user.email "youremail@example.com"git config --global init.defaultBranch main

The demonstration#

Six terminals. The git in them is the real thing in miniature: it keeps a repository inside the box, speaks with Git 2.47's own sentences, and resets when you press Run again. Each box starts from a clean home, so the later ones rebuild the previous save in a line or two before continuing. Run each, read every line of output, then redo the parts that surprised you at the prompt.

Walkthrough 0: who are you. Identity first, because the first commit will refuse without it. --global means "for every repository on this machine"; the settings land in ~/.gitconfig.

SHyou type
git --versiongit config --global user.name "Arthur the Adventurer"git config --global user.email "arthur@newworld.dev"git config --global init.defaultBranch maingit config --global --listcat ~/.gitconfig
Shell simulatoradventurer@newworld · simulated bashnot run yet

Browser simulation with temporary files, not a real Bash process. Tool versions and app launches are examples. Rerunning or reloading resets this box.

adventurer@newworld:~$

Walkthrough 1: your first repo. Make a folder, turn it into a repository, and watch two files travel through the three states one door at a time. Read every git status out loud; by the end of the box you should be able to predict each one before it prints.

SHyou type
# identity first (one line here; the identity terminal above shows the three commands)git config --global user.name "Arthur the Adventurer" && git config --global user.email "arthur@newworld.dev" && git config --global init.defaultBranch maincd ~mkdir git-questcd git-questgit initls -lagit statusecho "# Git Quest" > README.mdecho "My first Git project. I am learning version control." >> README.mdecho "wooden-sword" > inventory.txtecho "cloth-shirt" >> inventory.txtecho "health-potion x3" >> inventory.txtgit statusgit add README.mdgit statusgit add inventory.txtgit statusgit commit -m "Initial commit: add README and inventory"git statusgit log
Shell simulatoradventurer@newworld · simulated bashnot run yet

Browser simulation with temporary files, not a real Bash process. Tool versions and app launches are examples. Rerunning or reloading resets this box.

adventurer@newworld:~$

Four things to take from that box. git init made exactly one thing, a hidden .git folder, and that folder is the repository: every save you ever make lives inside it, and deleting it turns the project back into a plain folder. The first git status after you create files says Untracked, meaning Git can see them but has never been asked to care. After git add README.md the status splits into two sections, Changes to be committed for the file in the waiting room and Untracked for the one still outside; that is the three-states picture in text. And after the commit, git status says nothing to commit, working tree clean, which is the sentence you want to see before you stop work for the night. git log shows the save: the full hash, (HEAD -> main) saying this is where you stand, your name, the date, and the message indented underneath.

Walkthrough 2: more saves, and the diff. Now change a file that is already tracked. Its state is modified, not untracked, and before you stage it you can ask Git exactly what changed. git diff compares your folder with the waiting room; git diff --staged compares the waiting room with the last save. After git add, the first goes quiet and the second lights up, because the change moved rooms.

SHyou type
# rebuild the save from the last terminal (each terminal on this page starts fresh)git config --global user.name "Arthur the Adventurer" && git config --global user.email "arthur@newworld.dev" && git config --global init.defaultBranch mainmkdir ~/git-quest && cd ~/git-quest && git initprintf "# Git Quest\nMy first Git project. I am learning version control.\n" > README.mdprintf "wooden-sword\ncloth-shirt\nhealth-potion x3\n" > inventory.txtgit add . && git commit -m "Initial commit: add README and inventory"# now: change a tracked file and watch the three statesecho "iron-sword" >> inventory.txtecho "leather-armor" >> inventory.txtgit statusgit diffgit add inventory.txtgit diffgit diff --stagedgit commit -m "Add iron sword and leather armor to inventory"echo "Quest 1: Learn Git" > quests.txtecho "Status: In Progress" >> quests.txtgit add quests.txtgit commit -m "Add quest tracker"git log --onelinegit log
Shell simulatoradventurer@newworld · simulated bashnot run yet

Browser simulation with temporary files, not a real Bash process. Tool versions and app launches are examples. Rerunning or reloading resets this box.

adventurer@newworld:~$

Read the diff slowly once. --- a/inventory.txt and +++ b/inventory.txt are the before and after. @@ -1,3 +1,5 @@ says the old version had lines one to three here and the new has one to five. Lines starting with a space are unchanged context; lines starting with + were added; a - would mean removed. Every code review you ever do will be in this format. Then look at the two logs: --oneline gives one line per save, short hash and message, newest at the top, and is the form you will type fifty times a week. The full form adds author and date. Three commits, three save points, each one a whole snapshot you can go back to.

A word on messages, because yours will be read by other people and by future you. Say what the save does, in the present tense, as a short imperative: Add quest tracker, Fix login redirect, Remove unused styles. Not stuff, not changes, not asdf. If you find yourself writing and in a message, that may be two commits. There is a convention for this called Conventional Commits that teams adopt later; for now, clear and specific is the whole rule.

Walkthrough 3: things that must never be saved. Some files do not belong in a repository: secrets, passwords and API keys, because a repository gets shared; node_modules and other folders of downloaded libraries, because they are huge and can be re-downloaded; logs and scratch output, because they are noise. A file called .gitignore in the root of the repo lists patterns Git should pretend not to see. One name per line: an exact filename, a folder name with a trailing slash, or a wildcard like *.log.

SHyou type
git config --global user.name "Arthur the Adventurer" && git config --global user.email "arthur@newworld.dev" && git config --global init.defaultBranch mainmkdir ~/git-quest && cd ~/git-quest && git initprintf "wooden-sword\ncloth-shirt\n" > inventory.txt && echo "Quest 1: Learn Git" > quests.txtgit add . && git commit -m "Add quest tracker"# files that must never be savedecho "my-secret-password" > secrets.txtmkdir node_modulestouch node_modules/big-library.jsecho "debug output" > debug.loggit statusecho "secrets.txt" > .gitignoreecho "node_modules/" >> .gitignoreecho "*.log" >> .gitignorecat .gitignoregit statusgit add .gitignoregit commit -m "Add .gitignore to exclude secrets, node_modules, and logs"git log --onelinegit add secrets.txtls
Shell simulatoradventurer@newworld · simulated bashnot run yet

Browser simulation with temporary files, not a real Bash process. Tool versions and app launches are examples. Rerunning or reloading resets this box.

adventurer@newworld:~$

Before the .gitignore exists, git status lists all three intruders as untracked, with the folder shown as node_modules/ rather than every file inside it. After, they are gone from the list and only .gitignore itself shows up, and yes, you commit that: the ignore rules are part of the project, so everyone who gets the repo gets them. The last two lines are the proof. git add secrets.txt refuses and says the file is ignored (git check-ignore -v secrets.txt names the rule responsible), and ls shows the file is still right there on disk. Ignored is not deleted. Git is simply never going to put it in a save.

Walkthrough 4: the side quest, a branch and a fast-forward. A new timeline, a save on it, a look back at main to see that nothing there moved, and then the merge. Watch the folder change when you check out.

Top, two timelines leaving the same first commit d1a2a91: main with a dashed next save, side-quest with commit f7b168e and HEAD on it; below, two terminal views of the same folder, one after git checkout side-quest showing side-quest.txt and magic-staff in inventory.txt, one after git checkout main where both are absent
Fig 3 · One folder, two timelines. checkout rewrites the folder to match the branch you move to; nothing is deleted, the folder is showing a different save.

SHyou type
git config --global user.name "Arthur the Adventurer" && git config --global user.email "arthur@newworld.dev" && git config --global init.defaultBranch mainmkdir ~/git-quest && cd ~/git-quest && git initecho "wooden-sword" > inventory.txt && printf "Quest 1: Learn Git\nStatus: In Progress\n" > quests.txtgit add . && git commit -m "Initial commit: add inventory and quest tracker"git branchgit checkout -b side-questgit branchecho "Side Quest: Defeat the Goblin King" > side-quest.txtecho "Reward: 500 gold" >> side-quest.txtecho "magic-staff" >> inventory.txtgit add .git commit -m "Add side quest and magic staff"git log --onelinegit checkout mainlscat inventory.txtgit log --onelinegit merge side-questlscat inventory.txtgit log --onelinegit branch -d side-questgit branch
Shell simulatoradventurer@newworld · simulated bashnot run yet

Browser simulation with temporary files, not a real Bash process. Tool versions and app launches are examples. Rerunning or reloading resets this box.

adventurer@newworld:~$

Follow the labels. After checkout -b side-quest the star moves and you are on the new timeline. The commit lands there: git log --oneline shows it at the top with (HEAD -> side-quest) and the first commit below with (main), so you can see that main is one save behind. Then git checkout main, and the folder changes: ls has no side-quest.txt, cat inventory.txt has no staff. Nothing was deleted. The folder is displaying main's latest save, and the side quest's save is safe in .git. git merge side-quest brings it in, and because main had not moved since the fork, Git prints Fast-forward: it moved the main label up to the side quest's commit and no new commit was needed. The files are back. Both labels now sit on the same save, (HEAD -> main, side-quest), so the branch has done its job and git branch -d removes the label, nothing else.

Left, a fast-forward: main sits behind side-quest on a straight line and after the merge the main label has simply moved up to the same commit, with the Updating and Fast-forward lines git prints; right, a three-way merge: main and dragon-slayer both moved since the fork, so the merge creates a new commit with two parents, with the Merge made by the ort strategy line and the CONFLICT line that appears when both sides changed the same lines
Fig 4 · Two shapes of merge. When main never moved, git fast-forwards the label; when both sides moved, git makes a merge commit with two parents, and asks you only if the same lines changed on both.

Walkthrough 5: the conflict. Now both timelines will change the same two lines of the same file, so Git cannot fast-forward and cannot combine them on its own. It will stop and ask. In the box, you answer by writing the file you actually want with echo; on your machine you would open quests.txt in VS Code, delete the marker lines and keep what you want, then save. The three Git moves after that are identical.

Left, quests.txt after a conflict: the <<<<<<< HEAD marker, two lines from your branch, the ======= fence, two lines from dragon-slayer, the >>>>>>> dragon-slayer marker, each region labelled; right, the three moves that end a conflict: edit until only the file you want remains, git add the file, git commit, plus git merge --abort as the escape hatch
Fig 5 · Reading a conflict. Git wrote both versions into the file with fences around them; you edit until only the version you want is left, then add and commit.

SHyou type
git config --global user.name "Arthur the Adventurer" && git config --global user.email "arthur@newworld.dev" && git config --global init.defaultBranch mainmkdir ~/git-quest && cd ~/git-quest && git initprintf "wooden-sword\nmagic-staff\n" > inventory.txt && printf "Quest 1: Learn Git\nStatus: In Progress\n" > quests.txtgit add . && git commit -m "Add side quest and magic staff"git checkout -b dragon-slayerecho "Quest 1: Slay the Dragon" > quests.txtecho "Status: Ready" >> quests.txtgit add quests.txtgit commit -m "Update quest to dragon slaying"git checkout mainecho "Quest 1: Learn Git (COMPLETE)" > quests.txtecho "Status: Done!" >> quests.txtgit add quests.txtgit commit -m "Mark git quest as complete"git merge dragon-slayergit statuscat quests.txt# resolve: write the file you actually want (in VS Code you would delete the markers by hand)echo "Quest 1: Learn Git (COMPLETE)" > quests.txtecho "Quest 2: Slay the Dragon" >> quests.txtecho "Status: In Progress" >> quests.txtcat quests.txtgit add quests.txtgit statusgit commit -m "Merge dragon-slayer: resolve quest conflict, add dragon quest"git log --oneline --graphgit branch -d dragon-slayer
Shell simulatoradventurer@newworld · simulated bashnot run yet

Browser simulation with temporary files, not a real Bash process. Tool versions and app launches are examples. Rerunning or reloading resets this box.

adventurer@newworld:~$

Read the conflict with the picture. git merge printed three lines: Auto-merging quests.txt, meaning it tried; CONFLICT (content): Merge conflict in quests.txt, naming the file; and Automatic merge failed; fix conflicts and then commit the result., telling you exactly what to do next. git status lists the file under Unmerged paths as both modified and repeats the instructions. Then cat quests.txt: everything between <<<<<<< HEAD and ======= is yours, the branch you are standing on, main; everything between ======= and >>>>>>> dragon-slayer is theirs, the branch coming in. Git kept both and drew fences so you could decide.

You decided to keep the completed first quest, add the dragon as a second, and set a fresh status. Once the file holds only that, with no markers left anywhere, git add quests.txt tells Git the fight is over, git status confirms All conflicts fixed but you are still merging, and git commit makes the merge commit, the save with two parents. git log --oneline --graph draws the result: two lanes that split at the shared save and join again at the top. If you ever want out instead, git merge --abort puts everything back exactly as it was before you typed git merge. Nothing is lost either way, which is the whole point of the save system.

Merge conflicts look scary the first time. They are the computer saying "two people changed the same thing, which do you want?" You pick, you save, you move on. After today you have done it once; by the end of the programme you will have done it fifty times and stopped noticing.

Where it breaks#

Your turn#

The quest#

By the start of Session 1.4, have on your machine:

  • explorers-journal turned into a repository with one commit, your name in git log.
  • branch-quest with at least four commits, a merge conflict you created and resolved, skills.txt on main, and only main left in git branch.
  • quest-log built to the requirements: two branches merged into main, both deleted, a final README commit, and a git log --oneline --graph you can read aloud and explain, including why one merge fast-forwarded and the other made a merge commit.

Budget ninety minutes to two hours for the Quest Log if you did not finish it here. Next session you push it to GitHub, so it must exist, in your home directory, under that exact name.

Side quests#

Three pages in the library look further down roads you walked today. They are written for later units, so read the opening and the first picture, not the code.

Side quests from this session

  • LinkedListA commit holds the hash of its parent and nothing else points forward: `git log` is walking a linked list from the newest node back to the root. Read the opening and the first picture; the `next` pointer there is Fig 2's parent arrow with the direction reversed, and in Unit 9 you will build the structure yourself.
    Data Structures
  • MementoMemento is the save-point pattern written as code: capture an object's whole state so it can be restored later without the outside world knowing what is inside. A commit is a memento of a folder, `.git` is the caretaker, and `git checkout` is the restore. Read the intent and the first diagram; the Java-style class names can wait.
    Design Patterns
  • GraphsOnce a merge commit has two parents the history is no longer a line, it is a graph, which is why the command is called `git log --graph` and why Git can find the shared ancestor of two branches before it merges them. Read what a node and an edge are and look at the first picture; the traversal code is for Unit 9.
    Data Structures

Before next session#

Make the save a reflex. Every time you finish a piece of work, however small: git status, git add ., git commit -m with a message that says what you did. Then git log --oneline and look at the stack growing. By Session 1.4 you should have a dozen commits across your three repositories and the habit should feel slightly compulsive. That is correct.

Spend twenty minutes on Learn Git Branching, the first four levels of the introduction sequence. It draws the commit graph live as you type, and watching main and HEAD move will fix the picture from Fig 2 permanently. If you prefer a game, Oh My Git! covers the same ground as cards.

Next time: Guild Sync. Your repositories leave your machine. You will make a GitHub account, prove who you are with an SSH key, push quest-log to a remote, clone it back down, open your first pull request, and build the project every developer eventually builds: a dotfiles repository that can set up a new machine in one command. The save system goes online, and your code becomes something other people can see.

You now have a power most people in the world do not. You can track every change to every file, go back in time, try things in parallel and combine the best results. That is Git. You learned it in one session, and the save crystal is yours for the rest of the game.

See also