Core Workflow
Ninety percent of Git use is one small loop: change files, inspect what changed, stage the parts you want, commit them. This page covers that loop in depth, then the far more interesting question of how to undo each step.
The loop
Section titled “The loop”git status # what changed?git diff # what exactly changed?git add src/parser.js # stage itgit diff --staged # confirm what's going ingit commit -m "Fix header parsing"git log --oneline -5 # confirm it landedgit status
Section titled “git status”git status answers “where am I and what is different?”. Run it constantly; it is free and it tells
you which command to use next.
git statusOn branch mainYour branch is up to date with 'origin/main'.
Changes to be committed: (use "git restore --staged <file>..." to unstage) modified: src/parser.js
Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory) modified: src/api.js
Untracked files: (use "git add <file>..." to include in what will be committed) notes.txtThree sections map exactly onto the three areas: staged (index vs last commit), unstaged (working directory vs index), and untracked (files Git has never recorded).
The short form is what you will actually use day to day:
git status -sb## main...origin/mainM src/parser.js M src/api.jsMM src/config.js?? notes.txtTwo columns: left is the index, right is the working directory. So M means staged, M means
modified but not staged, and MM means staged and modified again since staging. ?? is
untracked, A is a newly added file, D is deleted, R is renamed.
git add — building a commit
Section titled “git add — building a commit”git add copies the current contents of a path into the index.
git add file.txt # one filegit add src/ # a directory, recursivelygit add . # everything under the current directorygit add -A # everything in the repo, including deletionsgit add -u # only files Git already tracks (updates + deletions)git add . and git add -A behave identically in Git 2.x when run from the repository root. From a
subdirectory, . is limited to that subdirectory while -A covers the whole repo.
Partial staging with -p
Section titled “Partial staging with -p”This is the feature that makes the index worth having. git add -p walks you through each hunk
(a contiguous block of changed lines with a few lines of context) and asks whether to stage it.
git add -p src/api.js@@ -14,7 +14,9 @@ function request(url) {- return fetch(url);+ console.log('DEBUG', url);+ return fetch(url, { headers }); }Stage this hunk [y,n,q,a,d,s,e,?]?The prompts you will use:
| Key | Effect |
|---|---|
y |
Stage this hunk. |
n |
Skip this hunk. |
s |
Split into smaller hunks (only if there is a gap of context lines). |
e |
Edit the hunk manually — stage individual lines. |
q |
Quit; keep what you have staged so far. |
a / d |
Stage / skip this hunk and all remaining hunks in the file. |
Use it to split a session of mixed work into clean commits, and to catch debug statements before
they ship. git add -p with no path walks every modified tracked file.
git commit
Section titled “git commit”A commit takes whatever is in the index and writes it as a new snapshot, with the current commit as its parent, then moves the current branch pointer to it.
git commit -m "Add retry logic to the HTTP client"git commit # opens your editor for a multi-line messagegit commit -a -m "..." # stage all *tracked* modified/deleted files, then commit-a never adds untracked files. It is a shortcut for git add -u && git commit.
Writing good commit messages
Section titled “Writing good commit messages”The convention Git itself and virtually all projects follow:
Short subject line, imperative mood, under ~50 chars
Blank line, then a body wrapped at ~72 columns. Explain *why* thischange exists and what problem it solves. The diff already showswhat changed; it cannot show your reasoning, the alternatives yourejected, or the bug report that prompted it.
Fixes #142Rules that matter:
- Imperative mood — “Add retry logic”, not “Added” or “Adds”. It reads as an instruction: apply this commit and it will add retry logic. This matches Git’s own generated messages (“Merge branch…”, “Revert …”).
- Blank line after the subject. Git treats the first paragraph as the subject; tools like
git log --oneline, GitHub, and email formatting rely on that blank line. - Explain why, not what. The diff is the what.
- Do not end the subject with a period; it is a title, not a sentence.
git diff — three questions, three commands
Section titled “git diff — three questions, three commands”The confusion around diff disappears once you name what it is comparing.
| Command | Compares |
|---|---|
git diff |
Working directory vs index — “what have I not staged yet?” |
git diff --staged |
Index vs HEAD — “what will this commit contain?” |
git diff HEAD |
Working directory vs HEAD — “everything I’ve changed since the last commit”. |
git diff <commit> |
Working directory vs that commit. |
git diff A B |
Commit A vs commit B. |
git diff main..feature |
Same as git diff main feature. |
git diff main...feature |
feature vs the merge base — “what does this branch add?” |
--cached is an exact synonym for --staged.
The three-dot form is the one you want when reviewing a branch: it ignores everything that landed on
main after your branch diverged, so you see only your own changes.
Useful modifiers:
git diff --stat # summary: files and +/- countsgit diff --name-only # just the filenamesgit diff -w # ignore whitespace changesgit diff --word-diff # highlight changed words, not whole linesgit diff -- src/parser.js # limit to a pathgit diff --stat src/api.js | 12 ++++++++---- src/parser.js | 3 +-- 2 files changed, 11 insertions(+), 6 deletions(-)git log — reading history
Section titled “git log — reading history”git log # full messages, newest firstgit log --oneline # one line per commitgit log --oneline --graph --decorate # ASCII graph with branch/tag namesgit log --oneline --graph --all # ...including all branches--decorate has been on by default since Git 2.13, but including it is harmless.
git log --oneline --graph --all -8* 9c1f0a3 (HEAD -> feature) Add pagination to results* 4e2b7d1 Extract query builder| * 1a2b3c4 (origin/main, main) Bump dependencies|/* 7d8e9f0 Rename config keys* 3f2a1b0 Initial commitFiltering, which is where log earns its keep:
git log -5 # last 5 commitsgit log --author="Ada" # by authorgit log --since="2 weeks ago" # by dategit log --grep="parser" # search commit messagesgit log -S"functionName" # commits that add/remove that string ("pickaxe")git log -p # show the diff of each commitgit log --follow -- src/parser.js # history of one file, across renamesgit log main..feature # commits on feature not on maingit log --merges # only merge commits-S is the underrated one: it finds the commit that introduced or removed a given string anywhere
in the codebase, which is usually how you answer “when did this function appear?”.
Inspect a single commit:
git show HEADgit show 9c1f0a3 --statgit show HEAD:src/parser.js # the file's contents at that commitReferring to commits
Section titled “Referring to commits”You almost never type a full hash. Git accepts many forms:
| Reference | Means |
|---|---|
HEAD |
The commit you currently have checked out. |
HEAD~1, HEAD~ |
Its first parent (one commit back). |
HEAD~3 |
Three commits back along first parents. |
HEAD^ |
First parent — same as HEAD~1. |
HEAD^2 |
The second parent — only meaningful on a merge commit. |
9c1f0a3 |
An abbreviated hash; any unambiguous prefix works (usually 7+ chars). |
main, v1.2.0 |
A branch or tag name resolves to the commit it points at. |
origin/main |
The remote-tracking branch. |
@{-1} |
The previously checked-out branch. |
HEAD@{2} |
Where HEAD was two moves ago — see the reflog. |
The ~ versus ^ distinction only matters at merges: ~ walks back along first parents, ^
selects which parent.
A ── B ── M M^ = B (first parent) / M^2 = D (second parent) C ── D M~2 = A (back two, via first parents).gitignore
Section titled “.gitignore”A .gitignore file lists patterns for files Git should not track or report as untracked. Commit it
— it is part of the project.
# Dependenciesnode_modules/
# Build outputdist/*.tsbuildinfo
# Logs*.lognpm-debug.log*
# Environment and secrets.env.env.*!.env.example
# OS and editor cruft.DS_Store.idea/*.swpPattern rules
Section titled “Pattern rules”| Pattern | Matches |
|---|---|
build |
Any file or directory named build, at any depth. |
build/ |
Only directories named build, at any depth. |
/build |
Only build in the same directory as this .gitignore (anchored). |
*.log |
Any .log file at any depth. * does not cross /. |
doc/*.txt |
.txt files directly in doc/, not in doc/sub/. |
doc/**/*.txt |
.txt files anywhere under doc/. ** crosses directories. |
!important.log |
Negation — re-include a file an earlier pattern excluded. |
[0-9]*.tmp |
Character ranges work, as in shell globbing. |
# comment |
A comment. Escape a literal # as \#. |
Two rules people trip over:
-
The last matching pattern wins. Ordering matters, so put negations after the broad pattern they carve an exception out of.
-
You cannot re-include a file inside an excluded directory. If
logs/is ignored, Git never descends into it, so!logs/keep.loghas no effect. Exclude the contents instead:logs/*!logs/keep.log
Precedence between sources
Section titled “Precedence between sources”Git consults several ignore sources. Later entries here override earlier ones:
core.excludesFile— your personal global ignores..git/info/exclude— repo-local, not committed, not shared..gitignorefiles, from the repository root downwards. A.gitignorein a deeper directory overrides one above it.
Set up a global ignore for editor and OS files so you never put them in a project’s .gitignore:
git config --global core.excludesFile ~/.gitignore_globalprintf '.DS_Store\n.idea/\n*.swp\n' >> ~/.gitignore_globalDebug a surprising ignore with:
git check-ignore -v dist/app.js# => .gitignore:5:dist/ dist/app.jsIt prints the file, line number, and pattern responsible.
Removing and moving files
Section titled “Removing and moving files”git rm old-file.txt # delete from disk AND stage the deletiongit rm --cached secrets.json # stop tracking, keep the file on diskgit rm -r old-directory/ # recursivegit rm -f modified.txt # force, when the file has unstaged changesDeleting with your shell (rm file.txt) works too — Git notices, and git add -A or git add -u
stages the deletion. git rm just does both steps at once.
git mv old-name.js new-name.jsgit mv is pure convenience; it is exactly equivalent to renaming the file and running
git rm old-name.js && git add new-name.js.
Undoing things
Section titled “Undoing things”Four commands, four different jobs. Picking the right one is entirely about which of the three areas you want to change.
git restore — discard changes to files
Section titled “git restore — discard changes to files”git restore (Git 2.23+) is the modern, unambiguous way to throw away file changes.
# Discard unstaged changes to a file — copy from the index over the working directorygit restore src/api.js
# Unstage a file — copy from HEAD over the index; working directory untouchedgit restore --staged src/api.js
# Do both — discard staged and unstaged changesgit restore --staged --worktree src/api.js
# Restore a file as it was in an older commit (into the working directory)git restore --source=HEAD~3 src/api.js
# Everythinggit restore .Older tutorials use git checkout -- <file> and git reset HEAD <file> for these two jobs. They
still work, but checkout is heavily overloaded (it also switches branches), which is exactly why
restore and switch were introduced.
git reset — move the branch pointer
Section titled “git reset — move the branch pointer”git reset <commit> moves the current branch to point at <commit>. The mode flag decides whether
the index and working directory follow.
| Mode | Moves branch | Resets index | Resets working directory |
|---|---|---|---|
--soft |
yes | no | no |
--mixed (default) |
yes | yes | no |
--hard |
yes | yes | yes |
Read it as a ladder: each mode does everything the one above it does, plus one more area.
# Undo the last commit but keep everything staged — the classic "redo that commit"git reset --soft HEAD~1git commit -m "A better message and the file I forgot"
# Undo the last commit and unstage everything; changes stay in your filesgit reset HEAD~1
# Undo the last commit and throw away all its changesgit reset --hard HEAD~1git reset with a path behaves differently: it never moves HEAD, it only copies from the given
commit into the index. git reset HEAD <file> is the old way to unstage.
git revert — undo safely, in public
Section titled “git revert — undo safely, in public”reset rewrites history: it makes commits unreachable. That is fine locally and dangerous on a
branch other people have pulled.
git revert instead creates a new commit whose diff is the inverse of the target commit. History
grows rather than changes, so everyone’s clone stays consistent.
git revert 9c1f0a3 # revert one commit; opens an editor for the messagegit revert --no-edit HEAD # use the default messagegit revert HEAD~3..HEAD # revert a range (newest first)git revert -n 9c1f0a3 # stage the inverse but don't commit yetReverting a merge commit needs -m to say which parent’s line of development to keep — almost
always 1, the branch you merged into:
git revert -m 1 <merge-commit>Rule of thumb: if the commit has been pushed and others may have it, use revert. If it is purely
local, reset is cleaner.
git commit –amend — fix the last commit
Section titled “git commit –amend — fix the last commit”Replaces the most recent commit with a new one built from the current index.
# Fix the message onlygit commit --amend -m "Fix header parsing, not headers parsing"
# Add a forgotten file to the previous commitgit add src/forgotten.jsgit commit --amend --no-edit
# Change the authorgit commit --amend --author="Ada Lovelace <ada@example.com>"--no-edit keeps the existing message without opening an editor.
Decision table
Section titled “Decision table”| Situation | Command |
|---|---|
| Staged a file by mistake | git restore --staged <file> |
| Want to throw away edits to a file | git restore <file> |
| Forgot a file in the last commit | git add <file> then git commit --amend --no-edit |
| Bad message on the last commit | git commit --amend |
| Last 3 commits should be one | git reset --soft HEAD~3 then git commit |
| Want to erase the last local commit entirely | git reset --hard HEAD~1 |
| Need to undo a commit already pushed | git revert <commit> |
| Want to see an old version of a file | git show <commit>:<path> |
Key points
Section titled “Key points”git status -sbshows index state (left column) and working-directory state (right column).git add -pis the reason the staging area exists; use it to build reviewable commits.diff= working vs index,diff --staged= index vs HEAD,diff HEAD= working vs HEAD,diff main...feature= only what the branch adds..gitignoreaffects untracked files only, last matching pattern wins, and you cannot re-include inside an excluded directory.restoretouches files,resetmoves the branch pointer,revertadds an inverse commit,--amendreplaces the last commit.- Anything committed is recoverable; anything not committed is not.