Domain 2 of 4 · Chapter 1 of 7

Git Version Control Operations

The model: pointers, three trees, and add vs rewrite

Two engineers undo the same commit and get opposite results. One runs git revert and the bad change is cancelled by a new commit everyone can pull safely; the other runs git reset, the branch pointer slides backward, and history teammates already have is quietly rewritten. Almost every command on this page sorts into one of those two camps, and knowing which camp a command is in answers the single question the 350-901 exam asks most: is this safe to run on a branch other people share?

Branches, HEAD, and the three trees

A branch is not a container of commits. It is a lightweight, movable name that points at exactly one commit and advances to the new commit each time you commit on it; .git/refs/heads/main literally holds one commit SHA. HEAD is Git's pointer to where you are standing right now, normally a symbolic ref that points at your current branch, which in turn points at a commit. Git tracks three things a command can touch, which Pro Git calls the three trees: HEAD (the commit your branch is on), the index (the staging area git add writes into, which becomes your next commit), and the working tree (the actual files on disk). A commit is a saved snapshot of the index at one moment. Every mechanic later in this guide is some command moving a pointer and, optionally, dragging one or more of those trees along.

The dividing line: add versus rewrite

Sort the commands by what they do to history and the whole topic falls into place. One family adds to history: git merge, git cherry-pick, and git revert only ever append new commits, leaving every existing commit and its SHA exactly where it was. Because they touch nothing already published, they are safe on a branch others have pulled. The other family rewrites history: git rebase, git reset, and the squash tools replace commits with new ones that carry new SHAs. That is invisible and useful while the commits live only on your machine, and dangerous the moment those commits have been pushed, because your rewritten history and the copies on everyone else's clones become two different stories of the same work. This one rule, rewrite only what you have not shared, is why git rebase, git reset, and squashing all carry the same "local commits only" warning; each section below states it as a delta rather than re-deriving it.

The safety net: git reflog

The reason the rewrite family is not terrifying is the reflog[1]. git reflog records every movement of HEAD, newest first, each addressed as HEAD@{0}, HEAD@{1}, and so on: commits, checkouts, resets, rebases, merges, and cherry-picks all leave an entry. When a reset or rebase seems to lose commits, they are not deleted; they sit in the object database and the reflog still lists their SHAs, so recovery is usually one command. The boundary that matters, and it belongs right here: the reflog records committed states only. Work you never committed, the uncommitted edits a hard reset overwrites, never entered the reflog and cannot be recovered from it. The reflog is also local to your clone and expires, since Git's garbage collection[2] eventually prunes unreachable entries, so it is an emergency rescue, not a filing cabinet. Every recovery move later in this guide comes back to this one tool.

Add to history — safe on a shared branch these commands only append new commits; already-pushed SHAs never change git merge fast-forward or a merge commit git cherry-pick copy one commit's change git revert append the inverse commit Rewrite history — safe on unpushed commits only these replace commits with new SHAs; sharing them forces collaborators to reconcile git rebase replay onto a new base git reset --soft / --mixed / --hard squash reset --soft · rebase -i merge --squash Safety net: git reflog records every HEAD movement recover a commit either family strands — committed states only, until git gc prunes
Two command families: git merge, cherry-pick, and revert add to history (safe on a shared branch); rebase, reset, and squashing rewrite it (unpushed commits only).

Merging a branch: fast-forward vs three-way

Run git merge <branch> and Git picks one of two behaviors from the shape of your history, and knowing which one you will get is most of this topic.

Fast-forward. If your current branch has not moved since it split off from the branch you are merging (its tip is a direct ancestor of the incoming tip), there is nothing to combine, so Git just advances your branch pointer up to the incoming commit. No merge commit is created and history stays a straight line. This is the default whenever it is possible.

Three-way merge. If both branches have new commits since their merge base (the most recent commit they share), Git cannot simply move a pointer. It performs a three-way merge, combining the two tips against that common base, and records a merge commit, the one kind of commit that has two parents, one reaching into each branch.

The single most-tested trap lives right here: a fast-forward records no merge commit. If a stem says a fast-forward "creates a merge commit with two parents", it has described a three-way merge and mislabeled it.

Two flags override the automatic choice:

git merge --no-ff feature      # always record a merge commit, even if a fast-forward was possible
git merge --ff-only feature     # only fast-forward; abort with an error if a merge commit would be needed

--no-ff is how teams keep a feature branch visible as one unit in history; --ff-only guards a protected branch, forcing an integration to be a clean fast-forward or to fail loudly rather than silently branch the history. The git merge reference[3] lists every option, and the Basic Branching and Merging[4] chapter walks both shapes end to end.

