Skip to content

Metadata Card

  • Prerequisites: Ch1 (File System), Ch2 (Basic Terminal Commands)
  • Estimated time: 40 min
  • Core difficulty: (Advanced)
  • Reading mode: High focus
  • Completion marker: Can independently manage personal projects with Git, create branches, resolve conflicts, and submit PRs

Your Progress

You're still in the workshop before setting out. You've gotten the hang of basic terminal commands, but you quickly discover a big problem — once code is broken, you can't get it back.

The workshop walls are covered with tools: you've learned the terminal, you've explored the file system. But you'll soon find a problem — you changed a bunch of code, realized halfway through that you were going in the wrong direction, and want to go back to the state from an hour ago — but it's too late. Files have been overwritten several times, previous versions vanished as if they never existed.

The name of this problem is "no undo button."

Your Task

Your first project already has a dozen files. You changed a function but you're not sure if it's right. You want to keep the current version AND keep the version before the change. You even want someone else to be able to edit these files, and then merge both people's work together. You need something — a tool that can fork time and merge it back together. You need a time machine.

Git is that time machine.

Chapter Layers

  • Required reading: git init, git status, git add, git commit, git log, git branch, git switch, git merge, git pull, git push, and handling a simple merge conflict
  • Optional reading: git rebase to clean up commit history, git stash to stash work, remote collaboration and Pull Requests
  • Advanced: Git Flow workflow model, git rebase --onto advanced history rewriting, Git's underlying object model

This chapter will NOT require you to understand

  • Full git rebase usage — only needed for advanced team collaboration
  • Strict Git Flow model — overly complex for personal or small-team development
  • git bisect binary search for bugs — come back when your project is big enough

The Breakthrough · Tracing the Origins

First Battle: Changed It Wrong, Want to Go Back

You've been scribbling on the workshop blueprints all afternoon. Now you stare at the design on the table, a bit panicked — because you knocked over the ink bottle, soaking the entire critical diagram.

"Wait, I..." You grab a cloth to wipe it, but it only makes things worse — the ink smears, the lines are all blurred. The drawing is ruined. What happens if you add one more stroke? It'll only be worse.

You start to realize a cruel truth: once a blueprint is destroyed, the previous version is lost forever. There's no correction fluid that can cross the line of "already ruined."

"If only I could go back to before I spilled that tea..."

You walk to the workshop's material shelf and rummage through your project cabinet. You carved some patterns into a copper plate, put it in the forge — and it cracked during quenching. You try to patch it — this time it's worse; the whole piece breaks in two. You think: "If only I could go back to before I poured the tea..."

At this moment, you notice a command in the terminal you haven't used:

bash
# First see if the workshop has a time machine
cd ~/project/my-first-project
git status

Language: Bash How to run: Execute in your project directory in the terminal Expected output: fatal: not a git repository (or any of the parent directories)

Sure enough, it doesn't. You need to install one first.


First Initialization

bash
# Install a time machine in your project
cd ~/project/my-first-project
git init

Language: Bash How to run: Execute in the terminal Expected output: Initialized empty Git repository in /home/steven/project/my-first-project/.git/

See that Initialized? Git has created a hidden folder .git in your project root directory. The time machine is installed now — it just has no saves yet.


First Save

You've finished editing main.py and think this version is worth keeping:

bash
# See which files have changed
git status

Language: Bash How to run: Execute in the project directory Expected output:

On branch master

No commits yet

Untracked files:
 (use "git add <file>..." to include in what will be committed)
 main.py
 README.md

Git says: you have files that aren't being tracked yet. In your project, these files are like materials scattered on the workshop floor — Git knows they exist, but hasn't picked them up and put them in the storeroom.

You need to first pick them up and put them on a "preparation table":

bash
# Put files into the staging area (preparation table)
git add main.py README.md

# Or use the shortcut: add all changes
git add .

Language: Bash How to run: Execute, then run git status again Expected output:

On branch master

No commits yet

Changes to be committed:
 (use "git rm --cached <file>..." to unstage)
 new file: main.py
 new file: README.md

Now they're in the "staging area" — this is where you confirm "I really do want to save these files." You can still change your mind and remove a file from staging: git rm --cached main.py.

Once you're sure everything is right, press the "save" button:

bash
# Create a save point (commit)
git commit -m "My first project: initialize main.py and README"

Language: Bash Expected output:

