Core Mental Model
Git can feel like a pile of commands you must memorize. It becomes much simpler once you understand three things: how Git stores data, where your changes live, and how your local copy relates to the remote copy. This page covers all three.
How Git stores data
Commits are snapshots, branches are pointers, and HEAD is where you are. Once this clicks, everything else makes sense.
Snapshots, Not Differences
Many tools store only the changes between versions. Git stores a snapshot of the whole project at every commit.
This sounds heavy, but it is not. If a file did not change, Git does not save it again. The new snapshot just points to the same stored file as before.
The Three Object Types
Inside the hidden .git folder, everything is stored as an object. Each object is named by a hash (a 40-character ID) that Git calculates from its content.
- Blob: the contents of one file.
- Tree: a folder. It is a list of names, each pointing to a blob or to another tree.
- Commit: points to one tree (the snapshot), to its parent commit, and holds the author, date, and message.
1commit a1b2c3d2 |-- tree 7f8e9d0 (the project folder)3 | |-- blob 3c4d5e6 README.md4 | |-- blob 9a8b7c6 app.js5 | |-- tree 1d2e3f4 src/6 | |-- blob 5f6a7b8 index.js7 |-- parent 0f1e2d3 (the commit before this one)8 |-- author, date9 |-- message "Add login page"Because the hash is calculated from the content:
- The same content always gets the same hash.
- Change one character, and the hash becomes completely different.
- A commit's hash includes its parent's hash. So if you change an old commit, every commit after it changes too. This is why history cannot be changed silently. "Rewriting history" really means creating new commits.
You can look at these objects yourself:
git cat-file -p HEAD # show the latest commit objectgit ls-tree HEAD # show the tree (folder) of that commitgit cat-file -p 3c4d5e6 # show a blob (use a hash from ls-tree)
Commits Form a Chain
Every commit remembers its parent. So the history is a chain, and each arrow points backward to the parent.
1A <---- B <---- C <---- DGit does not store "the next commit". A commit only knows where it came from.
Branches Are Just Pointers
A branch is a tiny file that holds one commit hash. That is all. The branch main is just a pointer to a commit.
1A <---- B <---- C <---- D2 ^3 mainThis is why branches are cheap and instant. Creating a branch only writes a new small file. No files are copied.
1A <---- B <---- C <---- D2 ^3 main, feature (two pointers to the same commit)When you commit, Git creates the new commit and moves the current branch pointer forward to it.
1Before commit: A <---- B <---- C main is at C2 3After commit: A <---- B <---- C <---- D main moved to DHEAD: Where You Are
HEAD is a pointer that says "you are here". Normally, HEAD does not point to a commit directly. It points to the branch you are on.
1HEAD ----> main ----> DThis chain explains how a commit works:
- You run
git commit. - Git creates a new commit whose parent is where HEAD leads to.
- Git moves the branch that HEAD points to.
When you switch branches, HEAD moves. The next commit then moves that new branch, and the old branch stays where it was.
cat .git/HEAD # ref: refs/heads/maincat .git/refs/heads/main # the commit hash that main points togit switch feature # move HEAD to the feature branchgit switch -c new-feature # create a new branch and move HEAD to itgit log --oneline --graph --decorate --all
Example output of the last command:
1* d4e5f6a (HEAD -> main) Add search2* c3d4e5f Fix login bug3* b2c3d4e Add login page4* a1b2c3d Initial commitDetached HEAD
Sometimes HEAD points straight to a commit instead of a branch. This is called a detached HEAD. It happens when you check out an old commit.
1HEAD ----> b2c3d4e (no branch in between)If you commit now, the new commit is not on any branch. When you switch away, it is easy to lose it. To keep your work, create a branch right there:
git switch --detach b2c3d4e # look at an old commit (detached HEAD)git switch -c rescue # create a branch here, so new commits are safe
A Simple Picture
Think of the history as a photo album.
- Each commit is a photo of the project at one moment.
- A branch is a sticky note stuck on one photo. Moving the note does not change any photo.
- HEAD is a "you are here" pin on one of the sticky notes.
| Thing | What it really is |
|---|---|
| Commit | A snapshot, plus a pointer to its parent, plus a message |
| Branch | A pointer to a commit that moves forward when you commit |
| Tag | A pointer to a commit that never moves |
| HEAD | A pointer to the current branch (or to a commit, if detached) |
Working directory, staging area, and repository
add, commit, restore, and reset all do one thing: they move changes between three areas.
The Three Areas
- Working directory: the real files on your disk. This is where you edit.
- Staging area (also called the index): a draft of your next commit. It holds exactly what will go into the next snapshot.
- Repository (the
.gitfolder): the saved commits.
1Working directory Staging area (index) Repository (.git)2(your files on disk) (draft of next commit) (saved commits)3 | | |4 |------- git add -------->| |5 | |------- git commit ------>|6 | | |7 |<----- git restore ------| |8 | |<-- git restore --staged -|Think of packing a parcel:
- The working directory is the items on your desk.
- The staging area is the box. You choose which items go in.
- A commit is sealing the box and putting a label on it. Once sealed, it is saved.
The Four States of a File
| State | Meaning |
|---|---|
| Untracked | A new file that Git does not know yet |
| Modified | A tracked file that you changed, but did not stage |
| Staged | A change that is in the staging area, ready to commit |
| Committed | The change is saved safely in the repository |
git status always tells you which files are in which state.
A Walk Through One Change
git status # notes.txt is modified (in the working directory)git diff # working directory vs staging areagit add notes.txt # copy the change into the staging areagit diff --staged # staging area vs the last commitgit commit -m "Update notes" # save the staging area as a new commitgit status # working tree clean
The two diff commands are easy to mix up:
| Command | Compares |
|---|---|
git diff | Working directory vs staging area |
git diff --staged | Staging area vs last commit |
Why Do We Need a Staging Area?
It lets you build clean commits. Say you fixed a bug and also added a temporary debug print in the same file. You can stage only the bug fix and commit it, and keep the debug print out.
git add -p # choose which parts of a file to stage, piece by piecegit commit -m "Fix login bug"
One commit should be one idea. The staging area makes that possible.
Undoing with restore
git restore changes files only. It does not move any branch.
git restore notes.txt # discard edits: copy the staged version into the working directorygit restore --staged notes.txt # unstage: copy the last commit's version into the staging area
git restore filethrows away your edits in the working directory. It cannot be undone, because those edits were never saved anywhere.git restore --staged fileonly unstages the file. Your edits stay in the working directory.
Moving the Branch with reset
git reset is different. It moves the current branch to another commit, and can also reset the staging area and the working directory. The mode decides how far it goes.
1git reset --soft HEAD~1 moves: branch2git reset --mixed HEAD~1 moves: branch + staging area (this is the default)3git reset --hard HEAD~1 moves: branch + staging area + working directory| Mode | Moves the branch? | Resets the staging area? | Resets the working directory? |
|---|---|---|---|
--soft | Yes | No | No |
--mixed (default) | Yes | Yes | No |
--hard | Yes | Yes | Yes |
HEAD~1 means "one commit before HEAD". Here is what each mode does to the last commit:
git reset --soft HEAD~1 # undo the commit, keep the changes STAGEDgit reset HEAD~1 # undo the commit, keep the changes as EDITS in your filesgit reset --hard HEAD~1 # undo the commit AND delete the changes
Warning:
--harddeletes uncommitted work, and it cannot be recovered. Work that was already committed can usually be found again withgit reflog, which lists where HEAD has been.
Do not reset commits that you already pushed and others may have. Use
git revertinstead. It adds a new commit that cancels the old one, so shared history stays safe.
Cheat Sheet
| I want to... | Command |
|---|---|
| Stage a file | git add file |
| Unstage a file | git restore --staged file |
| Discard my edits in a file | git restore file |
| Undo the last commit, keep work staged | git reset --soft HEAD~1 |
| Undo the last commit, keep work in files | git reset HEAD~1 |
| Undo the last commit, delete the work | git reset --hard HEAD~1 |
| Undo a pushed commit safely | git revert HEAD |
Local vs remote
origin, tracking branches, and what fetch does compared with pull.
What Is a Remote?
A remote is another copy of your repository, usually stored on a server like GitHub. Each remote has a name.
origin is the default name for the remote you cloned from. It is just a nickname for a URL. A repo can have several remotes, for example origin and upstream when you work with a fork.
git clone https://github.com/user/project.git # copy a remote repo, and name it origingit remote -v # list remotes and their URLsgit remote add origin https://github.com/user/project.git # add a remote by hand
Local Branches vs Remote-Tracking Branches
This is the part that confuses most people. There are three different things with similar names:
mainis your local branch. It moves when you commit.origin/mainis a remote-tracking branch. It is your local bookmark that remembers wheremainwas onoriginthe last time you talked to it.mainon GitHub is the real branch on the server.
You cannot commit on origin/main. Git updates it only when you talk to the remote: fetch, pull, or push.
1GitHub (origin) Your computer2 3main: A --- B --- C --- D origin/main: A --- B --- C4 main: A --- B --- C --- EIn this picture:
- Someone pushed commit D to GitHub. You do not know about it yet.
- You made commit E on your computer. GitHub does not know about it yet.
origin/mainstill says C, because you have not talked to GitHub since then.
Think of origin/main as a photo of the shared whiteboard, taken the last time you looked. The whiteboard may have changed since. The photo does not update by itself.
Tracking (Upstream)
A local branch can be linked to a remote branch. That remote branch is its upstream. This link is what lets Git tell you "ahead" or "behind", and lets plain git push and git pull know where to go.
git push -u origin feature # push, and link feature to origin/featuregit branch -vv # show each local branch and its upstream
When you clone, Git sets this up for you: local main tracks origin/main.
git status compares your branch with its remote-tracking branch:
git status says | Meaning | What to do |
|---|---|---|
| ahead by N commits | You have commits the remote does not | git push |
| behind by N commits | The remote has commits you do not | git pull |
| diverged | Both sides have new commits | git pull (merge or rebase), then git push |
| up to date | Same as the remote, as of the last fetch | Nothing |
Gotcha: "up to date" only compares with your last photo of the remote. It does not check the server. Run
git fetchto refresh the photo.
fetch: Look, Do Not Touch
git fetch downloads the new commits from the remote and updates origin/main. That is all.
- It does not change your local branches.
- It does not change your working files.
- So it is always safe. You can run it any time.
1Before fetch: origin/main at C, main at C2 (meanwhile, someone pushes D to GitHub)3 4After fetch: origin/main at D, main still at C "behind by 1 commit"After a fetch, you can look before you decide what to do:
git fetch origingit log --oneline main..origin/main # commits on the remote that I do not havegit diff main origin/main # the actual changesgit merge origin/main # bring them into my branch, when I am ready
pull: Fetch Plus Merge
git pull is two steps in one:
1git pull = git fetch + git merge origin/mainIt downloads the new commits and then merges them into your current branch. This changes your branch and your working files, and it can cause merge conflicts.
With --rebase, the second step is a rebase instead of a merge. This keeps the history in a straight line instead of adding a merge commit.
git pull # fetch + mergegit pull --rebase # fetch + rebase (straighter history)
| Question | git fetch | git pull |
|---|---|---|
| Contacts the remote? | Yes | Yes |
Updates origin/main? | Yes | Yes |
| Updates your current branch? | No | Yes |
| Changes your working files? | No | Yes |
| Can cause conflicts? | No | Yes |
| Safe to run any time? | Yes | Commit or stash your work first |
push: Send Your Commits
git push uploads your commits and moves the branch on the remote to match yours.
git push origin main # send my main commits to origingit push # same thing, when an upstream is set
If the remote has commits that you do not have, Git rejects your push. It would otherwise throw away those commits. The fix is to fetch or pull first, combine the work, and then push again.
Do not force push to a shared branch.
git push --forceoverwrites the remote's history. If you really must, use--force-with-lease. It refuses to push if someone else added commits that you have not seen.
A Typical Day
git clone https://github.com/user/project.git # once: copy the projectgit switch -c feature # start a branch for your work# ...edit files...git add . # stage the changesgit commit -m "Add search" # save a snapshotgit fetch origin # see what changed on the remotegit rebase origin/main # put my commits on top of the latest maingit push -u origin feature # share my branch
Summary
- A commit is a snapshot with a pointer to its parent. Its hash depends on its content and its parent, so history cannot change silently.
- A branch is a pointer to a commit that moves forward when you commit. HEAD is the "you are here" pointer.
- Changes move between three areas: the working directory, the staging area, and the repository.
addandcommitmove changes forward.restorefixes files.resetmoves the branch, and its mode decides how far it goes.origin/mainis only a local bookmark of the remote. It updates only when youfetch,pull, orpush.fetchis safe and only downloads.pullis fetch plus merge and changes your branch.
Interview Answer: How Does Git Work?
Git stores a project as a series of snapshots called commits. Each commit points to a tree that describes the files, and to its parent commit. Objects are named by a hash of their content, so history is tamper-evident. If an old commit changes, every later hash changes too.
A branch is just a pointer to a commit, and HEAD points to the branch I am on. Committing creates a new commit and moves the branch pointer forward. That is why branching in Git is cheap.
My changes move through three areas: the working directory, the staging area, and the repository. git add stages changes, git commit saves them, git restore fixes files, and git reset moves the branch pointer, with --soft, --mixed, and --hard deciding how much to reset.
For collaboration, origin is the default name of the remote. origin/main is a local bookmark of the remote branch, updated only on contact. git fetch downloads new commits without touching my work, and git pull is fetch plus merge, or fetch plus rebase.