git merge branch Diverged since the merge base? did your branch also move on? No Yes Fast-forward pointer advances, no merge commit Three-way merge merge commit with two parents Override the automatic choice no-ff forces a merge commit; ff-only refuses anything but a fast-forward
git merge fast-forwards when history did not diverge (no merge commit); otherwise it records a three-way merge commit with two parents.

Resolving a merge conflict

A merge conflict is narrower than it feels. Git merges most changes on its own and stops only when both branches changed the same lines of the same file and it cannot know which edit should win. It never guesses; it writes both versions into the file and hands you the decision.

A conflicted region looks like this:

<<<<<<< HEAD
timeout = 30        # your current branch's version
=======
timeout = 60        # the incoming branch's version
>>>>>>> feature/raise-timeout

Read the three markers precisely, because the middle block is the one nearly everyone misreads. The lines between <<<<<<< HEAD and ======= are your current branch (where HEAD points). The lines between ======= and >>>>>>> feature/raise-timeout are the incoming branch, not a second copy of your own work. The text after >>>>>>> names the branch or commit being merged in.

Unmerged paths, and the resolution loop

While any file is conflicted, git status groups it under Unmerged paths (both modified) and Git refuses to record a commit; the merge is not finished until every conflicted path is resolved and staged. The loop is always the same four steps:

git status                 # 1. see which paths are Unmerged
$EDITOR config.ini         # 2. edit to the final content, delete ALL <<<<<<< ======= >>>>>>> lines
git add config.ini         # 3. stage the file — this marks that path resolved
git commit                 # 4. finish the merge (Git pre-fills the merge message)

Staging with git add is what tells Git a path is resolved; there is no separate "resolve" command, and simply saving the edited file is not enough. Once the last Unmerged path is staged, git status reports "All conflicts fixed but you are still merging" and git commit records the merge commit. Hold onto this loop: a rebase, a cherry-pick, and a revert all pause on a conflict and resolve the exact same way, differing only in the command that finishes them (git rebase --continue, git cherry-pick --continue, or git revert --continue in place of a bare git commit). See git status[5] and the Basic Merge Conflicts[4] walkthrough.

Whole-file resolutions: --ours and --theirs

When a conflicted file is generated, a lock file, or a binary you just want one side of, take the whole file in one command instead of picking through hunks:

git checkout --ours   package-lock.json   # keep the current branch's whole file
git checkout --theirs vendor/bundle.js    # keep the incoming branch's whole file
git add package-lock.json vendor/bundle.js

The names are the trap. During a merge, --ours is your current branch, the one you were on when you ran git merge, the one HEAD points at, and --theirs is the branch being merged in. It is tempting to read --theirs as "other people's branch, not mine", but relative to the merge it is simply the incoming side. One caution belongs beside it: a rebase flips this mapping, treating the branch you rebase onto as --ours and your own replayed work as --theirs, so settle which operation is running before you decide what --ours means.

Bailing out cleanly

Do not reach for git reset --hard. git merge --abort is the purpose-built undo: it restores the working tree and index to exactly the pre-merge state, discarding the in-progress merge safely, and it works even after you have started resolving files. git reset --hard can land in the same place only if you already know the right commit to reset to, and it also throws away unrelated uncommitted work; --abort targets just the merge. The git merge reference[3] documents both.

1. git status lists the files under Unmerged paths 2. Edit to the final content delete every conflict marker line 3. git add the file staging marks that path resolved 4. git commit records the merge commit
The four-step resolution loop: git status to find Unmerged paths, edit out the markers, git add to mark each path resolved, git commit to finish.

cherry-pick: copying a commit's patch

A one-line security fix lands on main, but release/1.4 shipped weeks ago and must not absorb everything else that has piled up on main since. git cherry-pick is built for exactly this: it reproduces that single commit's change on the branch you currently have checked out. The one fact to anchor on is that it copies a commit, it never moves one.

When you run git cherry-pick <commit>[6], Git takes the change that commit introduced (its diff against its own parent), applies it on top of your current HEAD, and records the result as a new commit with a new SHA. The commit you picked is untouched, keeping its original SHA on its original branch. Reading "cherry-pick" as "relocate this commit over here" is the most common misconception; nothing is removed from the source, so after the pick the same change exists in two places under two different SHAs. The SHA differs because a commit's hash folds in its parent and timestamps, and the copy has a new parent (your branch tip) and a fresh committer time.

git switch release/1.4
git cherry-pick 4c2f1a9      # copy the fix onto release/1.4 as a NEW commit