[master (root-commit) a1b2c3d] My first project: initialize main.py and README
 2 files changed, 15 insertions(+)
 create mode 100644 main.py
 create mode 100644 README.md

Congratulations — you've created your first commit. That string a1b2c3d is the unique ID (hash) of this save point. From now on, you can return to this moment anytime.


Going Back in Time

You continue editing main.py, change a bunch of things, and realize you've gone in the wrong direction. This is Git's most valuable feature — time travel:

bash
# See your save history
git log

# Go back to the last save (discard all uncommitted changes)
git checkout -- .

Language: Bash Expected output (git log):

commit a1b2c3d4e5f6... (HEAD -> master)
Author: You <you@example.com>
Date: Mon Jun 23 16:00:00 2026 +0800

 My first project: initialize main.py and README

Note: git checkout -- . will permanently discard all uncommitted changes — irreversible. Unless you're absolutely sure you don't need those changes anymore, use git stash first to stash them. Newer Git versions recommend git restore . — it's semantically clearer: it only does "restore," unlike checkout which also handles branches and files.

git restore . (or git checkout -- .) undoes all uncommitted changes in your working directory, restoring files to the state of the most recent commit. Want to restore just one file instead? Use git restore main.py or git checkout -- main.py.

Git's Three-Tree Model

What you're seeing isn't magic — it's the state flowing between three areas:

Working Directory
│ git add

Staging Area (Index)
│ git commit

Repository (.git)

You write code in the working directory → when you're ready, git add to the staging area → after confirming nothing's missing, git commit into the repository. Each commit in the repository is a "complete snapshot" — Git doesn't store diffs, it stores full files (combined with compression algorithms).


Second Battle: I Start Doing Multiple Experiments

You have the save feature now, and you feel more secure. But a new problem arises: you want to try a bolder approach — experiment with an entirely new forging process.

But you don't dare. You're afraid you'll ruin the sword blank you've already forged, and then the entire piece will be unrecoverable. Your current product, while not perfect, is usable — but what if the new process doesn't work, and you can't even recover the current version?

"If only I had two parallel workbenches," you mutter to yourself, "one where I maintain the current working version, and another where I can mess around with new materials — even if the furnace explodes, it won't affect the other."

The project has grown. You have an idea: you want to try a different design approach. But you don't want to directly modify the current blueprint — in case you break it and lose the working version you have now.

This is where you need branches. Branches are like the "parallel universe switch" on a time machine.

bash
# Create a branch called experiment
git branch experiment

# Switch to the experiment branch
git switch experiment

# The above two commands are equivalent to this one (create and switch)
git switch -c experiment

Language: Bash How to run: Execute in the project directory Expected output (first command): No output, created successfully Verify: git branch lists all branches, the current one has *

 master
* experiment

Now you're editing code on the experiment branch. No matter how many lines you change or files you delete, the master branch stays untouched. They are two parallel worlds.

bash
# Make changes on the experiment branch
echo "print('hello from experiment')" >> main.py
git add main.py
git commit -m "experiment: add new print logic"

After your experiment, you want to check master again:

bash
git switch master

Back on mastermain.py doesn't have that print line. It's perfectly preserved in the state you left it.

That's the value of Git branches: You can conduct multiple non-interfering explorations, each on its own independent timeline.


Third Battle: I Want to Merge My Experiment Results

You made a lot of changes on the experiment branch — the new forging process was verified, the casting defects you found earlier are fixed, and all samples passed testing.

Now the problem: how do you turn all the improvements from the experiment branch into part of the main blueprint? You can't just handwrite the entire new design and redraw it on the master blueprint.

"You've drawn two versions of the blueprint," the workshop master appears behind you. "Now you want to add the improvements from the second version to the first. How?"

The experiment was a success — you want to merge the experiment branch into master. This is called merging.

bash
# On the master branch
git merge experiment

Language: Bash Expected output (smooth case):

Updating a1b2c3d..e5f6g7h
Fast-forward
 main.py | 1 +
 1 file changed, 1 insertion(+)

See that Fast-forward? It means master hasn't changed anything since the experiment branch was created — so Git just needs to "fast forward" the commits from experiment. This is the simplest case.

But what if both branches modified the same place? — That's the real challenge.


Fourth Battle: Two People Changed the Same Line

The previous merges went smoothly — you just added new part drawings to the main blueprint, which didn't conflict with existing designs. But this time is different.

