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.
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.
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.
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.
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.
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~3is the fastest: it moves the branch tip back three commits while keeping all their changes staged, so onegit commitrecords them as a single commit. This is the same--softfrom 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 ordinarygit commityourself. Expecting--squashto commit for you leaves you hunting for a commit that was never made.git rebase -i HEAD~3opens an editor listing the commits, each prefixedpick; change a line tosquashto fold that commit into the one above it and keep its message for you to edit, orfixupto fold it in and discard its message. Inverting that pair, thinkingfixupkeeps the message andsquashdrops 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.
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.
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].
Choosing the history operation
| Criterion | git merge | git rebase | git cherry-pick | git revert | git reset |
|---|---|---|---|---|---|
| What it does | Joins two branches | Replays your commits onto a new base | Copies one commit's change here | Appends a commit's inverse | Moves the branch pointer back |
| History effect | Adds | Rewrites | Adds | Adds | Rewrites |
| Creates a new commit? | Yes, unless fast-forward | Replays as new commits | Yes, one per pick | Yes, an inverse commit | No, it removes commits |
| Safe on a shared branch? | Yes | No | Yes | Yes | No |
| Reach for it when | Integrating a finished branch | You want linear local history | Backporting one fix | Undoing a pushed commit | Dropping recent local commits |
Decision tree
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-ffforces a merge commit even when a fast-forward is possible.Trap Claiming a fast-forward merge always records a merge commit.
- 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, thengit committo finish the merge.Trap Believing the middle section between ======= and >>>>>>> is your own branch's version.
4 questions test this
- Midway through a three-way merge, an engineer edits a few conflicted files but has not finished the others. They run 'git status' to understand the repository state before attempting to commit. Which
- While merging feature/telemetry into main, only a generated lock file conflicts. The engineer has determined the incoming branch's version of that file is authoritative and wants to take it wholesale
- While merging feature/acl into main, an engineer edits one conflicted role file to its final content but has not yet run any further commands. They attempt git commit and it is rejected, and git statu
- During a merge of feature/nat into main, an engineer opens a conflicted playbook and sees a region delimited by <<<<<<< HEAD, then =======, then >>>>>>> feature/nat. They decide on the exact combined
- Aborting an in-progress merge
git merge --abortrestores 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.
- Conflicted paths reported as Unmerged
While a merge is conflicted,
git statuslists the files under "Unmerged paths" (both modified); the merge is not complete and cannot be committed until every conflicted path is resolved and staged withgit add.4 questions test this
- Midway through a three-way merge, an engineer edits a few conflicted files but has not finished the others. They run 'git status' to understand the repository state before attempting to commit. Which
- While merging feature/telemetry into main, only a generated lock file conflicts. The engineer has determined the incoming branch's version of that file is authoritative and wants to take it wholesale
- While merging feature/acl into main, an engineer edits one conflicted role file to its final content but has not yet run any further commands. They attempt git commit and it is rejected, and git statu
- During a merge of feature/nat into main, an engineer opens a conflicted playbook and sees a region delimited by <<<<<<< HEAD, then =======, then >>>>>>> feature/nat. They decide on the exact combined
- Resolving a file with --ours or --theirs
During a conflict,
git checkout --ours <file>keeps the current branch's version andgit 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
- While merging feature/telemetry into main, only a generated lock file conflicts. The engineer has determined the incoming branch's version of that file is authoritative and wants to take it wholesale
- During a merge of feature/logging into main, only a generated config file, config.yaml, conflicts. The engineer determines that main's current version of this file is authoritative and wants to keep i
- 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 ordinarygit commit.Trap Expecting --squash to create the commit automatically.
- Squashing with git reset --soft
git reset --soft HEAD~3moves the branch tip back three commits while keeping all their changes staged, so onegit commitcollapses 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
- A developer has three unpushed commits on the local branch feature/ospf-timers and wants to collapse them into one commit with a new message before opening a merge request, preserving the exact combin
- An engineer squashed six commits on the shared branch feature/telemetry that had already been pushed and reviewed by teammates. Afterward git push is rejected as a non-fast-forward update. Which state
- Interactive rebase squash vs fixup
git rebase -i HEAD~3opens an editor listing the commits; changingpicktosquash(s) merges a commit into the prior one and lets you edit the combined message, whilefixup(f) merges but discards the squashed commit's message.Trap Swapping the roles: believing fixup keeps the message and squash drops it.
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
- A developer has three unpushed commits on the local branch feature/ospf-timers and wants to collapse them into one commit with a new message before opening a merge request, preserving the exact combin
- An engineer squashed six commits on the shared branch feature/telemetry that had already been pushed and reviewed by teammates. Afterward git push is rejected as a non-fast-forward update. Which state
- 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
- An engineer on branch main runs git cherry-pick 4d21ab to copy a syslog fix that currently exists only on branch dev. The command completes cleanly and a new commit appears on main. A teammate asks wh
- An engineer cherry-picks commit 9f2a1c from branch hotfix onto main to port a syslog fix, running git cherry-pick 9f2a1c which completes cleanly. A colleague asks whether the new commit on main is the
- 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
- An engineer wants to port a single Terraform variable fix from branch dev onto main, but must first edit the commit message and inspect the staged diff before it is recorded as a commit. Which approac
- An engineer runs git cherry-pick -n on two commits from branch feature-a onto main, intending to release them together as one logical change. Both picks apply without conflict. Running git status show
- cherry-pick commit ranges are exclusive of the start
git cherry-pick A..Bpicks the commits after A up to and including B — commit A itself is excluded; useA^..Bto also include A.Trap Assuming A..B includes commit A.
- Resuming cherry-pick after a conflict
On a conflict, cherry-pick pauses; resolve the files,
git addthem, then rungit cherry-pick --continue(or--abortto cancel entirely,--skipto drop the current commit).- cherry-pick -x records provenance
git cherry-pick -xappends 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
- An engineer on branch feature/vlans has two local commits that add Terraform VLAN resources plus edits to a variables file, none of them pushed. Wanting to restructure the history before opening a mer
- A developer on a local branch has one committed-but-flawed Terraform commit, several unrelated uncommitted edits in the working tree, and two files already staged with git add. Frustrated, the develop
- An engineer needs to correct only the commit message on the most recent local commit, which was never pushed, without altering the tree it recorded. The plan is to run git reset --soft HEAD~1 and then
- A developer on a local, unpushed branch runs git reset --soft <hash>, where <hash> is an ancestor commit four commits back, intending to reshape the history before opening a merge request. No files ar
- After committing a batch of Ansible interface edits locally, an engineer decides the commit was premature and runs git reset HEAD~1 with no mode option specified. The engineer wants to keep the edited
- A developer has uncommitted edits to an Ansible OSPF playbook in the working tree, plus one committed-but-broken commit on a local branch that was never pushed. Intending only to drop the broken commi
- 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
- An engineer on branch feature/vlans has two local commits that add Terraform VLAN resources plus edits to a variables file, none of them pushed. Wanting to restructure the history before opening a mer
- An engineer ran git add on templates/uplink.j2 to stage it, then realized more edits are needed and the file should be committed separately later. The engineer runs git reset templates/uplink.j2 with
- An engineer needs to correct only the commit message on the most recent local commit, which was never pushed, without altering the tree it recorded. The plan is to run git reset --soft HEAD~1 and then
- A developer on a local, unpushed branch runs git reset --soft <hash>, where <hash> is an ancestor commit four commits back, intending to reshape the history before opening a merge request. No files ar
- After committing a batch of Ansible interface edits locally, an engineer decides the commit was premature and runs git reset HEAD~1 with no mode option specified. The engineer wants to keep the edited
- A developer has uncommitted edits to an Ansible OSPF playbook in the working tree, plus one committed-but-broken commit on a local branch that was never pushed. Intending only to drop the broken commi
- 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
- An engineer on branch feature/vlans has two local commits that add Terraform VLAN resources plus edits to a variables file, none of them pushed. Wanting to restructure the history before opening a mer
- A developer on a local branch has one committed-but-flawed Terraform commit, several unrelated uncommitted edits in the working tree, and two files already staged with git add. Frustrated, the develop
- A developer on a local, unpushed branch runs git reset --soft <hash>, where <hash> is an ancestor commit four commits back, intending to reshape the history before opening a merge request. No files ar
- After committing a batch of Ansible interface edits locally, an engineer decides the commit was premature and runs git reset HEAD~1 with no mode option specified. The engineer wants to keep the edited
- A developer has uncommitted edits to an Ansible OSPF playbook in the working tree, plus one committed-but-broken commit on a local branch that was never pushed. Intending only to drop the broken commi
- reset with a path unstages a file
git reset <file>(i.e.git reset HEAD <file>) unstages that file — the inverse ofgit add— without changing its working-tree content and without moving HEAD.2 questions test this
- An engineer ran git add on templates/uplink.j2 to stage it, then realized more edits are needed and the file should be committed separately later. The engineer runs git reset templates/uplink.j2 with
- An engineer ran git add -A in a network-automation repo, staging edited playbooks together with a build/report.html file that a linter generated and that must stay out of this commit. No commit has be
- Recovering commits after a reset
Commits orphaned by a reset remain reachable through
git refloguntil garbage collection; runninggit 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
- An engineer working on branch feature/acl needs the roles/ospf/tasks/main.yml file exactly as it exists on branch main, but must not switch away from feature/acl or disturb the other uncommitted edits
- To reproduce a bug present in an older device configuration, an engineer runs 'git checkout a1b2c3d', where a1b2c3d is a commit SHA rather than a branch name. Git prints a note about the new state. Wh
- An engineer working on branch main has committed an Ansible playbook. To add VLAN tasks, they run 'git checkout feature/vlans' and notice several files in the working directory change immediately. Ass
- After entering detached HEAD state to test a fix, an engineer creates two commits, then runs 'git checkout main'. Git warns that the two new commits will become unreachable. Which action, taken before
- 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 isgit 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
- An engineer working on branch feature/acl needs the roles/ospf/tasks/main.yml file exactly as it exists on branch main, but must not switch away from feature/acl or disturb the other uncommitted edits
- An engineer edited templates/vlan.j2 while building a RESTCONF payload generator but decides the edits are wrong. They want to discard only the uncommitted working-tree changes to that single file and
- 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
- To reproduce a bug present in an older device configuration, an engineer runs 'git checkout a1b2c3d', where a1b2c3d is a commit SHA rather than a branch name. Git prints a note about the new state. Wh
- An engineer working on branch main has committed an Ansible playbook. To add VLAN tasks, they run 'git checkout feature/vlans' and notice several files in the working directory change immediately. Ass
- After entering detached HEAD state to test a fix, an engineer creates two commits, then runs 'git checkout main'. Git warns that the two new commits will become unreachable. Which action, taken before
- switch and restore split checkout's roles
Modern Git separates checkout's overloaded behavior:
git switch <branch>(with-cto create) changes branches andgit 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
- A GitLab CE repository enforces an audit policy that the full commit history of the shared main branch must never be rewritten, because compliance requires an immutable record of every change. A commi
- An engineer runs git revert on a commit that was already pushed to the shared main branch of a network-automation repository, expecting the offending commit to disappear from git log. Instead a new co
- An automation repository's main branch has a linear history of eight pushed commits. The third commit from the tip added a faulty ACL line to a Jinja2 template, but the four commits after it are corre
- An engineer attempts git revert <merge-sha> against a merge commit on the shared release branch and Git refuses, reporting that the commit has more than one parent. The merge brought a hotfix branch i
- revert vs reset for shared history
git resetrewrites history and is unsafe once commits are pushed, whereasgit revertis 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
- A GitLab CE repository enforces an audit policy that the full commit history of the shared main branch must never be rewritten, because compliance requires an immutable record of every change. A commi
- An engineer runs git revert on a commit that was already pushed to the shared main branch of a network-automation repository, expecting the offending commit to disappear from git log. Instead a new co
- 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
- A merge commit on the main branch of an automation repository combined a feature branch into main; main was the checked-out branch when the merge was created. An engineer plans to revert the merge and
- An engineer attempts git revert <merge-sha> against a merge commit on the shared release branch and Git refuses, reporting that the commit has more than one parent. The merge brought a hotfix branch i
- 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, versusgit mergewhich preserves branch topology by adding a merge commit.Trap Believing rebase preserves the original commit SHAs.
4 questions test this
- A CI pipeline requires a strictly linear commit history with no merge commits. An engineer has three unpushed local commits on feature/vlan and wants to place them on top of the latest origin/main bef
- An engineer has two unpushed local commits on feature/vlan while origin/main has advanced with new upstream work. Team policy forbids merge commits in the integrated history. The engineer wants to pul
- Two engineers share the branch feature/ospf on the origin remote and both have local work based on its current commits. One engineer rebases feature/ospf onto an updated main and force-pushes the rewr
- An engineer on branch feature/acl runs 'git rebase main'. A colleague working from an identical starting point instead runs 'git merge main' on their own copy of feature/acl. Comparing the two resulti
- 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
- A CI pipeline requires a strictly linear commit history with no merge commits. An engineer has three unpushed local commits on feature/vlan and wants to place them on top of the latest origin/main bef
- Two engineers share the branch feature/ospf on the origin remote and both have local work based on its current commits. One engineer rebases feature/ospf onto an updated main and force-pushes the rewr
- Resolving conflicts during a rebase
On a rebase conflict Git pauses at the offending commit; resolve,
git add, thengit rebase --continue.git rebase --abortreturns to the pre-rebase state and--skipdrops the current commit.2 questions test this
- During git rebase main on a local branch, Git stops and reports a conflict in acl.yml at the second replayed commit. The engineer edits acl.yml to the correct content. Which sequence correctly finishe
- An engineer starts git rebase main and hits a series of complex conflicts in several files. Rather than resolve them now, the engineer wants to completely undo the rebase and return the branch to exac
- git pull --rebase keeps history linear
git pull --rebasefetches 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~nfollows the first parent n generations back, whileHEAD^is the first parent andHEAD^2the SECOND parent of a merge; thusHEAD~2equalsHEAD^^but is not the same asHEAD^2.Trap Treating HEAD~2 and HEAD^2 as equivalent.
3 questions test this
- An engineer maintains an IaC repository where three feature branches were integrated with a single octopus merge commit that now sits at HEAD. That merge commit has three parents: main is the first pa
- In a rebase runbook an engineer wants a revision expression that always resolves to the same commit as HEAD^^ - two steps back along the first-parent line from the current commit - and is documenting
- An engineer merges the branch feature/acl into main, producing a merge commit that now sits at HEAD. During review they must inspect the tip commit of feature/acl exactly as it was brought in - the se
- reflog records every HEAD movement
git reflogrecords 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.- 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), whereasgit branch -D <b>force-deletes regardless of merge state.Trap Thinking lowercase -d force-deletes an unmerged branch.
- git stash shelves uncommitted work
git stashsaves uncommitted tracked changes and resets the working tree to HEAD;git stash popreapplies and drops the most recent stash — used to switch branches without committing work in progress.
References
- https://git-scm.com/docs/git-reflog
- https://git-scm.com/docs/git-gc
- https://git-scm.com/docs/git-merge
- https://git-scm.com/book/en/v2/Git-Branching-Basic-Branching-and-Merging
- https://git-scm.com/docs/git-status
- https://git-scm.com/docs/git-cherry-pick
- https://git-scm.com/docs/git-revert
- https://git-scm.com/docs/git-reset
- https://git-scm.com/book/en/v2/Git-Tools-Reset-Demystified
- https://git-scm.com/docs/git-restore
- https://git-scm.com/docs/git-push
- https://docs.gitlab.com/user/project/merge_requests/squash_and_merge/
- https://git-scm.com/docs/git-rebase
- https://git-scm.com/book/en/v2/Git-Branching-Rebasing
- https://git-scm.com/docs/git-pull
- https://git-scm.com/docs/gitrevisions
- https://git-scm.com/docs/git-branch
- https://git-scm.com/docs/git-switch
- https://git-scm.com/docs/git-stash