One commit, a list, or a range

cherry-pick takes more than one target and applies them left to right, and it also accepts a range, where the single most-tested detail is a boundary rule: git cherry-pick A..B does not include commit A. The A..B set is every commit reachable from B but not from A, so A is excluded and B is included. To start the span at A, write A^..B: the trailing ^ means "the parent of A", pulling A in.

git cherry-pick 4c2f1a9 9b1e07d 21aa3fc   # several, applied left to right
git cherry-pick 4c2f1a9..21aa3fc          # a range: EXCLUDES 4c2f1a9
git cherry-pick 4c2f1a9^..21aa3fc         # same range, INCLUDING 4c2f1a9

When a stem gives a range and asks which commits apply, translate the .. before reading the options; the distractors are the off-by-one that wrongly includes or drops A.

Combining picks, and provenance

By default each picked commit becomes its own commit. -n (--no-commit)[6] applies the change to your working tree and index but stops short of committing, so you can stack several picks and fold them into one clean commit with a single git commit. And when you backport a fix to a release branch, -x[6] appends a (cherry picked from commit <sha>) line to the new commit's message, recording where the change came from, the standard convention for backports precisely because the copy has its own unrelated SHA and nothing else would link it back to the mainline fix.

When a pick stops

Because a pick replays a diff onto a branch that has moved on, the patch can fail to apply, and Git halts with the same conflict markers a merge produces (the loop from the previous section). Three exits get you out of the pause: resolve the files, git add them, and git cherry-pick --continue to create the commit and move to the next; git cherry-pick --skip drops only the commit currently in conflict and carries on with the rest of a range; git cherry-pick --abort cancels the entire operation and returns the branch to exactly where it was. Keep --continue and --skip apart: both move a range forward, but --continue keeps the current commit after you resolve it while --skip throws it away; --abort alone undoes everything.

mainrelease/1.4e91d0fix 4c2f1a9stays on mainb3d77a70c2c14b8copy 7f3e9c1new SHAcherry-pick -x
git cherry-pick copies commit 4c2f1a9 from main onto release/1.4 as a new commit 7f3e9c1; the original stays on main with its own SHA.

revert: undoing by adding an inverse commit

Push a broken commit, ten teammates pull it, and now the change has to go. Deleting it from history collides with everyone's next git pull, which is the wrong move. git revert <commit> takes the other road: it leaves the broken commit exactly where it is and adds a new commit whose content is its precise opposite, so the effect is cancelled without disturbing anyone's copy of history.

That one choice, add versus erase, is the whole idea, and it is the add-family instance of the model from the first section. git revert[7] computes the reverse of a commit's diff and records that reverse as a new commit on top of the current branch, moving history forward. Because it never rewrites, the original commit and its SHA stay put and the log shows both the change and the later commit that backs it out. The predictable misread is that revert deletes or edits the commit you name; it does neither, it can only append the inverse. That is exactly why revert, and not reset, is the safe way to undo a commit already shared: git reset rewinds the branch and rewrites history, safe only on unpushed commits, while git revert is additive and safe on a branch others have pulled.

Running a revert: conflicts and -n

A revert is a patch application, so it behaves like cherry-pick's mirror image. It applies the inverse to your working tree and index and commits it with a generated message like Revert "<original subject>". On a branch that has moved on, the inverse can overlap later edits and the revert pauses on a conflict exactly as a merge does: resolve, git add, then git revert --continue[7], or git revert --abort to bail. And like cherry-pick, git revert -n <commit> (--no-commit) stages the inverse without committing, so several reverts (or a OLDER..NEWER range) collapse into one revert commit:

git revert 1a2b3c4                   # undo one commit, make the revert commit now
git revert -n 1a2b3c4 9f8e7d6        # stage two inverses, do not commit yet
git commit -m "Roll back feature X"  # record both as a single commit

Reach for -n whenever a stem asks for several commits undone "as a single commit"; the plain form makes one revert commit per commit.

Reverting a merge needs -m

Reverting a merge is the one case with a mandatory flag: git revert -m 1 <merge-sha>. A merge commit has two parents, and revert works by inverting the diff against a parent, so Git cannot tell which side you mean as the mainline and refuses the merge without -m[7]. Parent 1 is the branch you were on when you ran git merge (usually your long-lived line, such as main); parent 2 is the branch that was merged in. git revert -m 1 <merge> keeps parent 1 as the mainline and undoes what the second side introduced.