You changed the dimensions of a part at your workbench, and your colleague at the next workbench changed the same part's dimensions too. Your changes are in opposite directions.

"Let's try merging?" your colleague says.

You put both drawings on the operating table and run the comparison. The table is silent for a second — then a red marker pops up:

Conflict

"What's a conflict?" You're starting to panic.

You and your colleague modified the same dimension on the same blueprint. You said "this interface should be round," they said "it should be square." You both registered your versions in the workshop log. Now you try to merge the two designs — the table is confused: "Which one of you is right?"

This is a merge conflict.

Let's create a conflict to see it:

bash
# From master, create the feature-a branch
git switch -c feature-a
# Change line 1 of main.py to "a"
echo "a" > main.py
git add main.py && git commit -m "feature-a: change to a"

# Back to master, create the feature-b branch
git switch master
git switch -c feature-b
# Change line 1 of main.py to "b"
echo "b" > main.py
git add main.py && git commit -m "feature-b: change to b"

Now back to master, merge feature-a first, then feature-b:

bash
git switch master
git merge feature-a # Smooth — master didn't change main.py
git merge feature-b # Conflict! Both branches changed the same line

Expected output:

Auto-merging main.py
CONFLICT (content): Merge conflict in main.py
Automatic merge failed; fix conflicts and then commit the result.

Open main.py and you'll see the markers Git left:

python
<<<<<<< HEAD
b
=======
a
>>>>>>> feature-b

Git is saying: "Above ======= is the current branch's version (master, now including feature-a's changes); below is the version you're trying to merge in (feature-b's version). You choose — or rewrite."

What you need to do:

  1. Delete the <<<<<<<, =======, >>>>>>> markers
  2. Change it to the content you want
  3. Save the file
  4. git add then git commit
bash
# Suppose I decide to keep both
echo -e "a\nb" > main.py
git add main.py
git commit -m "resolve merge conflict: keep both a and b"

Conflict resolved.

Core principle: Git won't resolve semantic conflicts for you ("a or b — which is right?"), it only resolves textual conflicts ("this line has two versions"). Semantic merging — that's your job as a human.


Fifth Battle: I Want to Share My Code with the World

You've been using Git more and more smoothly in your own workshop. But you have friends in other workshops — they also want to see the finished products and blueprints you've forged.

"Just make a copy of the blueprint and send it over?" your friend asks.

"No," you shake your head, "every time I change something, I'd have to recopy the whole thing. And the changes we each make will definitely conflict."

"Then what?"

You need a place — not in your workshop, but accessible to everyone — a shared repository that everyone can access.

It's great using Git locally in your own workshop. But eventually, you need to put your blueprints and designs somewhere everyone can reach them — so others can take them, improve them, submit modification suggestions, and continue your work when you're not around.

This is the meaning of remote repositories. The most common ones are GitHub/GitLab/Gitee.

You create a repository called my-project on GitHub. GitHub gives you an address, like https://github.com/your-username/my-project.git.

bash
# Link your local repository to the remote
git remote add origin https://github.com/your-username/my-project.git

# Push your code up
git push -u origin master

Language: Bash Expected output:

Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
...
To https://github.com/your-username/my-project.git
 * [new branch] master -> master
Branch 'master' set up to track remote branch 'master' from 'origin'.

origin is the default alias for the remote repository. -u means "remember this association — from now on, git push will know where to push."

For someone else to pull your code:

bash
git clone https://github.com/your-username/my-project.git
cd my-project

Sixth Battle: Someone Pushed New Code, I Want to Sync

You slept, came back to the workshop the next day, ready to continue your unfinished work. Habitually, you open the shared workshop log — and see that your colleague at the next workbench registered over a dozen new design modifications yesterday.

Your local blueprint is still from yesterday morning. Your drawings and your colleague's drawings are like two different forging paths — your path worked, their path worked too, but the two paths end at completely different destinations.

You can't just directly register your modifications in the shared log — because the version you have is already outdated. You need a way to pull down the updates others made and merge them with your changes.

While you were editing code locally, your colleague already pushed new code to the remote repository. Your local repository has fallen behind. You need to pull down their code and merge it with yours — this is git pull.

bash
# Pull the latest remote code and merge into local
git pull origin master

Language: Bash Expected output (smooth case):

remote: Enumerating objects: 3, done.
...
Updating a1b2c3d..f6g7h8i
Fast-forward
 utils.py | 10 ++++++++++
 1 file changed, 10 insertions(+)

