Every command you actually use, each with the process to run it: staging and committing, branching and merging, rebase and cherry-pick, remotes, and the several different ways to undo something.
Getting started
git init
Turn an ordinary folder into a Git repository by creating the .git directory inside it.
- Everything Git knows lives in the
.git folder beside your files; delete it and the project becomes an ordinary folder again with no history. git init creates no commit — the repository exists but has no branch until the first commit, which is why git branch prints nothing at first.git init -b main sets the initial branch name in one step, instead of creating master and renaming it afterwards.
git init [-b <branch>] [<directory>]
git init --bare <directory>.git
Starting a project that does not exist anywhere yet: Without a repository from the first hour, the early decisions have no record, and the first mistake has nothing to roll back to.
git clone
Copy an existing repository, its full history and its branches onto your machine.
- A clone brings the entire history, not just the latest files, which is why you can read a log and switch branches with no network connection.
- The source is saved as a remote named
origin, and the default branch is checked out with tracking already configured. --depth 1 makes a shallow clone that downloads only the latest commit — far faster for CI, but git log and git blame then have almost nothing to read.
git clone <url> [<directory>]
git clone --branch <name> --depth <n> <url>
git clone --recurse-submodules <url>
Joining a project that already exists: A download of the files gives you the code without the history, so you cannot see why any line is the way it is, or push a change back.
git config
Read and set the settings that decide how Git behaves, at repository, user or system level.
- Three levels stack:
--system for the machine, --global for your user, and the repository's own .git/config — the narrowest one set wins. git config --list --show-origin prints every setting along with the file it came from, which is the fastest way to explain surprising behaviour.- Identity is per repository when you set it without
--global, which is how one machine commits as you at work and as you personally at home.
git config [--global|--system|--local] <key> [<value>]
git config --list --show-origin
git config --global --unset <key>
Committing under the wrong name on a work machine: A global identity set once follows you into every client repository, and the fix afterwards is rewriting the author of every commit you made.
git status
Show which files are changed, which are staged for the next commit, and which Git is ignoring.
- It reports three zones at once: untracked files, changes not staged, and changes staged — and a file can appear in two of them at the same time.
git status --short gives one line per file with a two-letter code: the left letter is the staging area, the right one is the working tree.- It also names the branch, how far ahead or behind its upstream it is, and any operation in progress such as a rebase or a merge.
git status [--short] [--branch] [--ignored]
Knowing what you are about to commit: Committing without checking is how a debug print, a local config file or a half-finished refactor reaches the shared branch.
git add
Stage changes, choosing exactly what the next commit will contain.
- Staging is a separate step on purpose: it lets one messy working tree become several clean, separately reviewable commits.
git add -p walks through each change and asks whether to stage it, so a bug fix and a refactor made in the same file can still be committed apart.git add . stages everything under the current folder including new files; git add -u stages only changes to files Git already tracks.
git add <pathspec>...
git add -p [<pathspec>]
git add -A | -u | .
Separating a fix from the tidying done around it: One commit containing both is impossible to revert without undoing the other, and impossible to review without reading the whole diff twice.
git commit
Record the staged changes permanently in history, with a message explaining why.
- A commit stores a full snapshot of the tracked tree, not a patch — the diff you see is calculated against its parent when you ask for it.
- Every commit has an author, a committer, a parent and a hash over all of it, which is why rewriting any commit changes the hash of every commit after it.
git commit -a stages tracked files automatically but never adds new ones, which is why a new file can be missing from a commit made that way.
git commit [-m <message>]
git commit -a -m <message>
git commit --amend [--no-edit]
Making a change you can return to: Work that is never committed cannot be compared, reverted, shared or bisected — the only record of it is whatever the editor happens to still hold.
.gitignore
List the paths Git should not track, so build output and secrets never enter history.
- Ignore rules apply only to untracked files — a file already committed keeps being tracked until you remove it with
git rm --cached. - Rules stack: the repository's
.gitignore, any .gitignore in a subfolder, .git/info/exclude for private rules, and a global ignore file for editor noise. git check-ignore -v <path> prints the exact file and line of the rule that is excluding a path, which ends most arguments about why a file will not add.
<pattern> # ignore matching paths
<directory>/ # ignore a whole directory
!<pattern> # re-include something an earlier rule excluded
git check-ignore -v <path>
Keeping credentials out of a public repository: A committed .env stays in history after it is deleted, so the only real fix is rotating every secret it contained.
git log
Read the commit history, filtered and formatted to answer a specific question.
- The default output is one screen per commit;
--oneline is what makes a history of thousands of commits readable at a glance. - Filters compose:
--author, --since, --grep for messages, -S for commits that changed how often a string appears, and -- <path> for one file. --graph --decorate --all draws the branch structure with branch and tag names on it, which is the fastest way to understand a repository you have just cloned.
git log [<revision-range>] [-- <path>...]
git log --oneline --graph --decorate --all
git log -S <string> | --grep <pattern> | --author <name>
Finding when a line of behaviour changed: Without a targeted log you read every commit since the release, or guess — and the commit that caused the bug is usually not the one that looks related.
git diff
Show the difference between any two states: working tree, staging area, commits or branches.
- With no arguments it compares the working tree to the staging area — the changes you have not staged yet, which is not the same as what will be committed.
git diff --staged compares the staging area to the last commit, and is the only one of the two that shows what the next commit will contain.git diff main...feature with three dots compares the branch against the point it diverged, which is what a pull request shows.
git diff [-- <path>...] # worktree vs index
git diff --staged [-- <path>...] # index vs HEAD
git diff <commit>..<commit> | <a>...<b>
Reviewing your own change before anyone else does: Most review comments are about things the author would have caught by reading their own diff — a stray log line, a commented-out block, a rename left half done.
Branching & merging
git branch
List, create, rename and delete branches — the movable names that point at commits.
- A branch is a forty-byte file containing one commit hash, which is why creating and deleting branches is instant however large the repository is.
git branch <name> creates a branch but does not switch to it; the branch you are on is the one HEAD points at.-d refuses to delete a branch whose commits are not merged anywhere; -D deletes it regardless and is how work is genuinely lost.
git branch [-v | -vv | -a | -r]
git branch <name> [<start-point>]
git branch -m [<old>] <new>
git branch (-d | -D) <name>...
Working on two things at once without them mixing: One branch means an urgent fix has to wait for the half-finished feature beside it, or ship alongside it by accident.
git switch
Move to another branch, or create one and move to it, without checkout's other meanings.
switch was added because checkout did two unrelated jobs; this one only changes which branch you are on.git switch -c <name> creates and switches in one step, which is the command most daily work actually starts with.- Switching refuses to run if it would overwrite uncommitted changes, unless those changes apply cleanly to both branches.
git switch <branch>
git switch -c <new-branch> [<start-point>]
git switch -
Dropping a feature to take an urgent fix: Without a clean way to move between branches, the fix gets made on top of unfinished work and ships with it.
git checkout
The original command for moving between branches, restoring files, and detaching HEAD at any commit.
- It is overloaded:
git checkout <branch> moves branch, git checkout -- <file> discards a file's changes, and git checkout <hash> detaches HEAD. git checkout -- <file> overwrites the file with no confirmation and no way back — it is the one Git command that genuinely destroys uncommitted work.- Checking out a commit rather than a branch puts you in detached HEAD: commits made there belong to no branch and disappear from listings when you leave.
git checkout <branch>
git checkout <commit> # detaches HEAD
git checkout <tree-ish> -- <path>...
Reading the code exactly as it was at a release: Reasoning about a year-old bug from the current code means reading around every change made since, and guessing which of them mattered.
git merge
Join another branch's work into the current branch, creating a merge commit when both have moved.
- Merge never rewrites history: both parents keep their original hashes, and the merge commit records that the two lines came together.
- If the current branch has not moved since the other branched off, Git fast-forwards — it just moves the pointer, and no merge commit exists.
--no-ff forces a merge commit even when a fast-forward is possible, which keeps a feature's commits visibly grouped in the history.
git merge [--no-ff | --ff-only | --squash] <branch>...
git merge -m <message> <branch>
git merge (--abort | --continue | --quit)
Bringing a finished feature into the main line: Without a merge, the feature's commits stay on a branch nobody deploys, and the main branch never gains the work.
Fast-forward vs merge commit
Understand when Git can simply move a branch pointer forward, and when it must record a merge.
- A fast-forward is possible only when the target branch has no commits the source branch lacks — the source is strictly ahead.
- A fast-forward leaves no trace that a branch existed: the commits appear as if they were made directly on the target.
--ff-only makes a merge fail rather than create a merge commit, which is how teams enforce a linear history.
git merge --ff-only <branch> # refuse anything that needs a merge commit
git merge --no-ff <branch> # always record one
git config merge.ff (true | false | only)
Deciding what the project's history should look like: A repository that merges everything with merge commits and one that rebases everything read completely differently, and mixing the two gives the worst of both.
Resolving merge conflicts
Work through the files Git could not merge automatically, choosing the correct combined result.
- A conflict means two branches changed the same lines; Git marks the file with
<<<<<<<, ======= and >>>>>>> and stops the operation. --ours is the branch you are on and --theirs is the branch being merged — and during a rebase those two are swapped, which is the most common cause of a wrong resolution.- Nothing finishes until you stage the resolved files:
git add is what tells Git the conflict is dealt with.
git status --short # UU marks an unmerged path
git checkout (--ours | --theirs) -- <path>
git add <path>... # marks it resolved
git (merge | rebase | cherry-pick) (--continue | --abort)
Two people changing the same function in the same week: The alternative is one of the two changes being silently discarded, which is only noticed when the behaviour it added stops working.
git merge --abort
Cancel a merge in progress and return the working tree to the state it had before it started.
- It only works while a merge is unfinished; once the merge commit exists you need
git reset or git revert instead. - Uncommitted changes that existed before the merge are preserved — Git refuses to start a merge that would lose them.
git merge --quit leaves the working tree as it is but forgets the merge, which is occasionally what you want and rarely what you mean.
git merge --abort
git merge --quit
A merge that turns out to be bigger than expected: Pushing on through forty conflicting files produces a resolution nobody can review, when the right answer was to rebase the feature first.
git merge --squash
Bring a branch's changes in as a single uncommitted change, discarding its individual commits.
- It stages the combined result but makes no commit and records no merge, so the branch is not marked as merged afterwards.
- The feature's individual commits are lost from the main line, which is a feature when they are "wip", "fix", "actually fix".
- Because no merge is recorded,
git branch --merged will not list the branch and -d will refuse to delete it.
git merge --squash <branch>
git commit -m <message>
A messy branch whose history nobody should have to read: Merging twenty exploratory commits into main makes the history unbisectable and the release notes unreadable.
git tag
Mark a specific commit with a permanent name, usually a release version.
- A lightweight tag is just a name for a commit; an annotated tag (
-a) is its own object with an author, a date and a message, and is what releases should use. - Tags are not pushed by
git push — they need --tags or an explicit refspec, which is why a release tag often exists only on the machine that made it. - Moving a tag that others have fetched does not update theirs; they keep the old target until they fetch with
--force.
git tag -a <name> [<commit>] -m <message>
git tag [-l <pattern>] [-n]
git tag -d <name>
git push [--follow-tags | --tags] | git push origin --delete <tag>
Knowing exactly what shipped: Without a tag, "the version in production last Tuesday" is reconstructed from deploy logs and guesswork during the incident it caused.
Branching strategies
Choose how branches are named, how long they live and how they reach production.
- Trunk-based development keeps branches alive for hours or days, so conflicts stay small and main is always releasable.
- Long-lived release branches are needed when several versions are supported at once, and they cost a cherry-pick for every fix that applies to more than one.
- The real variable is branch lifetime, not the diagram: a two-week branch conflicts badly under any strategy.
git switch -c <type>/<short-description>
git fetch origin && git rebase origin/main
git branch --merged <base>
git push origin --delete <branch>
A team where every merge is painful: Branches that live for weeks diverge so far that merging becomes a project of its own, usually done by whoever is least able to refuse.
Rebase & cherry-pick
What rebase is
Replaying your commits on top of a different base commit, so the branch looks as if it started there.
- Rebase does not move commits — it copies them. Each replayed commit is a new object with a new hash, and the originals stay until garbage collection removes them.
- The result is a linear history with no merge commit: your work appears to have been written after everything already on the target branch.
- Because the hashes change, rebasing a branch that someone else has pulled forces them to reconcile two versions of the same work.
git rebase <new-base>
git merge-base <a> <b> # where the two diverged
git log --oneline <base>..<branch> # what will be replayed
A feature branch that is three days behind main: Merging main in repeatedly leaves the branch full of merge commits nobody reads, and a pull request diff that includes other people's changes.
git rebase
Move the current branch's commits onto a new base, replaying each one in order.
git rebase <base> replays the commits that are on your branch but not on <base>, in the order you made them.- A conflict stops the rebase mid-way: you resolve it,
git add the files, and git rebase --continue to carry on with the next commit. git rebase --abort at any point returns the branch to exactly where it was before the rebase started.
git rebase [<upstream>] [<branch>]
git rebase (--continue | --skip | --abort)
git push --force-with-lease
Getting a clean pull request: A branch merged repeatedly from main shows reviewers a diff full of other people's commits, and hides the five commits that are actually up for review.
git rebase -i
Interactive rebase: edit, reorder, combine, split or drop your commits before sharing them.
- Git opens a todo list of the commits, oldest at the top, each with a command word you change —
pick, reword, edit, squash, fixup, drop. - Reordering the lines reorders the commits, and deleting a line drops that commit entirely; the file you save is the plan Git then executes.
squash keeps both commit messages for you to merge; fixup discards the second message, which is what you want for a "fix typo" commit.
git rebase -i <base>
git rebase -i --autosquash <base>
# todo words: pick, reword, edit, squash, fixup, drop
Turning eight exploratory commits into three explainable ones: A reviewer reading "wip", "try again", "actually fix it" has to reconstruct the intent from the diff, so they review the result instead of the reasoning.
git rebase --onto
Move a precise range of commits onto a new base, when the branch's own base is not what you want to move from.
- It takes three arguments: the new base, the old base, and the branch — "take what is on <branch> but not on <old base>, and put it on <new base>".
- This is the tool for branches built on other branches: when the parent branch is squash-merged,
--onto moves only your own commits across. - It is also how you drop a run of commits from the middle of a branch, by using the commit before them as the old base.
git rebase --onto <new-base> <old-base> [<branch>]
git log --oneline <old-base>..<branch> # exactly what moves
A branch stacked on another branch that got squashed: A plain rebase replays the parent branch's commits too, producing a pull request full of changes that are already in main under different hashes.
Rebase vs merge
Decide which to use by asking who else has the commits and what the history is for.
- Merge preserves what actually happened, including that two lines of work ran in parallel; rebase presents a tidier story that never occurred.
- The one hard rule is about sharing: rebase only commits nobody else has pulled, because rebasing changes their hashes.
- A common compromise is rebase locally to keep the branch current, then merge with
--no-ff so the feature stays visible as a unit.
git rebase <upstream> # replay yours on top, linear history
git merge --no-ff <branch> # record the join, keep both lines
git config pull.rebase (true | false)
A team arguing about history every sprint: Without an agreed rule, half the branches arrive rebased and half arrive with merge commits, and the log is unusable for bisecting or release notes either way.
What cherry-pick is
Copying the change one commit introduced onto the current branch, without taking anything else from its branch.
- Cherry-pick applies a commit's diff, not the commit: the result is a new commit with a new hash, the same message and a new parent.
- Nothing about the source branch is recorded, so the same change can exist twice in history with no link between the two copies.
- It is the right tool for one commit and the wrong one for ten — a run of commits is a branch, and a branch should be merged or rebased.
git cherry-pick [-x] <commit>
git show <commit> # read it before copying it
git log --cherry-mark --left-right <a>...<b>
A fix on main that a supported release also needs: Merging main into the release branch would drag in every feature since the release, which is exactly what a maintenance branch exists to avoid.
git cherry-pick
Apply one or more named commits onto the current branch as new commits.
-n applies the change without committing, so you can adjust it or combine several picks into one commit.- A range uses
A..B, which is exclusive of A — use A^..B when you mean to include A itself. --continue, --skip and --abort control a pick that stopped on a conflict, exactly as they do during a rebase.
git cherry-pick [-x] [-n] <commit>...
git cherry-pick <start>^..<end> # an inclusive range
git cherry-pick -m <parent-number> <merge-commit>
git cherry-pick (--continue | --skip | --abort)
Backporting a security fix to two supported versions: Re-implementing the fix by hand on each branch produces three slightly different fixes, and the one nobody tested is the one that ships.
git rerere
Record how you resolved a conflict and replay that resolution automatically next time it appears.
- "Reuse recorded resolution" is off by default; once enabled it records every conflict resolution you make and reapplies matching ones silently.
- It pays for itself during a long rebase, where the same conflict recurs on commit after commit, and when re-merging a long-lived branch.
- The recorded resolutions live in
.git/rr-cache and are local to your machine — they are not shared with the team.
git config --global rerere.enabled true
git rerere (diff | status | forget <path>)
A rebase that hits the same conflict twenty times: Resolving the same three lines on every replayed commit is where people give up on rebasing and merge instead.
git revert
Undo a commit by making a new commit that applies the opposite change.
- Revert adds to history rather than rewriting it, which makes it the only safe undo for a commit that has already been pushed.
- Reverting a merge needs
-m 1 to say which parent is the mainline, and the result is that the merged branch cannot simply be re-merged later. -n reverts without committing, so several reverts can be combined into one commit with one explanation.
git revert [-n] <commit>...
git revert -m <parent-number> <merge-commit>
git revert (--continue | --abort)
A bad change that is already in production: Resetting and force-pushing a shared branch breaks every clone of it; revert undoes the change while leaving everyone's history valid.
Remotes & collaboration
git remote
Manage the named URLs of the other repositories this one exchanges commits with.
- A remote is just a name for a URL plus a refspec;
origin is a convention, not a keyword, and a repository can have any number of remotes. git remote -v shows fetch and push URLs separately, which is how a repository can read from one host and write to another.- Remote-tracking branches such as
origin/main are local read-only copies updated by fetch — they are not the server's branch, only your last view of it.
git remote [-v]
git remote add <name> <url>
git remote set-url <name> <url> | rename <old> <new> | remove <name>
git remote show <name>
Working on a fork while following the original project: With only one remote you cannot pull upstream's new commits, so your fork drifts until merging it back is a rewrite.
git fetch
Download new commits and branches from a remote without changing any of your own work.
- Fetch is the only network command that cannot affect your working tree — it updates
origin/* references and nothing else. git fetch --prune deletes remote-tracking branches whose counterparts were removed on the server, which is what keeps git branch -r honest.- After fetching,
git log HEAD..origin/main shows exactly what you are about to merge or rebase onto, before you commit to doing it.
git fetch [<remote>] [<refspec>]
git fetch --all --prune --tags
git fetch <remote> <branch> # lands in FETCH_HEAD
Seeing what changed on the server before deciding what to do: Pulling straight away merges or rebases whatever arrived, which is how an unexpected force-push or a large refactor lands in the middle of your work.
git pull
Fetch from the remote and immediately integrate the result into the current branch.
- Pull is two commands: a fetch, then a merge by default or a rebase if configured — so its risk is entirely the risk of the second step.
pull.ff only makes a pull fail rather than create an unexpected merge commit, which forces you to choose deliberately when histories diverge.- Pulling with uncommitted changes can leave a conflicted working tree mid-merge, which is why committing or stashing first is the habit worth having.
git pull [<remote>] [<branch>]
git pull (--rebase | --no-rebase | --ff-only)
Starting the day on a branch several people work on: Working on yesterday's code means the first push is rejected and the conflicts are a day old rather than an hour old.
git pull --rebase
Fetch, then replay your local commits on top of what arrived instead of merging.
- It keeps a shared branch linear: your commits always sit after everyone else's rather than beside them.
- Because it rebases, your local commits get new hashes — which is fine, since they had not been pushed yet.
git config --global pull.rebase true makes it the default, and --autostash lets it run with a dirty working tree.
git pull --rebase [<remote>] [<branch>]
git config --global pull.rebase true
git config --global rebase.autoStash true
A shared branch that would otherwise fill with merge commits: Every developer pulling twice a day on a team of six adds a dozen merge commits a day that record nothing anyone will ever want to read.
git push
Send your commits to a remote and update its branch to point at them.
- A push is refused unless it fast-forwards the remote branch, which is the check that stops one person's work overwriting another's.
-u sets the upstream the first time, after which git push and git pull need no arguments on that branch.- Tags and branches are pushed separately:
git push sends the branch, and annotated tags need --follow-tags or --tags.
git push [-u] [<remote>] [<branch>]
git push --force-with-lease[=<branch>[:<expected>]]
git push [--follow-tags | --tags]
git push <remote> --delete <branch>
Making your work exist somewhere other than your laptop: Unpushed work is invisible to review, absent from CI, and gone entirely when the machine is.
Upstream tracking branches
Link a local branch to its counterpart on a remote so Git knows what to compare, pull and push.
- Tracking is what makes
git status say "ahead 2, behind 1" and what lets git push work with no arguments. git branch -vv lists every local branch with its upstream and its divergence, which is the fastest audit of a messy repository.- A branch can track a branch of a different name, or a different remote entirely, which is normal when working from a fork.
git push -u <remote> <branch>
git branch --set-upstream-to=<remote>/<branch> [<local>]
git branch -vv
git branch --unset-upstream
Knowing whether your branch is behind before you start work: Without tracking, Git cannot tell you that the branch moved, and you find out when the push is rejected an hour later.
git push --force-with-lease
Overwrite a remote branch, but only if nobody else has pushed to it since you last fetched.
- It compares the remote's current tip against your remote-tracking reference and refuses if they differ, which plain
--force never does. - Running
git fetch immediately before it defeats the protection, because the lease is then taken against what you just downloaded. - It is the correct way to publish a rebased or amended branch that is genuinely yours.
git push --force-with-lease
git push --force-with-lease=<branch>:<expected-commit>
Publishing a branch you rebased before review: A plain force-push on a branch a colleague also pushed to erases their commits with no warning and no record.
Forks and pull requests
Contribute to a repository you cannot push to, by pushing to your own copy and proposing the change.
- A fork is a server-side clone you own; your local repository then has two remotes —
origin for your fork and upstream for the original. - A pull request compares your branch against the target branch's merge base, which is why keeping the branch rebased keeps the diff small.
- Pushing more commits to the same branch updates the open pull request; you do not open a new one for a review comment.
git remote add upstream <original-url>
git switch -c <branch> upstream/main
git push -u origin <branch>
git fetch upstream && git rebase upstream/main
Contributing to a project you do not have write access to: Without a fork the only way to share a change is a patch file in an issue, which nobody can test, review in context or merge with one click.
git fetch --prune
Delete the local references to remote branches that no longer exist on the server.
- Remote-tracking branches are not removed automatically when the server's branch is deleted, so
git branch -r grows stale forever. git config --global fetch.prune true makes every fetch and pull prune, which is the setting almost everyone eventually wants.- Pruning removes only the
origin/* reference; a local branch that tracked it stays, and git branch -vv then marks it "gone".
git fetch --prune [--prune-tags]
git config --global fetch.prune true
git remote prune <remote>
A branch list that no longer reflects what is being worked on: Hundreds of dead remote branches make the list useless, so nobody reads it and duplicate work starts.
git submodule
Embed another repository inside this one at a pinned commit.
- The parent repository records a path, a URL and one exact commit — not a branch — so a submodule is a pin rather than a dependency range.
- A fresh clone leaves submodule folders empty until
--recurse-submodules or git submodule update --init fills them. - Changes inside a submodule are committed in the submodule first, then the new pointer is committed in the parent, which is two commits in two repositories.
git submodule add <url> <path>
git submodule update --init --recursive
git submodule update --remote <path>
git submodule (status | deinit -f <path>)
Sharing a component between products without publishing it: Copying the folder into each product means a fix has to be applied three times and drifts after the second.
Undoing & recovering
git restore
Discard changes in the working tree, or unstage them, without touching any commit.
git restore <file> throws away the working-tree changes to that file; --staged instead moves it out of the index and leaves the file alone.--source=<commit> restores the file as it was at any revision, which is how you get one file back from an old commit.- Discarding working-tree changes is genuinely irreversible — nothing was ever committed, so there is no reflog entry to recover from.
git restore [--worktree] <pathspec>...
git restore --staged <pathspec>...
git restore --source=<tree-ish> -- <pathspec>...
An experiment in one file that turned out to be wrong: Undoing it by hand leaves traces — a reordered import, a changed quote style — that turn up in the next diff and confuse review.
git reset --soft
Move the branch pointer back but keep every change staged, ready to be recommitted.
- Only HEAD moves: the index and the working tree are untouched, so the undone commits' changes are still staged.
git reset --soft HEAD~3 is the simplest way to combine the last three commits into one new commit.- Nothing is lost, which makes it the safest of the three resets and the right one when you only dislike the commit boundaries.
git reset --soft <commit>
git reset --soft HEAD~<n>
Three commits that should have been one: Pushing "add endpoint", "fix it", "fix it again" gives reviewers three diffs to reconcile and the release notes three entries for one change.
git reset --mixed
Move the branch pointer back and unstage everything, keeping the changes in the working tree.
--mixed is the default, so a bare git reset HEAD~1 behaves this way — commit undone, changes still on disk but unstaged.git reset with no commit argument unstages everything without moving the branch at all, which is the everyday "I staged too much" fix.- It is the right reset when you want to redo the staging as well as the commit, for instance to split one commit into several.
git reset [--mixed] [<commit>]
git reset # unstage everything, move nothing
A commit that mixed two unrelated changes: Left as one commit it cannot be reverted, reviewed or cherry-picked without dragging the other change along.
git reset --hard
Move the branch pointer back and throw away every change in the index and the working tree.
- It is the only reset that destroys uncommitted work, and it does so with no prompt and no undo.
- Commits it moves past are not destroyed — they are unreachable, and
git reflog can still find them for a few weeks. git reset --hard ORIG_HEAD undoes a merge or a pull that has just gone wrong, because Git saves the previous HEAD there.
git reset --hard <commit>
git reset --hard ORIG_HEAD # undo the merge or pull just made
git reset --hard <remote>/<branch>
A local branch that has gone irretrievably wrong: Trying to unpick a bad merge by hand takes longer than starting again from the server's copy, and usually leaves something half-undone.
git commit --amend
Replace the last commit with a new one, to fix its message or add something you forgot.
- It does not edit the commit — it creates a replacement with a new hash, which is why an amended commit that was pushed needs a force push.
- Anything staged when you amend is folded into the commit; with nothing staged, only the message changes.
--no-edit amends without opening the editor, which is the usual form when you are only adding a forgotten file.
git commit --amend [--no-edit]
git commit --amend -m <message>
git push --force-with-lease
A commit missing one file, noticed immediately: A follow-up "add missing file" commit is noise in the history and makes the original commit unbuildable if anyone checks it out.
git reflog
List everywhere HEAD and your branches have pointed, including commits no longer reachable.
- Every move — commit, checkout, reset, rebase, merge — is recorded with a position such as
HEAD@{5}, which you can check out or reset to. - It is how you recover a commit after a bad reset, a dropped interactive-rebase line, or a deleted branch.
- Entries are local and expire, by default after ninety days for reachable commits and thirty for unreachable ones — so it is a safety net, not an archive.
git reflog [show] [<ref>]
git reset --hard HEAD@{<n>}
git reflog expire --expire=<time> --all
Work that seems to have disappeared: Without the reflog, a git reset --hard past good commits, or a branch deleted with -D, is unrecoverable and the work is redone from memory.
git stash
Put uncommitted changes aside so the working tree is clean, and get them back later.
- A stash is a real commit on a hidden ref, which is why it survives branch switches and can be inspected with
git show. - By default it ignores untracked files —
-u includes them, and forgetting that is why a new file seems to vanish from the stash. - Stashes are a stack:
stash@{0} is the newest, and -m gives each one a message so the stack stays readable.
git stash push [-u] [-m <message>] [-- <pathspec>...]
git stash list
git stash show [-p] [<stash>]
git stash (drop <stash> | clear)
An urgent fix arriving mid-change: Committing half-finished work to get it out of the way puts a broken commit in the history that has to be amended or reset later.
git stash pop vs apply
Reapply stashed work, choosing whether the stash entry survives the attempt.
pop applies the stash and deletes it — but only if the apply succeeded cleanly; a conflict leaves the entry in place.apply never deletes, which is what you want when reapplying onto a branch that has moved and might conflict.git stash branch <name> creates a branch at the commit the stash was made from and applies it there, which sidesteps conflicts entirely.
git stash pop [<stash>] # apply, then drop if it went cleanly
git stash apply [<stash>] # apply and keep the entry
git stash branch <name> [<stash>]
Stashed work that no longer applies cleanly: A pop that conflicts halfway leaves a half-applied change and an entry you are no longer sure is still needed.
git clean
Delete untracked files and directories from the working tree.
- It only touches untracked files, so nothing committed or staged is at risk — but untracked files have no backup anywhere.
-n is a dry run listing what would be deleted, and should be the first form you ever type.-x also removes ignored files, which is how you get a truly pristine tree — and how you delete .env and node_modules by accident.
git clean -n [-d] [-x] # dry run: always first
git clean -f [-d] [-x] [-e <pattern>]
git clean -i -d
A build that works locally and fails from a fresh clone: Stale generated files make the local tree behave differently from CI, and the difference is invisible until someone else checks the branch out.
git rm and git mv
Delete or move a tracked file so that Git records the change rather than discovering it.
git rm removes the file and stages the deletion in one step; deleting it in the file manager leaves an unstaged deletion to remember.git rm --cached untracks a file while leaving it on disk, which is how you stop tracking something you have just added to .gitignore.git mv is shorthand for remove-plus-add; Git does not store renames, it detects them when it shows the diff.
git rm [-r] [--cached] <pathspec>...
git mv <source> <destination>
git diff --staged -M --stat
Stopping a committed secrets file from being tracked: Deleting it locally leaves it in the repository for everyone else, and re-adding .gitignore does nothing for a file already tracked.
git filter-repo
Rewrite the entire history to remove a file, a path or a piece of data from every commit.
- It is the supported replacement for
filter-branch, which was orders of magnitude slower and easy to get subtly wrong. - Every commit after the first affected one gets a new hash, so the rewrite invalidates every clone, fork, tag and open pull request.
- Removing a secret from history does not un-leak it — the value must be rotated regardless, because it was published.
git filter-repo --path <path> --invert-paths
git filter-repo --replace-text <expressions-file>
git push --force --mirror
A credential or a large binary committed months ago: The file is in every clone and every fetch, and a repository carrying a 400 MB stray asset is slow to clone forever.
Inspecting history
git show
Print one object — usually a commit with its message and full diff.
- It accepts any revision expression, so
HEAD~3, a tag, a branch name or a hash all work in the same place. git show <rev>:<path> prints a file exactly as it was at that revision, without checking anything out.- On a tag it shows the tag object first — who tagged it and why — then the commit it points at.
git show [<object>]
git show <rev>:<path>
git show --stat <commit> | git show -m <merge-commit>
Understanding a commit before cherry-picking or reverting it: Copying a commit without reading it is how a fix arrives on a release branch missing the refactor it depends on.
git blame
Show which commit last changed each line of a file, and who made it.
- It answers "when did this line become this", not "who is responsible" — the last change is often a reformat, not the decision.
-w ignores whitespace and -C follows lines moved between files, which together skip most of the noise.-L 40,60 restricts it to a line range, which is much faster and far more readable on a large file.
git blame [-L <start>,<end>] [-w] [-C] [<rev>] -- <file>
git config blame.ignoreRevsFile <file>
A line of code nobody can explain: Without the commit behind it, an odd-looking guard clause is either removed — breaking the edge case it existed for — or left forever because nobody dares.
git bisect
Find the commit that introduced a bug by binary search through history.
- You mark one commit good and one bad; Git checks out the midpoint and repeats, so a thousand commits take about ten tests.
git bisect run <command> automates it entirely — exit status zero means good, non-zero means bad.git bisect reset returns you to the branch you started on, and must be run whether or not the search succeeded.
git bisect start [<bad> [<good>]]
git bisect (good | bad | skip)
git bisect run <command> [<args>...]
git bisect reset
A regression with no obvious cause: Reading six hundred commits since the last release takes days, and the commit responsible is usually not one that looks related to the symptom.
git log --graph
Draw the branch and merge structure of the history as ASCII alongside the commits.
--graph --oneline --decorate --all is the canonical combination: shape, one line per commit, names on refs, every branch.--first-parent follows only the mainline through merges, which is how you read what actually landed on a branch rather than every contributing commit.- A custom
--pretty=format: string turns the log into exactly the columns you want, and can be saved as an alias.
git log --oneline --graph --decorate --all
git log --graph --first-parent <branch>
git log --graph --pretty=format:"<placeholders>" --date=short
Understanding a repository you have just been handed: A flat list of commits hides whether the team merges or rebases, how long branches live, and where releases were cut.
git shortlog
Group commits by author, for release notes and contribution summaries.
-s -n gives the count per author, sorted — the fastest answer to "who worked on this".- Without
-s it prints each author's commit subjects grouped under their name, which is a usable draft of release notes. - A range argument such as
v1.4.0..v1.5.0 restricts it to one release, which is how the notes get written.
git shortlog [-s] [-n] [--no-merges] [<revision-range>]
git shortlog --since=<date>
Writing release notes from the commits: Assembling them from memory or a ticket board misses the changes nobody filed a ticket for, which are usually the fixes.
git grep
Search the tracked files, or any past revision, far faster than a filesystem search.
- It searches only tracked files, so it never wastes time in
node_modules or build output. - Passing a revision searches the code as it was then, which no ordinary text search can do.
-n adds line numbers and --and / --or combine patterns, which finds files that contain two things at once.
git grep [-n] [-l] <pattern> [<rev>] [-- <pathspec>...]
git grep --all-match -e <pattern> -e <pattern>
Finding every caller before changing a function: A grep that misses the generated file or the test fixture means the rename compiles and fails at runtime.
git describe
Name the current commit in terms of the nearest tag, for build and version strings.
- The output
v1.4.0-12-g9fceb02 means twelve commits after v1.4.0, at commit 9fceb02 — human-readable and exact at once. --tags includes lightweight tags; without it only annotated tags are considered, which is why it sometimes reports nothing.--dirty appends a marker when the working tree has uncommitted changes, which stops an unreproducible build being labelled as a release.
git describe [--tags] [--dirty] [--always] [<commit>]
Stamping a build with the version it came from: A binary labelled only with a date cannot be tied back to a commit, so an incident report cannot say which code was running.
git diff --staged
Show exactly what the next commit will contain, by diffing the index against HEAD.
- It is the only diff that answers "what am I committing" — plain
git diff shows what you are *not* committing. --cached is an older spelling of the same flag and appears throughout older documentation.- Reading it before every commit is what catches debug output, commented code and half-finished renames before review does.
git diff --staged [--stat] [-- <pathspec>...]
git diff --cached # the same flag, older spelling
A commit that turns out to contain a debug print: It reaches review, someone comments, a follow-up commit removes it, and the history now has two commits for one change.
git range-diff
Compare two versions of the same series of commits, to see what a rebase or a revision changed.
- It matches commits between the two ranges by content and shows a diff of the diffs, rather than a diff of the code.
- It is how you check that a rebase or a force-push changed only what you intended.
- Reviewers use it to see what changed between review rounds without rereading the whole branch.
git range-diff <base> <old-tip> <new-tip>
git range-diff <old-base>..<old-tip> <new-base>..<new-tip>
Proving a rebase changed nothing but the base: Without it, a reviewer faced with a force-pushed branch has to re-read every commit to find the one line that also changed.
git notes
Attach information to a commit after it has been made, without changing its hash.
- Notes live in their own ref, so adding one to an ancient commit rewrites nothing and invalidates nobody's clone.
- They are not fetched or pushed by default — the refspec has to be configured, which is why notes so often exist on one machine only.
git log --notes shows them alongside the commit messages, and CI systems use them to record build and review results.
git notes [--ref=<namespace>] add -m <message> [<commit>]
git notes append -m <message> [<commit>]
git log --notes[=<namespace>]
git push <remote> "refs/notes/*"
Recording which build or review a commit passed: Putting it in the commit message means amending the commit, which changes its hash and invalidates everything downstream.
How Git works
Objects, refs and the DAG
The four object types and the pointers over them that everything else in Git is built from.
- There are four objects: blob (file contents), tree (a directory listing), commit (a tree plus parents and a message), and tag. Each is named by the SHA of its own content.
- A branch is a file under
.git/refs/heads containing one commit hash; HEAD is a file naming the current branch. Both are that simple. - Commits point backwards to their parents, forming a directed acyclic graph — which is why history can branch and merge but never loop.
git cat-file (-t | -p) <object>
git rev-parse [--abbrev-ref] <revision>
git show-ref | git for-each-ref --format=<format>
Understanding why a Git command did what it did: Memorised recipes fail the first time the situation is slightly different, and the recovery then looks like guesswork rather than reasoning.
git worktree
Check out several branches at once, each in its own folder, from one repository.
- Each worktree has its own working directory and index but shares the object store, so a second checkout costs almost no disk space.
- A branch can only be checked out in one worktree at a time, which Git enforces rather than letting two folders fight.
git worktree remove is the correct way to delete one; deleting the folder leaves a stale registration that has to be pruned.
git worktree add [-b <new-branch>] <path> [<commit-ish>]
git worktree list
git worktree (remove <path> | prune)
Reviewing a colleague's branch while your own build is running: Stashing, switching, reviewing and switching back interrupts a running dev server and loses the build cache every time.
git sparse-checkout
Check out only part of a large repository's tree, while keeping the full history.
- Cone mode takes directory prefixes and is fast; the non-cone mode takes full gitignore-style patterns and is much slower on large trees.
- It changes only which files appear on disk — history, branches and commits are unaffected, and a commit still covers the whole tree.
- Combined with
--filter=blob:none, it is how a monorepo of millions of files is worked on from a laptop.
git sparse-checkout init --cone
git sparse-checkout (set | add) <directory>...
git sparse-checkout (list | disable)
One team's folder inside a monorepo of a million files: A full checkout costs tens of gigabytes and makes every editor search and file watcher unusably slow.
Git hooks
Run your own scripts at points in Git's lifecycle, such as before a commit or after a merge.
- Hooks live in
.git/hooks, which is not version-controlled — so a hook is per clone unless core.hooksPath points somewhere in the repository. pre-commit runs before the message is written and can reject the commit by exiting non-zero; commit-msg can reject the message itself.- Local hooks are advisory: anyone can skip them with
--no-verify, so a rule that matters has to be enforced by CI as well.
git config core.hooksPath <directory>
<hooks-directory>/<hook-name> # executable, non-zero exit rejects
git commit --no-verify
Keeping formatting and lint failures out of the branch: Without a check before the commit, every pull request opens red and the first two review comments are about whitespace.
Signing commits
Attach a cryptographic signature so a commit or tag can be proved to come from you.
- Git author fields are free text, so anyone can commit as anyone; a signature is the only thing that makes authorship verifiable.
- SSH signing needs only a key you already have, which made signing practical for most teams compared with GPG.
git log --show-signature and the host's "verified" badge both read the same signature; neither is meaningful unless the key is registered.
git config --global gpg.format ssh
git config --global user.signingkey <path-to-public-key>
git commit -S -m <message> | git tag -s <name> -m <message>
git log --show-signature | git tag -v <name>
Proving who released a version: Without signatures a compromised token can push a release tag under a maintainer's name and nothing in the repository contradicts it.
git gc and git maintenance
Compact the object store and drop unreachable objects, so the repository stays fast.
gc packs loose objects and prunes unreachable ones — which is what finally deletes commits you reset away and their reflog entries expired.git maintenance start registers a scheduled background job, which is the modern replacement for running gc by hand.--aggressive repacks everything from scratch; it is slow, rarely needed, and not a general performance fix.
git gc [--aggressive] [--prune=<date>]
git count-objects -vH
git maintenance (start | run --task=<task>)
A repository that has become slow to work in: Hundreds of thousands of loose objects make every status and log call touch the filesystem far more than it needs to.
git fsck
Verify the integrity of the object store and list objects that nothing points at.
--lost-found writes dangling commits and blobs where you can inspect them, which recovers work after a reflog has expired.- Dangling objects are normal after amends, rebases and resets — they are not corruption and do not need fixing.
- Real corruption reports a missing or broken object, and the fix is almost always re-cloning from a good copy.
git fsck [--full] [--unreachable] [--no-reflogs] [--lost-found]
A commit lost after the reflog expired: Without fsck, the work exists in the object store but is unreachable by every ordinary command, so it is written off.
git bundle
Package commits into a single file that can be moved without a network connection.
- A bundle is a file you can clone from or fetch from, exactly like a remote — it carries real objects and refs, not a patch.
- An incremental bundle takes a range, so only the new commits travel, which is how a large repository is updated across an air gap.
git bundle verify checks that the receiving repository has the commits the bundle expects as a base, before you try to fetch.
git bundle create <file> <git-rev-list-args>
git bundle verify <file>
git clone <file> <directory> | git fetch <file> <refspec>
Moving a repository to a machine with no network access: A zip of the folder carries the working tree and any local state, and cannot be fetched from incrementally afterwards.
git archive
Export the files of one revision as a tar or zip, with no history and no .git folder.
- It exports a tree, not a repository — the result has no
.git, so it cannot be committed to or fetched from. .gitattributes export-ignore marks paths to leave out, which is how test fixtures and CI config stay out of a release archive.--prefix puts everything inside a named folder, which is what stops an archive unpacking over the current directory.
git archive --format=<fmt> [--prefix=<dir>/] -o <file> <tree-ish> [<path>...]
git archive --remote=<url> --format=<fmt> -o <file> <tree-ish>
Shipping a source release without the repository: Zipping the working folder includes .git, node_modules and whatever local files happen to be there.
.gitattributes
Set per-path behaviour: line endings, diff drivers, merge strategies and export rules.
* text=auto normalises line endings in the repository while leaving each checkout native, which is the fix for a Windows-and-macOS team.binary on generated or media files stops Git attempting a line diff and a merge that could only corrupt them.merge=ours on a generated file makes conflicts resolve to the current branch automatically instead of stopping every merge.
<pattern> <attribute>[=<value>]...
* text=auto
<pattern> (binary | export-ignore | merge=<driver>)
git check-attr -a <path>
A pull request where every line appears changed: One developer on Windows commits CRLF endings and every file looks entirely rewritten, hiding the three lines that actually changed.
git cherry
List which commits on one branch have an equivalent on another, and which do not.
- It compares commits by the patch they introduce rather than by hash, so a cherry-picked or rebased commit is recognised as the same change.
- A
- marks commits already present upstream and a + marks those that are not, which answers "what still needs backporting". git log --cherry-mark --left-right A...B gives the same information with the commit messages attached.
git cherry [-v] <upstream> [<head>]
git log --oneline --cherry-mark --left-right <a>...<b>
Checking a maintenance branch has every fix: Comparing hashes says nothing, because every cherry-picked fix has a different hash on the release branch.