One trap rides along with this, and the reconciling fact belongs beside it: after you revert a merge, the feature branch's commits are still reachable, so Git considers them already merged. Merge that branch again later and Git brings in nothing. To reintroduce the work you must revert the revert (git revert <the-revert-commit>) or rebuild on a fresh branch, because reverting a merge undoes the code, not the record that the merge happened. And note the limit revert shares with nothing else here: it cancels behavior, it does not scrub bytes. A secret committed and then reverted still sits in the old commit forever; purging content needs a history rewrite, not a revert.

earlier main parent 1 (main line) merge commit (two parents) revert commit M' via -m 1 parent 2 (feature) second parent git revert -m 1 keeps parent 1 (the main line) and undoes parent 2's changes, appending M'
A merge has two parents; git revert -m 1 keeps parent 1 (the main line) and undoes parent 2's changes, appending M'. From the git revert docs.

reset and squashing recent local history

Run git reset --hard HEAD~3 and three commits leave your branch along with any uncommitted edits; run git reset --soft HEAD~3 on the same branch and those three commits' changes are sitting in your staging area, ready to re-commit. One flag separates those outcomes, and it stops being mysterious once you know what a reset moves.

The three modes

git reset <commit> always does one thing first: it points HEAD and the current branch at <commit>. The mode then decides how many of the three trees from the first section (HEAD, index, working tree) are pulled along to match.

Mode HEAD Index Working tree git status after
--soft moves to <commit> unchanged unchanged changes staged
--mixed (default) moves to <commit> reset to <commit> unchanged changes unstaged
--hard moves to <commit> reset to <commit> reset to <commit> clean

Read it as a staircase, each mode resetting one more tree. --soft moves the pointer and stops, so the undone commits' changes stay staged; the misread to kill on sight is that --soft discards work, when it touches neither the index nor the working tree. --mixed, the default when you name no mode, also resets the index, so the changes become unstaged modifications; reading the default reset as "delete the files" is wrong, it unstages and removes nothing from disk. --hard resets the index and the working tree, so the uncommitted changes are gone; expecting --hard to leave them staged the way --soft does has the mode backwards. The git reset reference[8] and Pro Git's Reset Demystified[9] are canonical.

The path form is a different tool

Hand git reset a path and it stops being a branch-rewinder. git reset <file> (short for git reset HEAD <file>) does not move HEAD and does not touch the working tree; it rewrites only that file's entry in the index back to the HEAD version, precisely the inverse of git add. The mode flags do not apply to it (--soft/--hard with a pathspec is an error), and newer Git gives the job its own unambiguous verb, git restore --staged <file>[10].

Recovery, and when not to reset

Never reset a branch whose commits others already have, because rewinding a shared branch makes everyone's history disagree with yours; to undo a published commit, use git revert from the previous section. But on your own local history, reset is forgiving: because it only moves a pointer, the commits it leaves behind stay in the object database, and git reflog still lists the SHA HEAD held before the reset, so git reset --hard <sha-from-reflog> puts the branch back. The catch, next to the recovery: the reflog remembers commits, not uncommitted edits, so the working-tree changes --hard overwrote are the one thing no reflog can restore.

Squashing: three tools, one job

Squashing collapses a trail of wip, fix typo, oops commits into one clean commit before you open a merge request, and it is just the rewrite family applied to your last few commits. Three tools do it:

  • git reset --soft HEAD~3 is the fastest: it moves the branch tip back three commits while keeping all their changes staged, so one git commit records them as a single commit. This is the same --soft from above, keeping the changes rather than discarding them.
  • git merge --squash <branch> applies another branch's combined change to your working tree and stages it, then stops. It does not create the commit and does not record the source branch as a parent; you make one ordinary git commit yourself. Expecting --squash to commit for you leaves you hunting for a commit that was never made.
  • git rebase -i HEAD~3 opens an editor listing the commits, each prefixed pick; change a line to squash to fold that commit into the one above it and keep its message for you to edit, or fixup to fold it in and discard its message. Inverting that pair, thinking fixup keeps the message and squash drops it, is the single most common trap in this topic.

All three write new commits with new SHAs in place of the originals, which is why the same rule governs every one of them: squash only commits you have not pushed. On a shared branch the rewrite forces a divergence and a coordinated git push --force-with-lease[11] that can clobber a teammate; a platform like GitLab can instead squash at merge time through the merge request's squash-and-merge[12] option, with no local rewrite at all.

