Git has a few dozen commands and about two dozen ideas. The ideas are the part people are missing when a command surprises them. These twenty-four videos teach the ideas in order, one at a time, for anyone who already runs Git and would like it to stop being surprising. Each video is about a minute. Below each one is the script and a question to check yourself with.
Made with the Video Curriculum Guide; the unit cards, recorded sources and the independent review are in the Git Curriculum.
1. A commit is a photo, not a list of changes
A commit is a complete snapshot of every file; unchanged files are links to copies Git already has; the changes you see are computed by comparing two snapshots.
Most people picture Git as a list of changes. Add this line. Remove that one.
That is not what Git stores. A commit is a photo of every file in the project. All of them, at once.
Three commits are three photos. Each one is complete on its own.
A file that did not change is not copied again. The new photo points at the copy it already has.
When Git shows you a change, it compares two photos. The change is worked out on the spot. The photo is what is stored.
Check yourself. Git shows you a one-line change between two commits. What does Git actually store: the change, or the two photos?
2. Everything is named by its content
An object's name is the hash of its bytes, so identical content has the same name everywhere, any change produces a new name, and tampering can always be proved.
Give Git a file. It runs the content through a hash function. Out come forty characters.
That is the file's name inside Git. Not a number Git picked. A fingerprint of the content. Not of who made it.
The same content gives the same name. On your laptop. On a server. In ten years.
Change one byte, and the name changes completely.
So nothing can be altered quietly. If the content differs, the name no longer matches. Git can prove it.
Check yourself. Two people commit the same file on different machines. What can you say about the file's name in each repository?
3. Three kinds of object
Git's three main objects are blobs (file content without a name), trees (names mapped to blobs and trees), and commits (a tree, parents, author, message), all named by content.
Git stores three main kinds of thing. All of them named by content. Blob. Tree. Commit.
A blob is the content of one file. Just the bytes. No filename.
A tree is a folder. It lists names, and for each name, the blob or tree it points at.
A commit is the photo. It points at one tree. It names its parent commit, the author, and a message.
Three kinds. Everything in your history is built from them.
Check yourself. Where is a filename stored?
4. One copy of each file, and packs underneath
Snapshots do not multiply storage: identical content is stored once, and packs store the newest version of a file whole and older versions as deltas.
A photo of every file, every commit. That sounds like a lot of space.
It is not. Identical content has the identical name. So Git keeps one copy, and every photo points at it.
A file you edit does get a new blob. One line added to a long file, a whole new copy.
Now and then Git packs. It keeps the newest version whole. Older versions are stored as differences from it.
So photos are what you see. Differences are a storage trick underneath. You never have to think about them.
Check yourself. You commit ten times without touching a large image. How many copies of the image does Git hold?
5. History is a chain of pointers to parents
A commit names its parent, the parent's name is part of the child's content, so one name covers a whole history; arrows always point backward; merges have two parents.
Each commit names the commit it was made on top of. Its parent.
The arrows point backward. From child to parent. A commit does not know what comes after it.
The parent's name is part of the child's content. So the child's name covers its parent too. And the parent's parent. All the way down.
One name stands for a whole history. Same name, same history, byte for byte.
A merge has two parents. The chain becomes a graph. Still, every arrow points back.
Check yourself. Two commits have the same name. What can you say about their histories?
6. A branch is a note on a commit
A branch is a 41-byte file holding one commit name; it moves when you commit on it; making or deleting it does nothing to the commits.
People picture a branch as a copy of the project. It is not.
A branch is a note with a name. It points at one commit. On disk it is a file holding that commit's name. Forty one bytes.
Commit again, and the note moves to the new commit. The commits stay where they are.
Making a branch writes one small file. Deleting one removes the note. The commits it pointed at are untouched.
So a branch is cheap. Make as many as you like. The history is the commits. The notes just mark places, and only move when Git moves them.
Check yourself. You delete a branch. What happens to its commits?
7. HEAD says where you are
HEAD points at the branch you are on; a commit's parent is the commit under HEAD and only that branch moves; switching moves HEAD and rewrites the files; HEAD itself is a one-line file naming the branch.
Several notes, one project. Which one are you on? HEAD answers.
HEAD is a marker that points at another note. Usually a branch.
When you commit, the parent is the commit under HEAD. And the branch under HEAD moves. Not the others.
Switching branches moves HEAD to a different note. Then Git rewrites your files to match that commit.
HEAD is itself a small file. Inside is one line: the name of the branch note.
Check yourself. You have branches main and fix. You commit. Which branch moves, and how does Git know?
8. Three places: your files, the tray, the last commit
Git moves content between the working tree, the index and HEAD: add copies a file into the index (staging it), commit photographs the index and moves the branch.
Git works with three places. Your files on disk. The index. And the last commit, the one under HEAD.
The index is the proposed next commit. Git also calls it the staging area. A tray where you lay out exactly what the next photo will hold. Putting a file there is staging it.
You edit a file. Only your copy changes. The tray still holds the old version.
git add copies the file into the tray. Now the tray and your copy agree. The last commit does not.
git commit takes the photo of the tray, not of your disk. Then it moves the branch to it. Now all three agree.
Check yourself. You edit a file after adding it, then commit. Which version is in the commit?
9. Every diff compares two of the three places
git diff compares the working tree with the index, git diff --staged compares the index with HEAD, and git status reports both gaps.
Three places. A diff is always a comparison between two of them.
git diff compares your files with the tray. It shows what you have not added yet.
git diff, staged, compares the tray with the last commit. It shows what the next commit will contain.
Add a file, and git diff goes quiet about it. The change did not vanish. It moved to the other gap.
git status reports both gaps at once. Changes not staged. Changes to be committed.
Check yourself. You added a file and git diff shows nothing. Where is the change?
10. HEAD can point at a commit with no note
HEAD can point straight at a commit; commits made there belong to no branch and will be collected unless a branch or tag is placed on them.
HEAD usually points at a branch. It can also point straight at a commit.
Check out an old commit, and that is what happens. Git calls it a detached HEAD.
Looking around is fine. Build it, run it, read it.
Commit here, and the new commit has no branch. Only HEAD points at it. Move HEAD away, and no note does.
To keep that work, make a branch before you leave. A note on the commit keeps it.
Check yourself. You committed in detached HEAD and want to keep it. What do you do before switching away?
11. Commits never change
A commit cannot be edited because its name is its content; amend and every other rewrite make new commits and move the note, leaving the old commits with no note on them until cleanup.
You cannot edit a commit. Its name is its content. Change the content, and you have a different commit.
So what does amend do? Amend is the command for fixing your last commit. It makes a new commit with the same parent and your new content. Then it moves the branch note.
The old commit is still there. No note points at it any more. It is just not on the branch.
Every command that rewrites history works like this. New commits. Moved notes. The old ones stay behind.
Only much later does Git clean up commits nothing points at. Until then, nothing is lost.
Check yourself. After amend, where is the original commit?
12. The reflog remembers where the notes have been
Git logs every move of HEAD and of each branch locally; entries for commits no note reaches last about thirty days; a "lost" commit is recovered by pointing a branch at the name in the log.
Every time HEAD or a branch moves, Git writes a line in a diary. The reflog.
Each entry says where the note was, and why it moved. Commit. Checkout. Reset.
You amended, and want the old commit back? It is in the reflog. Point a branch at it.
The diary is local. Yours alone. It stays with your folder. It never travels to another repository.
Entries for commits no note reaches expire after about thirty days. Only after that are those commits removed. Before then, everything is recoverable.
Check yourself. Does the reflog travel with a clone of your repository?
13. Reset moves the note; the flags say what follows
Reset moves the current branch to a commit, then optionally makes the index match (mixed, the default) and the working tree match (hard); only uncommitted work can be lost.
git reset moves the branch under HEAD to a commit you name. That is the first step. Always.
With soft, it stops there. The tray and your files keep what they had.
With no flag, the tray is made to match the commit too. Your files keep their changes.
With hard, your files are made to match as well. Changes that were never committed are gone.
The commits you moved away from are not deleted. The reflog has them. Uncommitted work is the only thing a hard reset can lose.
Check yourself. Which flag can lose work that was never committed?
14. A merge starts from the common ancestor
Git finds the last commit both branches share, measures each tip's changes from it, applies both, and records a commit with two parents.
Two branches, each with new commits. You want one result.
Git finds the last commit both branches share. The common ancestor.
Like two people who each copied one page and edited it. To combine them, compare each copy to the original. Not to each other.
Then it compares each tip to that ancestor. Two sets of changes. It applies both.
The result is a new commit with two parents. The merge commit.
Three photos go in. The ancestor, yours, theirs. That is why it is called a three way merge.
Check yourself. How many commits does a three-way merge read?
15. Fast-forward: nothing to merge
When the other tip already contains your tip in its history, a merge just moves your note; no commit is made unless you ask for one.
Sometimes there is nothing to combine. Your branch has no new commits. The other one is simply ahead.
The other tip already has your tip in its history.
So Git just moves your note forward to that tip. No new commit. That is a fast forward.
Nothing was merged, because there was nothing on your side to merge.
You can ask Git for a merge commit anyway. Some teams do, to keep a record that the branch existed.
Check yourself. A merge produced no new commit. What was true of the two branches?
16. A conflict is two changes to the same lines
When both sides changed the same lines Git stops, writes both versions between markers, and waits; you edit, add to mark resolved, and commit to finish the merge.
A merge applies two sets of changes. Usually they touch different lines. Git applies both.
Sometimes both sides changed the same lines. Git has no rule for picking one.
So it stops. It writes both versions into the file, between markers. And it waits.
You edit the file until it says what you want. Then git add. That marks it resolved.
Then git commit finishes the merge. A conflict is not an error. It is a question only you can answer.
Check yourself. What does git add do during a conflicted merge?
17. Cherry-pick copies a change, not a commit
Cherry-pick takes the difference a commit made against its parent, applies it on HEAD, and records a new commit with a new name; the original is untouched and unlinked.
A commit on another branch has a fix you want. You could merge the whole branch. Or copy just that one change.
Cherry pick takes the difference that commit made, compared to its parent.
It applies that difference on top of your current commit. And records a new commit.
New parent, new content, new name. Same message, same author. It is a copy, not a move.
The original stays on its branch. Git does not know the two are related. You do.
Check yourself. After a cherry-pick, how many commits carry that change?
18. Rebase replays your commits on a new base
Rebase cherry-picks each of your commits onto the new tip and moves your note; the old commits remain unreferenced; the files match a merge but the history is a line.
Your branch started from an old commit on main. Main has moved on.
Rebase moves your work to start from the new tip. It cherry picks your commits, one by one, onto it.
Then it moves your branch note to the last copy.
The copies have new names. The originals are still there, with no note. Same rule as always.
Merge and rebase end with the same files. The difference is the shape of the history. Rebase makes it a straight line.
Check yourself. After a rebase, are the commit names on your branch the same as before?
19. A clone is the whole repository
Everything Git knows is in .git; clone copies every object, so a clone works offline and is a full repository; origin is only a remembered name for where it came from.
Everything Git knows lives in one folder. Dot git. Every commit, every photo, every note.
Clone copies every object. Every commit, every photo, from the first one. Not just the latest files.
So you can read the history, make commits and branches, all without a network.
The place you cloned from is remembered under a name. Origin, by default. It is a remote. A repository somewhere else.
Both sides are full repositories. Neither is the master copy, except by agreement.
Check yourself. What does a clone contain that a downloaded zip of the files does not?
20. origin/main is your memory of their note
A remote-tracking branch is a read-only bookmark of where the remote's branch was at last contact; Git moves it on fetch and push, and it goes stale in between.
Your clone has your branches. It also has notes like origin slash main.
Origin slash main is where origin's main was, the last time you looked. A bookmark, not a live view.
You cannot commit on it. Git moves it for you, when you fetch or push.
Someone pushes to origin. Your bookmark does not move. It is stale until you ask.
So two notes for one branch. Main is yours. Origin slash main is your memory of theirs.
Check yourself. Someone pushes to origin. Does your origin/main move?
21. Fetch downloads. Pull also merges
Fetch downloads missing objects and moves the bookmarks only; pull is fetch followed by a merge or rebase of the bookmark into your branch.
git fetch talks to the remote. It downloads every commit you do not have yet.
Then it moves the bookmarks. Origin slash main now matches their main.
Your own branches do not move. Your files do not change. Fetch is safe to run any time.
git pull is fetch, then merge origin slash main into your branch. Or rebase onto it, if you set that.
So pull changes your branch and your files. Fetch only changes what you know.
Check yourself. After git fetch, has your branch moved?
22. Push asks the remote to move its note
Push sends your commits and asks the remote to fast-forward its branch; it refuses if the remote's tip is not in your history; force skips the check and strands the skipped commits.
git push sends your new commits to the remote. Then it asks: move your main to this commit.
Git says yes only if their main is in the history of your commit. A fast forward.
If someone pushed first, their commit is not in your history. The push is refused.
Fetch, then merge or rebase their work in, then push again. Now their commit is behind yours.
A force push skips the check and moves the note anyway. The commits it skipped over lose their note. Use it only on branches that are yours alone.
Check yourself. Why does the remote refuse a push?
23. Do not rewrite commits others have
Rewriting replaces commits others may have built on; their merge brings the abandoned commits back; rewrite only commits nobody else has built on.
Rebase and amend make new commits and abandon the old ones. On your own machine, that is fine.
But if someone pulled the old commits, their work is built on top of them.
You push the rewritten ones. Now their history and yours disagree about commits that look the same.
They have to merge the two, and the abandoned commits come back. Twice the history. Easy to get wrong.
The rule: rewrite only commits nobody else has built on. Once others have them, add a new commit instead.
Check yourself. You amended a commit that others have already pulled. What is the safe alternative?
24. A tag is a note that never moves
A tag is a note pinned to one commit; lightweight is only the pointer, annotated is a fourth kind of object with message, date, tagger and optional signature.
A branch note moves every time you commit. Sometimes you want a name that stays put.
A tag is a note pinned to one commit, forever. Version one point zero means this exact photo.
A lightweight tag is only the pointer. A file with a name, holding a commit name.
An annotated tag is a fourth kind of object. It carries a message, a date, and a tagger, and can be signed.
Pin one on every release. Months later, the branch has moved on. Version one point zero still means that one photo.
Check yourself. What does an annotated tag have that a lightweight tag does not?
Do you like what you are reading? Subscribe to receive updates.
Unsubscribe anytime