git pull = git fetch (download new remote commits to local) + git merge (merge the downloaded commits into your current branch).

If you don't want automatic merging, you can do them separately:

bash
git fetch origin
git log --oneline master..origin/master # See what others committed
git merge origin/master # Confirm, then merge

Advanced: The following two battles (rebase, PR) aren't part of a personal developer's daily routine. Reading through the sixth battle is enough to manage your own code. Come back when you need team collaboration.

Seventh Battle: I Want to Clean Up Git History

Your new weapon on the experiment branch is finally complete. Excitedly, you open the workshop log, ready to register your achievement — and then you see your modification history:

a1b2c3d debug output
d4e5f6g fix typo
g7h8i9j temp save
h0i1j2k try again
l3m4n5o actual feature implemented
p6q7r8s reverted

"This is so ugly..." you mutter. Six commits, only one actually useful. When others review your PR, they'll see a bunch of "debug output" and "try again" messages.

Your branch has several commits, but some are "debugging" or "temporary saves" — junk. You want to compress those junk commits into one clean history — use rebase.

bash
# Assuming you're on the feature branch, wanting to merge the last 3 commits
git rebase -i HEAD~3

Language: Bash How to run: Executing will open an editor (typically vim/nano), showing:

pick a1b2c3d debug output
pick d4e5f6g fix typo
pick g7h8i9j actual feature implemented

Change the first two pick to squash (short form s):

pick a1b2c3d debug output
s d4e5f6g fix typo
s g7h8i9j actual feature implemented

Save and exit. Git will prompt you to write a new commit message:

Implement new feature
# This is a combination of 3 commits.

Result: The original 3 messy commits become 1 clean one.

Rebase's golden rule: Never rebase a branch that has already been pushed to a shared remote repository. Because rebase rewrites commit history — others have already done work based on your old history; rewriting it will break their work.


Eighth Battle: I Want to Suggest Changes to Someone Else's Project

In the workshop catalog, you found an open-source design — a clever tool from a remote workshop you've been following. It has a small defect, and you already know how to fix it.

You made a copy of that design, fixed the issue, and hung the new blueprint on your own material shelf. Then you realize a problem: you can't directly write your modifications back into the original designer's workshop log.

"So how do I give my improvement to them?" you scratch your head. Send someone with a hand-copied blueprint? That's last century's approach.

GitHub has a clear answer: you can't push directly — but you can request them to pull your changes in.

You want to add a feature to an open-source project. But you're not the project's maintainer — you can't push code directly. This is where you need a Pull Request (PR, called Merge Request on GitLab).

Here's the workflow:

1. Fork (make a copy of the project under your GitHub account)
2. Clone (pull it down to your local machine)
3. Create a branch (git switch -c fix-bug-123)
4. Modify code, commit, push
5. On GitHub, click "New Pull Request"

When you click New Pull Request, GitHub will show:

base: original/main ← the branch you want to merge into
compare: your-username/fix-bug-123 ← your changes

After receiving your PR, the maintainer will on GitHub:

  • View the code changes (Files changed tab)
  • Leave line-by-line comments
  • Request modifications (you modify and push; the PR updates automatically)
  • Finally merge (Merge pull request)

PR is the heart of Git workflow. It's not just code merging — it's Code Review, the basic unit of team collaboration.

Advanced: Git Workflow Models

For solo development, you can branch freely. But in a team, you need a predictable branching strategy. Here are the two most common:

The following content isn't needed when you're the sole developer. Come back when you need team collaboration and standardized branching.

GitHub Flow

The simplest collaboration model:

master (always deployable)

 └── feature-A ← work here, then PR → master
 └── fix-bug-1 ← fix it, then PR → master
  • master is always stable and deployable
  • All changes happen on separate branches
  • Merged to master via PR
  • Suitable for continuous deployment projects

Git Flow (more rigorous model)

Suitable for projects with formal release cycles:

master (production version)

 └── develop (daily development)

 ├── feature/login (new feature development)

 └── release/1.2 (release prep — only bug fixes, no new features)

Daily Operations Quick Reference

ScenarioCommand
Check current statusgit status
View commit historygit log --oneline --graph
Stage changesgit add <file>
Unstage changesgit restore --staged <file>
Undo working directory changesgit restore <file>
Create and switch branchgit switch -c <branch>
View all branchesgit branch -a
Merge another branchgit merge <branch>
Abort merge conflictgit merge --abort
Temporarily stash workgit stash
Restore stashed workgit stash pop