git reset --<mode> <commit> 1. Move HEAD + current branch every mode: --soft, --mixed, --hard 2. Reset the index (staging area) --mixed (default) and --hard 3. Reset the working tree (files) --hard only --soft stops here changes stay staged --mixed stops here changes unstaged --hard stops here changes discarded
git reset -- moves HEAD first, then cascades into the index (--mixed, --hard) and the working tree (--hard only). After Pro Git, Reset Demystified.

rebase: replaying commits and the golden rule

Your feature branch is three commits deep, and while you worked, main moved ahead by two commits. You want your three commits to sit cleanly on top of the new main, as if you had started from there this morning. That is git rebase main[13]: it finds where your branch and main diverged (the merge base), then re-applies each of your commits, in order, on top of the current tip of main.

The mechanism is a replay, not a move. Git checks out the base and applies your commits one at a time, recording each as a new commit with a new SHA; the originals are left unreferenced once your branch pointer advances. The SHA changes because it hashes the commit's new parent and a fresh timestamp, so even an identical code change gets a different identity. This is the first trap: rebasing does not preserve your original SHAs. git merge is the opposite bargain, leaving every commit untouched and adding one merge commit that keeps the branchy shape, so the choice is linear-and-rewritten (rebase) versus faithful-and-branchy (merge), as the Pro Git rebasing chapter[14] frames it.

The golden rule

Rebase only commits that live nowhere but your own machine. Picture a commit X you pushed; a teammate pulls it. You rebase, replacing X with X' (a new SHA), and git push --force to overwrite the remote. Nothing about the force-push reaches your teammate's clone: they still have X, the remote now has X', and their next pull sees two lineages for the same work and asks them to merge the tangle. So "rebasing a shared branch is fine as long as you force-push" is wrong, because the force-push relocates the divergence rather than removing it, and recovery means coordinating with the whole team. The rule is about sharing, not committing: local commits are yours to rebase freely (the whole point of cleaning up before review), and the moment they are pushed you switch to git merge to integrate. That reconciles the two true-but-tense statements, that rebase is a safe everyday tool and that rebase can wreck a team's history, by whether the commits have left your machine.

When a rebase stops, and pull --rebase

A rebase re-applies each commit onto a base it was not written against, so one patch can fail and Git pauses on that commit with conflict markers. Same loop, different verb: resolve, git add, then git rebase --continue to commit the replayed change and move on. The most common wrong answer here is a bare git commit; during a rebase you do not commit by hand, and --continue is what records the commit and keeps the replay going. The two escape hatches: git rebase --skip drops the one commit currently in conflict (use it when that change is already on the new base) and carries on, while git rebase --abort unwinds the whole rebase back to the pre-rebase state.

Finally, git pull defaults to fetch-then-merge, which sprinkles a busy branch with tiny "Merge branch main" commits. git pull --rebase[15] swaps the second half: it fetches the upstream and rebases your local commits on top of it, keeping the branch a clean linear series with no merge bubble (git config --global pull.rebase true makes it the default). It does not suspend the golden rule, since pull --rebase still rewrites your local commits, so it is safe only while they are unpushed.

Before: forked historymainfeatureBmerge baseM1M2a1a2git rebase mainAfter: linear historyM2main tipa1'a2'new SHA
A rebase turns a fork into a line: Git finds merge base B, then replays a1, a2 onto main's tip as new commits a1', a2' with new SHAs. Pro Git: Rebasing.

Navigating and housekeeping

git checkout is the command people first meet by accident, when git status prints HEAD detached at 9f2c1a7 and they are not sure what they did. It reads oddly because the one verb does two unrelated jobs, which Git 2.23 later split into clearer commands.

Switching branches (checkout / switch)

Give git checkout a ref, a branch or a commit, and it moves HEAD, updating the index and working tree to match. git checkout <branch> switches branches; git checkout -b <new> creates a branch at the current commit and switches to it in one step. The modern, unambiguous spelling is git switch <branch> (and git switch -c <new> to create), which does only the branch-switch job. A plain branch switch never detaches HEAD, and Git refuses it if it would silently overwrite uncommitted changes (stash them first).

Discarding file edits (checkout -- / restore)

Give git checkout a path instead and it leaves HEAD alone and overwrites that file from the index: git checkout -- <file> throws away your uncommitted edits to the file, restoring the committed version (the modern verb is git restore <file>). Reading this as a way to stage a file is the classic misconception and it points the wrong way, because git add copies the working tree into the index while checkout of a path copies the index onto your files, the opposite direction. The bare -- matters: it tells Git the names after it are paths, not refs.

Detached HEAD

