On this page

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 a1b2c3d
2 |-- tree 7f8e9d0 (the project folder)
3 | |-- blob 3c4d5e6 README.md
4 | |-- blob 9a8b7c6 app.js
5 | |-- tree 1d2e3f4 src/
6 | |-- blob 5f6a7b8 index.js
7 |-- parent 0f1e2d3 (the commit before this one)
8 |-- author, date
9 |-- 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 object
git ls-tree HEAD # show the tree (folder) of that commit
git 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 <---- D

Git 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 <---- D
2 ^
3 main

This is why branches are cheap and instant. Creating a branch only writes a new small file. No files are copied.

1A <---- B <---- C <---- D
2 ^
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 C
2
3After commit: A <---- B <---- C <---- D main moved to D

HEAD: 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 ----> D

This chain explains how a commit works:

  1. You run git commit.
  2. Git creates a new commit whose parent is where HEAD leads to.
  3. 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/main
cat .git/refs/heads/main # the commit hash that main points to
git switch feature # move HEAD to the feature branch
git switch -c new-feature # create a new branch and move HEAD to it
git log --oneline --graph --decorate --all

Example output of the last command:

1* d4e5f6a (HEAD -> main) Add search
2* c3d4e5f Fix login bug
3* b2c3d4e Add login page
4* a1b2c3d Initial commit

Detached 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.
ThingWhat it really is
CommitA snapshot, plus a pointer to its parent, plus a message
BranchA pointer to a commit that moves forward when you commit
TagA pointer to a commit that never moves
HEADA 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 .git folder): 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

StateMeaning
UntrackedA new file that Git does not know yet
ModifiedA tracked file that you changed, but did not stage
StagedA change that is in the staging area, ready to commit
CommittedThe 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 area
git add notes.txt # copy the change into the staging area
git diff --staged # staging area vs the last commit
git commit -m "Update notes" # save the staging area as a new commit
git status # working tree clean

The two diff commands are easy to mix up:

CommandCompares
git diffWorking directory vs staging area
git diff --stagedStaging 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 piece
git 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 directory
git restore --staged notes.txt # unstage: copy the last commit's version into the staging area
  • git restore file throws away your edits in the working directory. It cannot be undone, because those edits were never saved anywhere.
  • git restore --staged file only 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: branch
2git reset --mixed HEAD~1 moves: branch + staging area (this is the default)
3git reset --hard HEAD~1 moves: branch + staging area + working directory
ModeMoves the branch?Resets the staging area?Resets the working directory?
--softYesNoNo
--mixed (default)YesYesNo
--hardYesYesYes

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 STAGED
git reset HEAD~1 # undo the commit, keep the changes as EDITS in your files
git reset --hard HEAD~1 # undo the commit AND delete the changes

Warning: --hard deletes uncommitted work, and it cannot be recovered. Work that was already committed can usually be found again with git reflog, which lists where HEAD has been.

Do not reset commits that you already pushed and others may have. Use git revert instead. It adds a new commit that cancels the old one, so shared history stays safe.

Cheat Sheet

I want to...Command
Stage a filegit add file
Unstage a filegit restore --staged file
Discard my edits in a filegit restore file
Undo the last commit, keep work stagedgit reset --soft HEAD~1
Undo the last commit, keep work in filesgit reset HEAD~1
Undo the last commit, delete the workgit reset --hard HEAD~1
Undo a pushed commit safelygit 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 origin
git remote -v # list remotes and their URLs
git 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:

  • main is your local branch. It moves when you commit.
  • origin/main is a remote-tracking branch. It is your local bookmark that remembers where main was on origin the last time you talked to it.
  • main on 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 computer
2
3main: A --- B --- C --- D origin/main: A --- B --- C
4 main: A --- B --- C --- E

In 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/main still 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/feature
git 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 saysMeaningWhat to do
ahead by N commitsYou have commits the remote does notgit push
behind by N commitsThe remote has commits you do notgit pull
divergedBoth sides have new commitsgit pull (merge or rebase), then git push
up to dateSame as the remote, as of the last fetchNothing

Gotcha: "up to date" only compares with your last photo of the remote. It does not check the server. Run git fetch to 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 C
2 (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 origin
git log --oneline main..origin/main # commits on the remote that I do not have
git diff main origin/main # the actual changes
git 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/main

It 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 + merge
git pull --rebase # fetch + rebase (straighter history)
Questiongit fetchgit pull
Contacts the remote?YesYes
Updates origin/main?YesYes
Updates your current branch?NoYes
Changes your working files?NoYes
Can cause conflicts?NoYes
Safe to run any time?YesCommit 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 origin
git 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 --force overwrites 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 project
git switch -c feature # start a branch for your work
# ...edit files...
git add . # stage the changes
git commit -m "Add search" # save a snapshot
git fetch origin # see what changed on the remote
git rebase origin/main # put my commits on top of the latest main
git 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.
  • add and commit move changes forward. restore fixes files. reset moves the branch, and its mode decides how far it goes.
  • origin/main is only a local bookmark of the remote. It updates only when you fetch, pull, or push.
  • fetch is safe and only downloads. pull is 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.

Next TopicBranching Workflows