Git: Snapshots and Moving Labels
Start with one file and a line of saved versions. Then add staging, branches, merging, remotes, and undo to the same picture, one idea at a time.
One folder, or one folder and its history
Two ways to keep old versions of the same file.
Every copy is a guess at what might matter later. The names can't tell you what changed, in what order, or which one was running last Tuesday.
Your folder
replicas: 4replicas: 2replicas: 3replicas: 3replicas: 4The folder holds only the current file. Each dot is a saved version you can open again, and the line keeps them in the order they were made.
Your folder
replicas: 4Saved versions
Git keeps the history so the folder doesn't have to
Renamed copies put the history into file names, where it gets lost. Git stores each saved version outside your working files and keeps them in order, so the folder can stay small and current.
- In the picture
- A dot is one saved version
- New word
- None yet
a folder → a line of dots → labels on the dotsThe rest of this page adds one thing at a time to this picture. Git's own vocabulary arrives after you have seen what each word names.
THE POINT TO KEEPGit is a history of your project, kept beside the files instead of inside their names.
A snapshot is a copy of everything, frozen
What does a saved version actually hold?
A small project folder. Nothing has been saved yet, so there is no way back from the next edit.
Your folder
name: shopreplicas: 2- add a cacheSaved versions
Nothing saved yet
A dot appears. It holds a copy of every file exactly as it was at that moment.
Your folder
name: shopreplicas: 2- add a cacheSaved versions
name: shopreplicas: 2- add a cacheYour folder has moved on to 3. Snapshot A still says 2, and it always will.
Your folder
name: shopreplicas: 3- add a cacheSaved versions
name: shopreplicas: 2- add a cacheSaving takes a picture of the whole project
A snapshot is not a list of edits. It records what every file contained at one moment, and nothing you do afterwards changes it. Git calls a snapshot a commit.
- In the picture
- The dot, and the files inside it
- New word
- commit: one saved snapshot
git show A:app.yaml“Save a snapshot” stands for two Git commands run together. View 4 separates them, once there is a reason to.
Pro Git: What is Git? ↗Pro Git: Recording Changes to the Repository ↗
THE POINT TO KEEPA commit is a frozen copy of the project. Your folder keeps changing; the commit does not.
Each snapshot points back at the one before
How do saved versions relate to each other?
A is saved. The folder already holds an edit that A doesn't have.
Your folder
name: shopreplicas: 3- add a cacheSaved versions
B is the new snapshot. Its arrow points back at A: every snapshot remembers the one it was made from.
Your folder
name: shopreplicas: 3- add a cacheSaved versions
name: shopreplicas: 3- add a cacheC points back at B. The line only ever grows at its newest end.
Your folder
name: shopreplicas: 4- add a cacheSaved versions
name: shopreplicas: 4- add a cacheA still holds the files as they were. Adding B and C changed nothing inside it, and looking at it changes nothing in your folder.
Your folder
name: shopreplicas: 4- add a cacheSaved versions
name: shopreplicas: 2- add a cacheHistory is a chain of snapshots
The arrow from a commit leads to its parent, the commit it was built on. Follow the arrows from the newest dot and you walk back through every earlier version.
- In the picture
- An arrow from newer to older
- New word
- parent: the commit this one came from
git log --onelineArrows point backwards because a commit knows its parent, while an older commit knows nothing about what came after it.
THE POINT TO KEEPNew commits are added at the end of the chain. Existing commits are never edited.
You choose what goes into the next snapshot
Two files changed, and only one is ready to save.
app.yaml is finished. notes.md has half an idea in it. Saving everything would put the half-idea into history.
Your folderworking directory
name: shopreplicas: 5- add a cache- half an ideaReady to savestaging area
Nothing waiting
Saved versionshistory
Git copies app.yaml, as it is right now, into the staging area. notes.md stays where it is.
Your folderworking directory
name: shopreplicas: 5- add a cache- half an ideaReady to savestaging area
name: shopreplicas: 5Saved versionshistory
D is built from the staging area. It has the new app.yaml and the old notes.md; the half-idea is still only in your folder.
Your folderworking directory
name: shopreplicas: 5- add a cache- half an ideaReady to savestaging area
Nothing waiting
Saved versionshistory
name: shopreplicas: 5- add a cacheA change lives in one of three places
Your folder holds what you are editing. The staging area holds what the next commit will contain. History holds what is already saved. git add moves a file's current contents from the first to the second; git commit turns the second into a new dot.
- New words
- working directory, staging area
- Commands
- git add, then git commit
git statusgit add copies the file as it is at that moment. If you edit it again afterwards, the staging area still holds the earlier contents until you add it again.
Pro Git: Recording Changes to the Repository ↗Git documentation: git-add ↗Git documentation: git-commit ↗
THE POINT TO KEEPA commit is built from the staging area, not straight from your folder.
A branch is a label stuck on one snapshot
How does Git know which snapshot is the latest?
You can see that D is the newest dot. Git needs a name it can look up.
Saved versionshistory
This label has been here since your first commit. It is called main, and it sits on the newest snapshot.
Saved versionshistory
E appears and main slides onto it. That is all a commit does to a label.
Saved versionshistory
The label moves; the snapshots stay put
Git calls this label a branch. It holds nothing except the name of one commit. When you commit, Git adds the new dot and moves the label onto it.
- In the picture
- A tag sitting on a dot
- New word
- branch: a movable name for one commit
git branch -vmain is only the usual name for the first branch. Git treats it like any other label.
Pro Git: Branches in a Nutshell ↗Git documentation: git-branch ↗
THE POINT TO KEEPA branch is a name that points at one commit and moves forward when you commit.
With two labels, Git tracks which one you are on
You want to try an idea without disturbing main.
main sits on the newest snapshot, E.
Saved versionshistory
A second label lands on the same snapshot. No files were copied and no dot was added.
Saved versionshistory
Two labels on one dot: which will the next commit move? Git marks the one you are on. It calls that marker HEAD.
Saved versionshistory
HEAD moves to idea. Both labels still point at E, so no file needs to change.
Saved versionshistory
F appears and only the label under HEAD moves. main still points at E.
Saved versionshistory
HEAD says which label the next commit moves
Making a branch adds a label and nothing else, which is why it is instant. HEAD is Git's “you are here” marker. A commit moves the branch HEAD is on and leaves every other branch where it was.
- New word
- HEAD: the branch you are on
- Commands
- git branch idea, git switch idea
git switch -c ideaHEAD can also sit directly on a commit with no branch between them, which Git calls a detached HEAD. Commits made there have no label holding them until you create one.
Pro Git: Branches in a Nutshell ↗Git documentation: git-branch ↗Git documentation: git-switch ↗
THE POINT TO KEEPCreating a branch copies nothing. Committing moves only the branch that HEAD is on.
Switching rewrites your folder from a snapshot
What happens to your files when HEAD moves?
HEAD is on idea, so your folder shows snapshot F. F added a cache line that E does not have.
Your folderworking directory
name: shopreplicas: 5cache: onSaved versionshistory
HEAD moves to main, and Git rewrites app.yaml to match snapshot E. The cache line is gone from the folder, not from F.
Your folderworking directory
name: shopreplicas: 5Saved versionshistory
Back on idea, the folder matches F again and the cache line returns.
Your folderworking directory
name: shopreplicas: 5cache: onSaved versionshistory
Switching would have to overwrite an edit you haven't saved, so Git refuses and moves nothing. Commit the edit or undo it, then switch.
Your folderworking directory
name: shopreplicas: 7cache: onSaved versionshistory
Refusederror: Your local changes to the following files would be overwritten by checkout: app.yaml
Your folder shows one snapshot at a time
Switching moves HEAD to another branch and then makes your files match the commit that branch points at. Both versions stay in history; only the folder changes.
- In the picture
- HEAD hops; the file is rewritten
- Command
- git switch main
git statusGit refuses only when an unsaved edit touches a file that differs between the two commits. An edit to a file that is the same on both sides travels with you.
THE POINT TO KEEPSwitching branches changes which snapshot your folder shows, and Git won't overwrite unsaved work to do it.
Merging joins two lines of work
idea worked. Now bring it into main.
idea has one snapshot that main lacks. You are on main.
Saved versionshistory
There was nothing to combine, so main slides forward to F. Git calls this a fast-forward. No new dot was needed.
Saved versionshistory
DoneFast-forward
G raised replicas on main. H added a timeout on idea. Each label now has a snapshot the other lacks: history has forked at F.
Saved versionshistory
M is a snapshot with two parents. It contains both changes, and idea has not moved.
Saved versionshistory
name: shopreplicas: 6cache: ontimeout: 30A merge either slides a label or adds a dot with two parents
If the other branch is simply ahead, Git moves your label forward. If both branches have new commits, Git combines the two sets of changes into one new commit that points back at both.
- New words
- fast-forward, merge commit
- Command
- git merge idea
git log --oneline --graphYou merge into the branch HEAD is on. The branch you name is read, never moved.
THE POINT TO KEEPA merge commit is an ordinary snapshot that happens to have two parents.
A conflict is Git asking you to choose
Both branches changed the same line.
A new branch, tune, starts at M. Then main sets replicas to 8 in I, and tune sets the same line to 10 in J.
Your folderworking directory
name: shopreplicas: 8cache: ontimeout: 30Saved versionshistory
name: shopreplicas: 10cache: ontimeout: 30Git cannot pick a number for you. It stops, adds no dot, and writes both versions into the file between markers.
Your folderworking directory
name: shop<<<<<<< HEADreplicas: 8=======replicas: 10>>>>>>> tunecache: ontimeout: 30Saved versionshistory
PausedCONFLICT (content): Merge conflict in app.yaml
You decide. Delete the markers and leave the line you want.
Your folderworking directory
name: shopreplicas: 10cache: ontimeout: 30Saved versionshistory
PausedThe merge is still open until you stage the file and commit
Staging the file tells Git the question is settled. The commit is an ordinary merge snapshot with two parents.
Your folderworking directory
name: shopreplicas: 10cache: ontimeout: 30Saved versionshistory
name: shopreplicas: 10cache: ontimeout: 30Git merges what it can and stops where it can't
Changes to different lines combine on their own, as they did in the last view. When both sides change the same lines, Git has no rule for choosing, so it hands the file to you and waits.
- In the file
- Yours above =======, theirs below
- To finish
- edit, git add, git commit
git merge --abortUntil you commit or abort, the repository is in the middle of a merge. Nothing on either branch has moved, so backing out loses only your conflict edits.
THE POINT TO KEEPA conflict pauses the merge. You settle the file, stage it, and commit to finish.
Rebase copies your work onto a new starting point
You would rather have one straight line than a fork and a join.
A new branch, fix, started at N. Since then main gained O and fix gained P. You are on fix.
Saved versionshistory
Git replays P's change on top of O as a new snapshot, P′, and moves fix onto it. The old P is still there, but no label points at it.
Saved versionshistory
The files end up the same either way. Rebase gives a straight line made of new snapshots; merge keeps both lines and joins them.
After git rebase main
Saved versionshistory
If you had run git merge main
Saved versionshistory
Rebase makes new commits; it does not move old ones
P was built on N. A commit cannot change its parent, so Git creates P′ with the same change and O as its parent, then moves your branch to it. The original is left behind without a label.
- In the picture
- Dashed dot: the old copy
- Command
- git rebase main
git rebase --abortRebase only work that nobody else has yet. If the old commits were already shared, other people are still building on them, and view 12 shows the disagreement that causes.
Git documentation: git-rebase ↗Git documentation: git-merge ↗
THE POINT TO KEEPMerge preserves how the work happened. Rebase rewrites it as if it had started later.
A remote is another copy of the same timeline
A shared server holds the project too.
The shared copy is called origin. It has its own dots and its own main. Right now both copies end at Q.
Your computer
Saved versionshistory
Shared copy · origin
Saved versionshistory
Your main moves to R. The dashed label origin/main has been on Q since you copied the project: it is your note of where the shared main was when you last checked.
Your computer
Saved versionshistory
Shared copy · origin
Saved versionshistory
Push sends R to origin and moves the shared main onto it. Your note, origin/main, is updated to match.
Your computer
Saved versionshistory
Shared copy · origin
Saved versionshistory
origin has moved on. Your copy has not heard: origin/main still says R.
Your computer
Saved versionshistory
Shared copy · origin
Saved versionshistory
Fetch downloads S and moves only origin/main. Your main and your files are untouched.
Your computer
Saved versionshistory
Shared copy · origin
Saved versionshistory
main fast-forwards to S, the same slide you saw in view 8. git pull runs fetch and this step together.
Your computer
Saved versionshistory
Shared copy · origin
Saved versionshistory
Push and fetch carry dots and labels between copies
Each copy is a complete repository. Nothing travels between them until you ask. Push uploads your commits and moves the shared branch; fetch downloads theirs and moves your origin/ labels.
- New words
- remote, origin, origin/main
- Commands
- git push, git fetch, git pull
git status -sborigin/main is stored on your computer and changes only when you fetch, pull, or push. It can be out of date without anything looking wrong.
Git documentation: git-fetch ↗Git documentation: git-push ↗Git documentation: git-pull ↗
THE POINT TO KEEPorigin/main is what you last saw, not what the server holds now. Fetch refreshes it.
When both copies moved, combine first and then push
You committed. So did a teammate.
You committed T. Meanwhile a teammate pushed U. Each copy has a snapshot the other lacks.
Your computer
Saved versionshistory
Shared copy · origin
Saved versionshistory
origin refuses. Moving its main to T would drop U from the shared history. Nothing changed on either side.
Your computer
Saved versionshistory
Refused! [rejected] main -> main (fetch first)
Shared copy · origin
Saved versionshistory
Now your copy has U and knows where the shared main is. Your own main is still on T.
Your computer
Saved versionshistory
Shared copy · origin
Saved versionshistory
The same join as view 8: V has your T and their U as parents.
Your computer
Saved versionshistory
Shared copy · origin
Saved versionshistory
origin already had U, so it receives T and V. The shared main moves forward and nobody's work is dropped.
Your computer
Saved versionshistory
Shared copy · origin
Saved versionshistory
A push may only move the shared branch forward
origin accepts a push when the commit you send has the current shared commit somewhere behind it. If it doesn't, you are missing someone's work, so you fetch it, combine it with yours, and push the result.
- Why it bounced
- T does not have U behind it
- The loop
- fetch, merge or rebase, push
git pull --rebasegit push --force makes origin take your branch anyway and discards what was there. This is how a rebase of shared commits loses a teammate's work.
Git documentation: git-push ↗Git documentation: git-fetch ↗Git documentation: git-pull ↗
THE POINT TO KEEPA rejected push means the server has commits you don't. Bring them in, then push again.
To undo, pick which place to rewind
Four different things people mean by “undo”.
Rewinds your folder. Git copies the file back from the staging area, and the edit is gone for good because it was never saved anywhere else.
Before
Your folderworking directory
name: shopreplicas: 14cache: ontimeout: 30Ready to savestaging area
Nothing waiting
Saved versionshistory
After
Your folderworking directory
name: shopreplicas: 12cache: ontimeout: 30Ready to savestaging area
Nothing waiting
Saved versionshistory
LostThe edit to 14 cannot be recovered
Rewinds the staging area. The file leaves the next commit, and your edit is still in the folder.
Before
Your folderworking directory
- add a cache- ship itReady to savestaging area
- add a cache- ship itSaved versionshistory
After
Your folderworking directory
- add a cache- ship itReady to savestaging area
Nothing waiting
Saved versionshistory
Rewinds the label. main moves back one snapshot to V. W's change stays in your folder as an unsaved edit, and W itself is left without a label.
Before
Your folderworking directory
name: shopreplicas: 12cache: ontimeout: 30Ready to savestaging area
Nothing waiting
Saved versionshistory
After
Your folderworking directory
name: shopreplicas: 12cache: ontimeout: 30Ready to savestaging area
Nothing waiting
Saved versionshistory
Rewinds nothing. Git adds a new snapshot that reverses W's change, so the history others have already seen stays intact.
Before
Your folderworking directory
name: shopreplicas: 12cache: ontimeout: 30Ready to savestaging area
Nothing waiting
Saved versionshistory
After
Your folderworking directory
name: shopreplicas: 10cache: ontimeout: 30Ready to savestaging area
Nothing waiting
Saved versionshistory
Undo depends on how far the change has travelled
An edit that is only in your folder can be thrown away. A staged file can be taken back out. A commit only you have can be dropped by moving the label. A commit others already have should be cancelled with a new commit.
- HEAD~1
- The commit one step before HEAD
- Safe after pushing
- git revert
git reset --soft | --mixed | --hardgit reset --hard also rewrites your folder to match, destroying unsaved edits. Plain git reset (the default, --mixed) keeps them; --soft keeps the staging area as well.
Git documentation: git-restore ↗Git documentation: git-reset ↗Git documentation: git-revert ↗
THE POINT TO KEEPRestore rewinds a file, reset rewinds a label, and revert adds a commit that cancels another.
What a dot is made of
Open a commit and see why the earlier views behaved as they did.
Four things are inside: a snapshot of the folder, the parent, who and when, and a message. The letters were nicknames. The real name is a fingerprint computed from these contents.
commit 4f9c2e1the dot you called W
- snapshot
- tree 8a31d07
- parent
- 1c7be52
- author
- you · Tue 14:02
- message
- Raise replicas to 12
The snapshot is a list of file names, each pointing at stored contents. notes.md has not changed since A, so every commit since then points at the same stored copy.
commit 4f9c2e1
- snapshot
- tree 8a31d07
- parent
- 1c7be52
tree 8a31d07the folder listing
- app.yaml
- blob e52b9d4
- notes.md
- blob 0d6f1aa
blob 0d6f1aacontents of notes.md, stored once
- - add a cache
Same files, same message, different parent, so a different fingerprint. This is why rebase had to create P′ instead of moving P.
commit 4f9c2e1original
- snapshot
- tree 8a31d07
- parent
- 1c7be52
commit b83a0f6the rebased copy
- snapshot
- tree 8a31d07
- parent
- 97de310
Git keeps a diary of everywhere HEAD has been. A commit that lost its label is still listed there, and a new label brings it back.
git reflognewest first
- 1c7be52
- HEAD@{0}: reset: moving to HEAD~1
- 4f9c2e1
- HEAD@{1}: commit: Raise replicas to 12
- 1c7be52
- HEAD@{2}: commit (merge): Merge origin/main
git branch rescue 4f9c2e1a label on the lost commit
- rescue
- → 4f9c2e1
- main
- → 1c7be52
A commit's name is computed from what it contains
Because the fingerprint covers the snapshot and the parent, a commit can never be edited in place: any change produces a different commit. That is what makes old dots trustworthy, and why rewriting history always means making new dots.
- New words
- hash, tree, blob, reflog
- A label is
- a small file holding one hash
git cat-file -p HEADThe hashes here are illustrative. The reflog is local and its entries expire: by default Git keeps entries for commits with no label for at least 30 days. Edits you never committed are not in it.
THE POINT TO KEEPCommits are immutable and named by content, so Git only ever adds a snapshot or moves a label.