git checkout <commit-sha> (or a tag) does the one thing a branch checkout never does: it points HEAD straight at a commit, a state Git calls detached HEAD. It is not an error, since checking out an old commit to build, test, or bisect is what it is for. The rule that explains everything: a commit you make in detached HEAD belongs to no branch. HEAD moves forward to your new commit but no branch pointer follows, so once you switch away nothing references it, and Git warns you as you leave. Assuming those commits followed the branch you were on before the detach is the trap; they did not. To keep the work, name it a branch before you leave with git switch -c <name> (or git branch <name>); if you already left, git reflog still lists the SHA (the safety net from the first section), so you can branch from it.

Naming a commit: ~ walks, ^ picks a parent

HEAD~2 and HEAD^2 look like one idea and are the most common navigation mistake in Git. ~ counts generations up the first-parent line: HEAD~1 is the parent, HEAD~2 the grandparent. ^ selects which parent of one commit: HEAD^ (the same as HEAD^1) is the first parent, and ^ and ^2 only diverge at a merge commit, where M^1 is the branch you were on and M^2 is the branch you merged in. So chaining first parents, HEAD^^ equals HEAD~2, while the jump to HEAD^2 does not walk two steps, it takes one step onto the second parent. Anchor on HEAD~2 = HEAD^^, never HEAD^2, per gitrevisions[16].

Deleting branches, and stashing

Two housekeeping moves round this out, and neither deletes a commit. git branch -d <b> refuses to delete a branch whose commits are not merged into your current HEAD; the lowercase form is the safety catch, aborting loudly on unmerged work, while git branch -D <b> (which is --delete --force) deletes regardless. Reading -d as "force delete" is the misconception that loses work, because it is the opposite. Deleting a branch removes a name, not the commits, so even a forced -D is usually recoverable through the reflog.

And when you are mid-edit on the wrong branch, git stash is the clean way out: it saves your uncommitted tracked changes, resets the working tree to HEAD so you can switch away, and git stash pop reapplies the shelved work and drops the stash. One trap: pop normally removes the entry, but if reapplying conflicts it reports the conflict and keeps the stash on the stack so you cannot lose it (remove it by hand with git stash drop after resolving). Like the reflog, the stash lives inside your own .git/ and is never pushed, so a teammate cannot see it. See git branch[17], git switch[18], and git stash[19].

C1 C2 M (merge) F2 merged-in tip F1 M^ = M~1 M^^ = M~2 M^2 parent
From merge commit M, ^ and ~ walk the first-parent line to C2 and C1 while ^2 steps onto the merged-in tip F2. Ancestry per gitrevisions.

Choosing the history operation

Criteriongit mergegit rebasegit cherry-pickgit revertgit reset
What it doesJoins two branchesReplays your commits onto a new baseCopies one commit's change hereAppends a commit's inverseMoves the branch pointer back
History effectAddsRewritesAddsAddsRewrites
Creates a new commit?Yes, unless fast-forwardReplays as new commitsYes, one per pickYes, an inverse commitNo, it removes commits
Safe on a shared branch?YesNoYesYesNo
Reach for it whenIntegrating a finished branchYou want linear local historyBackporting one fixUndoing a pushed commitDropping recent local commits

Decision tree

Copy a single commit here? Yes git cherry-pick -x keeps a link to origin No Undoing a commit's effect? No git merge rebase first for a linear history Yes Already pushed / shared? Yes git revert adds an inverse commit; safe on shared No git reset (--soft / --mixed / --hard) local commits only; --hard discards Lost a commit to a reset or rebase? git reflog recovers it committed states only — uncommitted work erased by --hard is gone

Sharp facts the exam loves — give these one last read before exam day.

Cheat sheet

Sharp facts the exam loves — scan these before test day.

Fast-forward vs three-way merge

git merge <branch> fast-forwards (just advances the pointer, no merge commit) when the current branch is a direct ancestor of the target; otherwise it performs a three-way merge that creates a merge commit with two parents. git merge --no-ff forces a merge commit even when a fast-forward is possible.

Trap Claiming a fast-forward merge always records a merge commit.

1 question tests this
Reading and resolving conflict markers

A merge conflict writes both versions into the file delimited by <<<<<<< HEAD (current branch), ======= (separator), and >>>>>>> <branch> (incoming); the engineer edits to the desired content, deletes all markers, git adds the file, then git commit to finish the merge.

Trap Believing the middle section between ======= and >>>>>>> is your own branch's version.

4 questions test this
Aborting an in-progress merge

git merge --abort restores the working tree and index to the exact state that existed before the merge began, safely discarding a conflicted, in-progress merge without needing a manual reset.

Trap Assuming git reset --hard is the only way to cancel a conflicted merge.