Common Pitfalls

Story One: .gitignore saved my life

The first time I pushed a Python project to GitHub, I found .pyc files and __pycache__ directories had been pushed along with it. Not a big problem, but every git status showed meaningless cache files. Worse — if someone pulled it down on a Mac, Mac would generate different cache files, and those would show up as "added binary files" noise in your PR review.

The solution is to create a .gitignore file:

__pycache__/
*.pyc
.env
node_modules/
.DS_Store

Put this file in your project root, and Git will automatically ignore the listed files.

Story Two: Large file disaster

Someone committed a 500MB dataset into a Git repository. From then on, every git pull had to download 500MB — even after the file was later deleted, the 500MB historical record forever exists in .git's object store. Git doesn't just store diffs — it stores every snapshot.

If you accidentally do this, you'll need git filter-branch or the BFG Repo-Cleaner to completely purge it from history. A smarter approach: manage large files from the start with .gitignore and Git LFS (Git Large File Storage).

Final Challenge

Warm-up (5 min, required)

  1. Run git init in any directory on your computer to create a repository
  2. Create a new hello.txt file, write your name in it, git add then git commit
  3. Modify hello.txt and commit again
  4. Run git log --oneline to confirm you see two commits
  5. Run git diff HEAD~1 HEAD to see the differences between the two commits

Challenge (30 min, optional)

  1. Create a test-branch branch, modify hello.txt on it
  2. Switch back to master, modify the same line of hello.txt to different content
  3. Try merging test-branch into master and watch the conflict appear
  4. Manually resolve the conflict and commit
  5. Use git log --graph --oneline --all to view the branch graph

Troubleshooting

Scenario 1: You run git pull and get:

error: Your local changes to the following files would be overwritten by merge

Diagnosis: You have uncommitted local changes that conflict with remote changes. Solution: Either git stash to stash your work, pull, then git stash pop; or commit your changes first.

Scenario 2: You realize your last commit forgot to include an important file.

Solution: git commit --amend

This modifies the most recent commit and lets you add the missing file. Note: if already pushed to remote, you'll need git push --force (only safe on a solo branch).

Checklist

  • [ ] Can explain Git's three-tree model (Working Directory ↔ Staging Area ↔ Repository)
  • [ ] Can independently create repos, commit, push, pull
  • [ ] Can create and switch branches
  • [ ] Can resolve a merge conflict
  • [ ] Knows that git pull = git fetch + git merge
  • [ ] Knows when not to use rebase (branches already pushed to shared remote) — advanced content, but jot down this rule

Common Sticking Points

  1. "What's the difference between working directory, staging area, and repository?"
  • Working directory is where you modify files; staging area is where you "confirm you want these files"; repository is where they're archived. Every git commit only saves the staging area contents, not the working directory.
  1. "I can't tell the difference between git reset, git restore, and git checkout"
  • These are Git's three most confusing commands. In newer Git, use git restore to undo changes, git switch to switch branches, and git reset to roll back commits. Don't use git checkout for these tasks — it's just a legacy interface.
  1. "Why do my merge conflicts always show <<<<<<< ?"
  • Don't panic. That's Git telling you which lines have conflicts. Delete the markers, fix the code, then add and commit. A conflict isn't a disaster — it's Git honestly saying "I can't decide for you."
  1. "What if git push is rejected?"
  • Usually your local version is behind the remote. First git pull (or git fetch + git merge) to sync, then git push.

No Need to Understand Now

  • Git's underlying object model (blob, tree, commit, tag — these are Git's internal data structures; understanding the three trees model is sufficient)
  • git rebase --onto (advanced history rewriting)
  • Git LFS, submodule, subtree
  • Binary search for bugs (git bisect) — come back when your project is big enough

Traveler's Notes

Git is not a file backup system — it's a timeline management tool. Branches let you explore multiple paths, merges let you bring results back, and conflicts are Git's honest way of saying it knows it's not good at making decisions for you.

Preview of Next Stop

Code is written, managed with Git. But what if the code has bugs? You stare at the flickering cursor on your screen, watching code scroll by — "Is there a tool that can stop the code mid-execution so I can see what's really going on inside?"

There is. Next chapter — your microscope.

Built with VitePress | Software Systems Atlas