Snapshots and staging
Trace commits and branches, choose an appropriate undo operation, and resolve a small merge conflict.
THE IDEA
Three places where your files live
Git does not simply "save your files". It keeps them in three places and moves changes between them on purpose. The working tree is the folder you edit. The staging area (also called the index) is a draft of your next snapshot. The repository holds the snapshots you have committed.
edit parser.py # a finished fix
edit notes.txt # half-finished idea
git add parser.py # put only the fix in the draft
git commit # the snapshot contains parser.py, not notes.txtWithout the middle step, every commit would swallow everything you touched, and one commit could not tell a single story.
print('hello')print('hello')print('hello')new or untracked differs from the area to its right in sync
Staging area and last commit are identical: a commit right now would have nothing to record.
Press a button. The commands run on a real (simulated) repository.
A commit is a snapshot, not a difference
Each commit records the full list of files as they were, plus a pointer to its parent and a message. Git still stays compact: a file with the same content has the same content id (a hash of the content), so unchanged files are shared between snapshots instead of being copied.
Commit 1: Start (604df4f)
| file | content id in this snapshot | same as the previous commit? |
|---|---|---|
| README.md | 0805455 | first commit |
| app.py | e59f059 | first commit |
| notes.txt | 258cd57 | first commit |
The commit lists every file, so it is a complete snapshot. Git stays small because identical content has an identical id and is stored once.
Check yourself first
Predict first. You run git add app.py, then edit app.py again, then git commit. What does the commit contain?
Chapter context
Git records snapshots in a graph of commits. Branch names are movable references to commits. Treat the working tree, staging area and recorded history as distinct states, and many apparently mysterious commands become predictable.
TRY IT YOURSELF
The folder ~/project has two files and no repository yet. The right side shows the three areas after every command. Use echo "text" >> file to edit.
- Turn this folder into a repository
- Stage app.py only: notes.txt must stay out of the staging area
- Make the first commit. It must contain app.py and not notes.txt
- Add a line print('v2') to app.py and stage it. Then add another line print('v3') WITHOUT staging it. The staged and working versions now differ
- Commit. Check that the commit holds the staged v2 and that v3 is still only in your working tree
- Commit everything that is left (v3 and notes.txt) until git status reports a clean tree
Commit graph (live)
No repository yet. Run git init.
Working tree, staging area, repository
new or untracked differs from the area to its right in sync
Stuck, or something not working? You can come back to it any time.
CHECK YOUR UNDERSTANDING
Edit a file after staging it; which version enters the next commit?
Chapter reference: math, worked example & code
This section belongs to the whole chapter, so it is the same on every lesson in it and may cover a different lesson's topic. The code for the experiment you just ran is inside the experiment itself.
Math and notation
Model history as a directed acyclic graph. A commit is reachable from a branch when following parent edges reaches it. A merge preserves both ancestry paths; rebasing creates a different path with new commit identities.
Worked example
Let B have parent A, C have parent B, and D have parent B. A merge M with parents C and D makes both C and D reachable. Moving a branch from C to M is different from copying D's files over C: the graph also records why those histories meet.
Reference code
The following example uses the Python standard library and does not call a remote service.
parents = {'A': [], 'B': ['A'], 'C': ['B'], 'D': ['B'], 'M': ['C', 'D']}
def ancestors(commit):
seen, todo = set(), [commit]
while todo:
node = todo.pop()
if node not in seen:
seen.add(node)
todo.extend(parents[node])
return seen
assert ancestors('M') == {'A', 'B', 'C', 'D', 'M'}
Python examples are provided for study and local execution; this page does not execute Python.
Common mistakes
Git commit does not automatically upload data. Rebase changes commit identities. Reset modes have different effects on uncommitted work.