1 question tests this
Conflicted paths reported as Unmerged

While a merge is conflicted, git status lists the files under "Unmerged paths" (both modified); the merge is not complete and cannot be committed until every conflicted path is resolved and staged with git add.

4 questions test this
Resolving a file with --ours or --theirs

During a conflict, git checkout --ours <file> keeps the current branch's version and git checkout --theirs <file> keeps the incoming branch's version, resolving that file wholesale (useful for generated or binary files) before staging it.

Trap Thinking --ours refers to the branch being merged in rather than the current branch.

2 questions test this
git merge --squash stages without committing

git merge --squash <branch> applies the branch's combined changes to the working tree and stages them but does NOT create a merge commit or record the source branch as a parent; the engineer then makes a single ordinary git commit.

Trap Expecting --squash to create the commit automatically.

1 question tests this
Squashing with git reset --soft

git reset --soft HEAD~3 moves the branch tip back three commits while keeping all their changes staged, so one git commit collapses them into a single commit; done on a local, unpushed feature branch this does not rewrite shared history.

Trap Thinking reset --soft discards the changes from the collapsed commits.

2 questions test this
Interactive rebase squash vs fixup

git rebase -i HEAD~3 opens an editor listing the commits; changing pick to squash (s) merges a commit into the prior one and lets you edit the combined message, while fixup (f) merges but discards the squashed commit's message.

Trap Swapping the roles: believing fixup keeps the message and squash drops it.

1 question tests this
Squash only unpushed commits

Squashing rewrites commit SHAs, so squashing commits that were already pushed and shared requires a force push and diverges collaborators' history; squash only local commits that have not been pushed.

Trap Assuming squashing already-pushed commits is safe without a force push.

2 questions test this
cherry-pick copies a commit's changes

git cherry-pick <commit> creates a NEW commit on the current branch that reproduces the changes introduced by the named commit, with a new SHA; the original commit stays where it is (it is copied, not moved).

Trap Thinking cherry-pick moves the commit off its original branch.

2 questions test this
cherry-pick -n stages without committing

git cherry-pick -n (--no-commit) applies the picked commit's changes to the working tree and index but stops before committing, allowing several picks to be combined into one commit.

2 questions test this
cherry-pick commit ranges are exclusive of the start

git cherry-pick A..B picks the commits after A up to and including B — commit A itself is excluded; use A^..B to also include A.

Trap Assuming A..B includes commit A.

1 question tests this
Resuming cherry-pick after a conflict

On a conflict, cherry-pick pauses; resolve the files, git add them, then run git cherry-pick --continue (or --abort to cancel entirely, --skip to drop the current commit).

1 question tests this
cherry-pick -x records provenance

git cherry-pick -x appends a "(cherry picked from commit )" line to the new commit message, recording where the change came from — common when backporting a fix to a release branch.

reset --soft keeps changes staged

git reset --soft <ref> moves HEAD (and the current branch) to but leaves the index and working tree untouched, so the changes from the undone commits remain staged and ready to re-commit.

Trap Believing --soft discards working-tree changes.

6 questions test this
reset --mixed (default) unstages changes

git reset --mixed <ref> — the default when no mode is given — moves HEAD and resets the index to match but leaves the working tree alone, so the changes become unstaged (modified) but are not lost.

Trap Thinking the default reset deletes files from the working tree.

6 questions test this
reset --hard discards working tree

git reset --hard <ref> moves HEAD and forcibly overwrites BOTH the index and the working tree to match , permanently discarding uncommitted changes and the work in the reset-away commits.

Trap Assuming --hard keeps the removed commits' changes staged like --soft.

5 questions test this
reset with a path unstages a file

git reset <file> (i.e. git reset HEAD <file>) unstages that file — the inverse of git add — without changing its working-tree content and without moving HEAD.

2 questions test this
Recovering commits after a reset

Commits orphaned by a reset remain reachable through git reflog until garbage collection; running git reset --hard <sha-from-reflog> restores the branch to a mistakenly-reset commit.

Trap Believing reset --hard is immediately and permanently unrecoverable.

checkout switches branches

git checkout <branch> updates HEAD, the index, and the working tree to the branch tip; git checkout -b <new> creates a new branch and switches to it in one command.

4 questions test this
checkout of a path discards local edits

git checkout -- <file> discards uncommitted working-tree changes to that file by restoring it from the index/HEAD (the modern equivalent is git restore <file>); it overwrites local edits, it does not stage them.

Trap Thinking checkout of a file path stages the file for commit.

2 questions test this
Checking out a commit detaches HEAD

