Git conflicts, reflog, reset vs revert, and bisect
Resolve conflicts without guessing, recover work you thought you destroyed with reflog, and find the commit that broke it with bisect.
Conflicts, reflog, reset/revert/restore, and bisect
Functional dependencies — closure, candidate keys, minimal cover
The sentence to own: once something is committed, Git essentially cannot lose it for about ninety days — so the fear that makes people avoid rebase, reset and history editing is unfounded, and
git reflogis the command that removes it.Today has two halves. Recovery — undo anything, safely. And
bisect— find the commit that broke it inlog₂(n)steps instead of reading a thousand diffs.
Part 1 — Resolving a conflict
<<<<<<< HEAD what is on the branch you are ON
limit = 100
======= the divider
limit = int(os.environ["LIMIT"])
>>>>>>> feature/config what is coming IN
git status # lists exactly the unmerged paths — read it first
git diff # during a conflict, shows a COMBINED diff
git checkout --ours file # take one whole side · --theirs the other
git checkout --merge file # put the markers back if you mangled it
git add file # "add" is how you say RESOLVED
git merge --continue # or `git rebase --continue`
git merge --abort # back to before. ALWAYS available.
⚠️⚠️ THE TRAP: "OURS" AND "THEIRS" INVERT DURING A REBASE.
MERGE: you are on main, merging feature ⇒ ours = main theirs = feature ✔ intuitive
REBASE: Git replays YOUR commits ON TOP OF the upstream ⇒ the upstream is
checked out and yours is the incoming patch ⇒ ours = UPSTREAM,
theirs = YOUR COMMIT. Exactly backwards from what the words suggest.
⇒ SO NEVER RESOLVE BY REACHING FOR --ours/--theirs FROM MEMORY. Read the
content. `git status` names the operation in progress at the top —
check it before you choose a side.
Resolve by understanding both intentions, not by picking a winner. The most common real resolution is neither side — it is the change both authors would have written had they known about each other. And after resolving: run the tests. A conflict resolution is a code change nobody reviewed.
git config --global rerere.enabled true # REuse REcorded REsolution
rerere records how you resolved a conflict and replays it automatically the next time the
same one appears — which is precisely what makes rebasing a long branch, or repeatedly merging main
into a feature branch, survivable (Day 086).
Part 2 — reset, revert, restore — three trees again
RESET MOVES THE BRANCH POINTER, AND THEN OPTIONALLY THE OTHER TWO TREES
(Day 085's diagram is the whole explanation):
git reset --soft HEAD~1 ⇒ HEAD moves. Index and working tree UNTOUCHED.
⇒ "uncommit, keep everything staged" —
the redo-my-last-commit move.
git reset --mixed HEAD~1 ⇒ HEAD + INDEX move. Working tree untouched. (default)
⇒ "uncommit and unstage; my edits are still there"
git reset --hard HEAD~1 ⇒ ALL THREE. ⚠️⚠️ YOUR EDITS ARE GONE.
⇒ ONLY --hard DESTROYS ANYTHING, and only UNCOMMITTED work. Committed
work survives even --hard (Part 3). Uncommitted work is the only
thing in Git that is genuinely at risk — which is an argument for
committing early and cleaning up later (Day 086).
| Want | Command |
|---|---|
| Undo the last commit, keep the changes staged | git reset --soft HEAD~1 |
| Unstage a file | git restore --staged f |
| Discard an uncommitted edit | ⚠ git restore f — irreversible |
| Get a file as it was two commits ago | git restore --source=HEAD~2 f |
| Undo a pushed commit | git revert <sha> |
| Throw away local commits on my own branch | git reset --hard origin/main |
| Park work without committing | git stash push -m "wip" · git stash pop |
REVERT vs RESET — THE DECISION IS ABOUT WHO ELSE HAS IT, AND NOTHING ELSE:
RESET rewrites history ⇒ only for commits nobody else has (Day 086's rule)
REVERT makes a NEW commit that applies the INVERSE diff
⇒ history is preserved, nothing is rewritten, no force-push,
⇒ SAFE ON A SHARED BRANCH. This is the production answer.
⇒ git revert -m 1 <merge-sha> to revert a merge (-m 1 = keep the first parent)
⇒ AND THE HONEST NOTE: reverting a merge means the branch cannot simply be
re-merged later — Git thinks it is already in. You revert the revert.
⚠ git stash is not a filing cabinet. It is an unnamed stack that is easy to forget and easy
to lose track of; git stash list after two weeks is a graveyard. A WIP commit on a branch is
almost always better — it has a message, it is visible, and Day 086's rebase -i erases it later.
Part 3 — reflog — the reason nothing is lost
$ git reflog
c2a3e19 HEAD@{0}: reset: moving to HEAD~3 the mistake
8f3a1cd HEAD@{1}: commit: Add rate limiter the commit I thought I destroyed
4b7d2e0 HEAD@{2}: rebase finished: returning to refs/heads/feature
...
$ git reset --hard HEAD@{1} # back. Completely.
WHAT THE REFLOG IS: A LOCAL LOG OF EVERY POSITION HEAD HAS HELD — commits,
checkouts, resets, rebases, merges — kept for 90 days by default.
⇒ AND DAY 084 EXPLAINS WHY IT WORKS: commits are objects in a content-
addressed store. `reset --hard` moved a POINTER; it did not delete
anything. The commit is still in .git/objects — merely UNREACHABLE.
The reflog is a second set of references that keeps it reachable.
⇒ WHAT IT RESCUES:
· a reset --hard that ate three commits ⇒ reset --hard HEAD@{1}
· a deleted branch ⇒ git switch -c name <sha>
· a rebase that mangled everything ⇒ reset --hard to pre-rebase
· an amend that lost the original message ⇒ it is in there
· commits made on a detached HEAD (Day 084) ⇒ still listed
⇒ ⚠️ WHAT IT CANNOT RESCUE: work that was NEVER COMMITTED. `git restore`
on an unsaved edit, or a `reset --hard` over uncommitted changes, is gone.
THAT is the one real way to lose work in Git — and it is the argument
for committing early.
git reflog show feature # the reflog of ONE branch
git fsck --lost-found # dangling commits even the reflog forgot
git gc --prune=now # ⚠️ THIS is what finally deletes them. Rare.
Part 4 — bisect — binary search over history
git bisect start
git bisect bad # current commit is broken
git bisect good v1.4.0 # this tag was fine
# Git checks out the midpoint. You test. You answer:
git bisect good | git bisect bad
# …repeat ~log₂(n) times…
# ⇒ "abc1234 is the first bad commit"
git bisect reset # back to where you started
THE NUMBERS ARE THE ARGUMENT:
1,000 commits ⇒ 10 TESTS. 10,000 commits ⇒ 14 tests.
⇒ reading diffs is O(n); bisect is O(log n). There is no debugging
technique with a better ratio of effort to certainty, and almost nobody
reaches for it.
# AUTOMATED — the version you should actually use:
git bisect start HEAD v1.4.0
git bisect run pytest tests/test_orders.py -x -q
# exit 0 = good · non-zero = bad · exit 125 = SKIP (cannot test this commit)
# ⇒ walk away. It prints the culprit.
BISECT IS WHERE DAY 085 GETS PAID BACK, AND THIS IS THE ARGUMENT TO MAKE:
bisect requires that EVERY COMMIT BUILDS AND RUNS. ⇒ therefore "wip",
"fix typo" and half-finished commits are not an aesthetic problem — they
BREAK YOUR MOST POWERFUL DEBUGGING TOOL, at the exact moment you need it.
⇒ and a commit that mixes a refactor with a fix tells you the WHAT but
not the WHY, so even a successful bisect lands you in a 400-line diff.
⇒ ATOMIC COMMITS ARE AN INVESTMENT THAT PAYS OUT ONCE, HUGELY, AT 2 A.M.
| Also for finding things | |
|---|---|
git log -S "func_name" |
the pickaxe — commits where that string's count changed |
git log -G "regex" |
commits whose diff matches |
git log -p -- path/f.py |
the full history of one file |
git blame -w -C -M f.py |
ignore whitespace, detect moved/copied lines |
git blame -L 40,60 f.py |
one region |
git log --oneline --graph --all |
the shape of the repo |
git log -S beats blame more often than people expect. blame shows who last touched a
line — frequently a formatting pass. The pickaxe finds the commit that introduced or removed the
string, which is usually the commit you actually wanted.
Part 5 — D-04 · Functional dependencies
X → Y ("X DETERMINES Y") MEANS: any two rows agreeing on X must agree on Y.
⇒ AN FD IS A BUSINESS RULE, NOT AN OBSERVATION ABOUT TODAY'S DATA.
"no two employees share an id" is a rule; "no two share a salary" is a
coincidence. Reading FDs off sample data is the classic beginner error.
ARMSTRONG'S AXIOMS — three rules, everything else follows:
REFLEXIVITY Y ⊆ X ⇒ X → Y (trivial)
AUGMENTATION X → Y ⇒ XZ → YZ
TRANSITIVITY X → Y and Y → Z ⇒ X → Z the one that causes anomalies
ATTRIBUTE CLOSURE X⁺ — "everything X determines". THE ALGORITHM, and it is the
only piece of theory here you will actually run by hand in an interview:
start: X⁺ = X
repeat: if some FD A → B has A ⊆ X⁺, add B to X⁺
until nothing changes
⇒ IF X⁺ = ALL ATTRIBUTES, X IS A SUPERKEY.
⇒ IF NO PROPER SUBSET OF X IS ALSO A SUPERKEY, X IS A CANDIDATE KEY (Day 081).
WORKED — R(A,B,C,D) with A→B, B→C, CD→A
A⁺ = A,B,C ⇒ not all ⇒ A is not a key
AD⁺ = A,D,B,C ⇒ ALL ⇒ AD is a superkey. A alone isn't, D alone isn't
⇒ AD IS A CANDIDATE KEY
BD⁺ = B,D,C,A ⇒ all ⇒ BD is also a candidate key
⇒ CD⁺ = C,D,A,B ⇒ CD too. THREE candidate keys — perfectly normal, and
it is why "the primary key" is a CHOICE (Day 081) rather than a discovery.
Minimal (canonical) cover — the smallest equivalent FD set: split right-hand sides to single attributes, remove redundant attributes from left-hand sides, drop FDs implied by the rest. It matters because it is the input to normalization (D-05, Day 091): an FD whose determinant is not a superkey is exactly a BCNF violation, and exactly the source of update anomalies.
Common mistakes
| Mistake | Correction |
|---|---|
Believing work is lost after reset --hard |
git reflog. Committed work survives ~90 days. |
--ours/--theirs from memory in a rebase |
They invert. Read the content. |
| Resolving a conflict without running tests | It is an unreviewed code change. |
reset --hard on a shared branch |
Use revert — it is a new commit, safe to push. |
reset vs revert confusion |
Rewrite vs new commit. The question is who else has it. |
Living in git stash |
A WIP commit has a name and is visible. |
| Reading 200 diffs to find a regression | git bisect run — 8 tests instead. |
| Broken intermediate commits | They break bisect when you need it most. |
blame for "who introduced this" |
git log -S — blame shows the last formatter. |
| Reading FDs off sample data | An FD is a rule, not an observation. |
| Assuming one candidate key | Several is normal; the primary key is chosen. |
Interview questions
Q: You ran git reset --hard and lost three commits. What now?
git reflog, find the position before the reset, andgit reset --hard HEAD@{1}. Nothing was deleted — commits are objects in a content-addressed store, and the reset only moved a pointer, so those commits were unreachable rather than gone. The reflog is a second set of references that keeps every position HEAD has held for about ninety days. The one thing it can't recover is work that was never committed, which is the real argument for committing early and tidying up with an interactive rebase later.
Q: reset or revert?
It depends on one thing: has anyone else got the commit?
resetmoves the branch pointer and rewrites history, so it's for local commits only.revertcreates a new commit applying the inverse diff, which changes nothing about existing history and needs no force-push, so it's the answer on any shared branch. In production, revert — every time. The wrinkle worth knowing is that reverting a merge means the branch can't simply be re-merged later, because Git believes it's already integrated; you end up reverting the revert.
Q: How do you find which commit introduced a regression?
git bisect. Mark a known-good tag and the current bad commit and Git binary-searches: a thousand commits is ten tests, ten thousand is fourteen. And I'd automate it —git bisect run pytest -xwalks the whole thing and prints the culprit while I do something else, using exit 125 to skip commits that can't be built. The prerequisite is that every commit builds and passes, which is the concrete reason atomic commits matter: "wip" commits don't just look untidy, they break your best debugging tool at the moment you need it.
Q: Why do --ours and --theirs swap meaning during a rebase?
Because a rebase replays your commits on top of the upstream, so the upstream branch is the one checked out and your commit is the incoming patch. "Ours" is whatever's currently checked out, which during a rebase is the upstream — the opposite of what the words suggest. So I never resolve from memory; I read the content, and
git statusnames the operation in progress at the top if I've lost track.
Q: What is a functional dependency, and how do you find candidate keys?
X → Y means any two rows agreeing on X must agree on Y — and it's a business rule, not something you read off sample data. To find candidate keys you compute attribute closure: start with the attribute set, repeatedly apply any FD whose left side is already contained, and stop when nothing changes. If the closure is every attribute it's a superkey; if no proper subset is also a superkey, it's a candidate key. There are often several, which is exactly why the primary key is a choice rather than a discovery. And the reason this matters is normalization: an FD whose determinant isn't a superkey is a BCNF violation, and that's precisely where update anomalies come from.
Mini task
- Create a conflict, resolve it by writing a third version that satisfies both intentions, and run the tests.
- Cause the same conflict twice with
rerere.enabled trueand watch the second resolve itself. - In a rebase conflict, check
git statusand identify which side is "ours". Predict before looking. - Commit three times,
git reset --soft HEAD~3, andgit status. Then redo it with--mixedand--hard, predicting the state of all three trees each time. reset --hard HEAD~3, then recover with the reflog.- Delete a branch with unmerged commits, then bring it back from the reflog.
- Make an uncommitted edit and
git restoreit. Try to recover it. Note that you cannot. git reverta pushed commit;git logand explain why this is safe onmain.- Build a bisect target: 30 commits, break something in the middle deliberately, then find it
with
git bisectmanually. - Do it again with
git bisect run pytest -x -q. Count the tests it ran. git log -S "some_function"on a real repo, thengit blamethe same line. Compare what each told you.- D-04: for R(A,B,C,D,E) with A→BC, CD→E, B→D, compute
A⁺,AB⁺andBD⁺, and list every candidate key.
Exit questions
Answer aloud, no notes.
- What do the three conflict markers mean?
- Why do
--ours/--theirsinvert during a rebase? - What does
rereredo, and when does it save you? reset --soft/--mixed/--hard— what moves in each?- What is the only thing Git can genuinely lose?
resetvsrevert— and the single question that decides.- What is the reflog, and why does it work? (Answer from the object model.)
- How would you recover a deleted branch?
- How many tests does bisect need for 1,000 commits?
- What does
git bisect runneed from each commit, and what does exit 125 mean? - Why does bisect justify atomic commits?
git log -Svsgit blame— when is each right?- What is an FD, and how do you compute closure?
- How do you get candidate keys from closure?
Articulation drill
Record two minutes: "A user reports a bug that definitely was not there last release. There are 400 commits since. How do you find it?"
Refuse the obvious answer first — that framing is the point of the question: "I would not read
diffs or guess from the symptom. Four hundred commits is ten to fifteen tests with git bisect, and
that's certainty rather than a hypothesis."
Then be concrete about the method: "first I need a reliable reproduction — a failing test, or
at minimum an exact command, because bisect is only as good as the verdict I give it each round. Then
git bisect start, mark the last release tag good and HEAD bad, and rather than testing by hand I'd
run git bisect run pytest tests/test_thing.py -x. It walks the whole range and prints the first bad
commit while I do something else. Exit 125 skips commits that can't even be built, which happens on
dependency bumps."
Then the honest limitation, which is what makes the answer credible: "bisect needs every commit to build and pass, so if history is full of 'wip' and 'fix typo' commits it degrades badly — which is the practical argument for atomic commits, and it isn't about tidiness. And bisect gives me the commit, not the cause: if that commit is a 400-line refactor with the message 'refactor', I've narrowed it to a haystack. So a good commit message and a small diff are the second half of the same investment."
Close with the alternative tool: "and if I already suspect a specific function or config key,
git log -S on that string is faster still — it finds the commit that introduced or removed it, where
blame usually just shows me whoever last reformatted the line."
Previous: Day 086 · Tomorrow: Day 088 — remotes, trunk-based vs git-flow, and PR discipline: why a 900-line pull request gets a worse review than a 90-line one