git checkout <commit-sha> (a commit, not a branch) puts the repo in DETACHED HEAD state; new commits made there belong to no branch and become unreachable once you switch away unless you first create a branch to hold them.

Trap Assuming commits made in detached HEAD stay attached to the previously checked-out branch.

3 questions test this
switch and restore split checkout's roles

Modern Git separates checkout's overloaded behavior: git switch <branch> (with -c to create) changes branches and git restore <file> restores file contents, removing the ambiguity of a single overloaded command.

revert adds an inverse commit

git revert <commit> creates a NEW commit that applies the inverse of the named commit's changes, undoing its effect while preserving history — making it the safe way to undo a commit that has already been pushed and pulled by others.

Trap Thinking revert deletes or rewrites the original commit.

4 questions test this
revert vs reset for shared history

git reset rewrites history and is unsafe once commits are pushed, whereas git revert is additive and non-destructive; on a shared branch you undo a bad commit with revert, not reset.

Trap Recommending reset as the safe way to undo an already-pushed commit.

2 questions test this
Reverting a merge needs -m

Reverting a merge commit requires -m <parent-number> (e.g. git revert -m 1 <merge-sha>) to name which parent is the mainline, because a merge has two parents and Git cannot infer which side to preserve.

Trap Assuming a merge commit can be reverted without specifying a mainline parent.

2 questions test this
revert -n stages without committing

git revert -n <commit> stages the inverse changes without creating the commit, letting several reverts be combined into a single commit.

rebase replays commits onto a new base

git rebase <base> replays the current branch's commits one at a time on top of , producing a linear history with NEW SHAs, versus git merge which preserves branch topology by adding a merge commit.

Trap Believing rebase preserves the original commit SHAs.

4 questions test this
The golden rule of rebasing

Never rebase commits that have been pushed and that others may have based work on: rebasing rewrites SHAs, forcing every collaborator to reconcile diverged history. Rebase only local, unshared commits.

Trap Claiming rebasing a shared branch is fine as long as you force-push.

2 questions test this
Resolving conflicts during a rebase

On a rebase conflict Git pauses at the offending commit; resolve, git add, then git rebase --continue. git rebase --abort returns to the pre-rebase state and --skip drops the current commit.

2 questions test this
git pull --rebase keeps history linear

git pull --rebase fetches the upstream and rebases local commits on top of it instead of creating a merge commit, keeping a feature branch's history linear when syncing with the remote.

HEAD~n vs HEAD^n notation

HEAD~n follows the first parent n generations back, while HEAD^ is the first parent and HEAD^2 the SECOND parent of a merge; thus HEAD~2 equals HEAD^^ but is not the same as HEAD^2.

Trap Treating HEAD~2 and HEAD^2 as equivalent.

3 questions test this
reflog records every HEAD movement

git reflog records every movement of HEAD — commits, resets, checkouts, rebases, merges — and is the primary recovery tool for finding "lost" commits after a bad reset or rebase.

1 question tests this
branch -d vs -D

git branch -d <b> refuses to delete a branch that is not merged into the current HEAD (guarding against losing work), whereas git branch -D <b> force-deletes regardless of merge state.

Trap Thinking lowercase -d force-deletes an unmerged branch.

1 question tests this
git stash shelves uncommitted work

git stash saves uncommitted tracked changes and resets the working tree to HEAD; git stash pop reapplies and drops the most recent stash — used to switch branches without committing work in progress.

References

  1. https://git-scm.com/docs/git-reflog
  2. https://git-scm.com/docs/git-gc
  3. https://git-scm.com/docs/git-merge
  4. https://git-scm.com/book/en/v2/Git-Branching-Basic-Branching-and-Merging
  5. https://git-scm.com/docs/git-status
  6. https://git-scm.com/docs/git-cherry-pick
  7. https://git-scm.com/docs/git-revert
  8. https://git-scm.com/docs/git-reset
  9. https://git-scm.com/book/en/v2/Git-Tools-Reset-Demystified
  10. https://git-scm.com/docs/git-restore
  11. https://git-scm.com/docs/git-push
  12. https://docs.gitlab.com/user/project/merge_requests/squash_and_merge/
  13. https://git-scm.com/docs/git-rebase
  14. https://git-scm.com/book/en/v2/Git-Branching-Rebasing
  15. https://git-scm.com/docs/git-pull
  16. https://git-scm.com/docs/gitrevisions
  17. https://git-scm.com/docs/git-branch
  18. https://git-scm.com/docs/git-switch
  19. https://git-scm.com/docs/git-stash