Git Interview Questions · 2026

Git Interview Questions (2026): 30 Most Commonly Asked, With Answers

A backend candidate at a Series B fintech got asked one question in March 2026 that ended the interview early: "You just ran git reset --hard HEAD~1 on main, and you'd already pushed that commit twenty minutes ago. Two teammates have pulled since then. What happens next?" He explained reset correctly. He had no idea what to do about the two teammates. That gap is what actually separates candidates who've used Git from candidates who've only used GitHub's merge button.

Here's an opinion that might be wrong: most teams that mandate a strict rebase-only workflow are optimizing for a diagram that looks clean in a slide deck, not for what happens when six people ship to the same branch under a deadline. Rebase produces a tidier git log. Merge produces an honest one. Both are defensible, and an interviewer who expects a strong opinion without also wanting the trade-off explained isn't testing the right thing.

This page covers the Git interview questions that show up across coding and DevOps loops in 2026: the object model under the porcelain commands, staging and commit hygiene, branching and merge mechanics, merge versus rebase, cherry-pick, the reset/revert/checkout/restore family (the one that trips up the most candidates), stash, remotes, conflict resolution, workflows, reflog recovery, bisect, and tags. GitHub processed 986 million commits across the platform in 2025, a 25 percent jump year over year, with 43.2 million pull requests merged in an average month (GitHub Octoverse, 2025). Every one of those pull requests eventually gets asked about in an interview somewhere.

52Questions
git rebase -iCore Command
3Reset Modes
2Merge Strategies

Git fundamentals: the three states, and what .git actually stores

Almost every loop opens here, even for candidates with five years of daily Git use. A candidate who describes Git as "a backup system for your code" usually can't reason about anything downstream of that.

Easy questions

20

A tracked file lives in one of three states: modified (changed in the working directory but not marked for the next commit), staged (marked via git add to go into the next commit), and committed (stored in the local repository's object database). git commit moves everything staged into a new commit and clears the staging area. Candidates who skip staging entirely usually stumble on why git commit -a exists at all, since it's a shortcut that bypasses staging for already-tracked files.

git init creates a .git subdirectory with an empty object database, a HEAD file pointing at a branch that doesn't exist yet, a repo-scoped config file, and sample hook scripts. Nothing else on disk changes. Worth knowing cold: the entire history of a repository lives in that one folder. Delete .git and every commit, branch, and tag is gone, the working files left behind are just whatever the last checkout happened to leave.

The tilde walks straight back through first parents: HEAD~2 means "the grandparent of HEAD," two commits back along whichever chain HEAD's first parent follows. The caret picks a specific parent when a commit has more than one, which only happens at merge commits: HEAD^2 means "the second parent of HEAD," the tip of whatever branch got merged in, not two commits back at all. Candidates who've never had to dig through a merge commit's history usually haven't hit ^2 and assume it behaves like ~2. On a repo with no merges the two even look interchangeable, which is exactly how the confusion sticks around.

bash
git log --oneline -1 HEAD~2   # second commit back, first-parent line
git log --oneline -1 HEAD^2   # second parent of a merge commit

Git reads config from three layers: system-wide (rare, affects every user on the machine), global (~/.gitconfig, your personal defaults across every repo), and local (.git/config, one specific repo). Local wins over global, global wins over system, so a work laptop can carry a personal global email address and still commit under a client's contract email inside that one client's repo, without editing global settings every time you switch projects.

bash
git config --global user.email "you@personal.com"
git config --local user.email "you@clientwork.com"   # overrides, this repo only
git config --list --show-origin   # see which file set each value

Staging lets you build a commit out of only some of your current changes. You might fix a bug and, in the same sitting, clean up an unrelated typo three files over. Staging turns those into two coherent commits instead of one that mixes unrelated work. Without it, "commit" and "save everything I've changed" would be the same operation.

An atomic commit does exactly one logical thing and leaves the codebase working at that point. Reviewers care because a giant commit bundling a refactor with a bug fix with a dependency bump makes git blame useless (every line traces to one 40-file commit) and makes git revert dangerous, since reverting the fix also reverts the refactor whether you wanted that or not.

A branch is a 41-byte text file containing a single commit hash, nothing more. Creating one is cheap (write one small file); it doesn't copy the codebase or history, both of which already live in the shared object database. That's what makes branches close to free to create or delete, unlike version control systems that duplicate files per branch.

Once a branch's commits are reachable from another branch, deleting the pointer itself with git branch -d doesn't delete those commits, they're still reachable from wherever they got merged into. The -d flag even refuses to delete a branch with unmerged commits, forcing -D if you really mean it.

Cherry-pick takes the diff introduced by one specific commit on another branch and applies it as a new commit on your current branch, with a new hash and parent. Unlike merge or rebase, nothing else from that branch's history comes along, just the one commit's changes.

git restore --staged <file> (or the older git reset <file>) moves that file back from staged to modified. The change stays in the working directory exactly as it was, you've just told Git not to include it in the next commit yet.

git restore <file> (or git checkout -- <file> on older Git) throws away working directory edits and restores the file to match the last commit. There's no undo here, the discarded edits aren't in any commit or stash, so they're genuinely gone.

Untracked files, ones Git was never told to track in the first place, survive reset --hard completely untouched, since reset only operates on files Git already knows about. Build artifacts, stray log files, a config file you created and forgot to add all stick around. git clean removes those, with -n to preview what would go before you commit to it and -d to also remove untracked directories, not just files.

bash
git clean -n -d   # dry run, lists what would be deleted, deletes nothing yet
git clean -fd     # actually deletes untracked files and directories

There's no reflog safety net for this one. Untracked files were never committed anywhere, so git clean -fd is permanent the instant it runs.

Stash saves tracked, modified changes, staged and unstaged, as a new entry, then resets the working directory to match HEAD, so it looks like there are no local changes at all. By default it skips untracked and ignored files; add -u to include untracked ones, -a for ignored ones too.

git stash apply reapplies the most recent stash and leaves that entry in the stash list. git stash pop does the same reapply and then deletes the entry, unless applying it produced a conflict, in which case Git keeps the stash around so you don't lose it mid-resolve.

git fetch downloads new commits and branches into your local object database and updates remote-tracking branches like origin/main, but doesn't touch your current branch. git pull is fetch immediately followed by a merge, or rebase with --rebase, into your current branch. Fetch is the safer default when you want to see what changed before deciding what to do about it.

The -u (or --set-upstream) flag links your local branch to a specific remote branch, so afterward plain git push and git pull with no arguments know exactly where to send or fetch from. Without it, Git either refuses to push a brand-new branch or asks which remote and branch you meant, every single time. Set it once when the branch is first pushed and every later push or pull on that branch just works.

bash
git push -u origin feature-x   # first push: links feature-x to origin/feature-x
git push                        # every push after that, no arguments needed

Git couldn't automatically reconcile two versions of the same lines, so it leaves both in the file, wrapped in markers, for you to resolve by hand.

bash
<<<<<<< HEAD
const MAX_RETRIES = 3;
=======
const MAX_RETRIES = 5;
>>>>>>> feature-x

Everything above the ======= divider is your current branch's version; everything below it, down to the >>>>>>> line, is the incoming branch's. Edit the file down to the correct result, delete all three marker lines, then git add to mark it resolved.

A lightweight tag is just a named pointer to a commit, essentially a branch that never moves. An annotated tag is a full object: tagger name, email, timestamp, message, and an optional GPG signature. git describe looks for annotated tags by default. Use annotated tags for anything that's an actual release; lightweight ones are fine for throwaway local bookmarks.

Tags push to a remote same as branches, which means a mistaken tag lives in two places once anyone has fetched: your local repo and the remote. Deleting it locally only removes your copy, the remote still serves it to every other clone until you explicitly delete it there too.

bash
git tag -d v2.4.0                  # delete locally
git push origin --delete v2.4.0    # delete on the remote

Anyone who already pulled the tag before you deleted it keeps their local copy regardless, deleting a ref doesn't reach into someone else's machine.

.gitignore tells Git which untracked files to leave alone when you run git status or git add ., nothing more. It has zero effect on a file Git is already tracking, adding a pattern after the fact doesn't retroactively untrack anything, which is exactly why a .env file committed once by accident keeps showing up in every diff even after someone adds it to .gitignore the next day.

bash
node_modules/
*.log
.env

Medium questions

24

A commit is an object in Git's content-addressable store, identified by a SHA-1 hash of its contents. Each commit points to exactly one tree object, a full snapshot of the directory at that point, not a diff, plus zero or more parent hashes, an author, and a message. Candidates who describe commits as storing diffs usually can't explain why checking out an old commit shows the full file tree instantly instead of replaying every change since. Git computes diffs on demand for display; it doesn't store history that way internally.

git log takes filters most candidates never learn past the default view. --author narrows to one person, --since and --until narrow to a date window, --grep searches commit messages. The one that surprises people is -S, the "pickaxe," which finds every commit where the number of occurrences of a given string changed, meaning it was added or removed, not just present somewhere in the diff.

bash
git log --author="Priya" --since="2 weeks ago" --oneline
git log --grep="fix.*race condition" -i
git log -S"MAX_RETRIES" --oneline   # commits that added or removed this string

That last one turns "who changed this constant and when" from a manual scroll through blame into a two-second search.

Windows tends to use CRLF line endings, macOS and Linux use LF, and if a repo doesn't pin down which one to store, one developer's editor can silently rewrite every line ending in a file, which Git then reports as a full-file change even though no actual code moved. core.autocrlf controls the conversion on checkout and commit; .gitattributes pins it per file type so the behavior doesn't depend on whoever happens to clone the repo next.

bash
# .gitattributes, committed to the repo so everyone gets the same behavior
* text=auto
*.sh text eol=lf
*.bat text eol=crlf

A pull request that shows 400 lines changed in a file where the actual fix was one line is almost always this, not a real rewrite.

git add -p (patch mode) walks through every changed hunk one at a time and asks whether to stage it, skip it, or split it further. It's the tool for exactly the scenario above: one file has both a real fix and an unrelated cleanup, and you want the fix staged now without dragging the cleanup into the same commit. Interviewers who ask "how do you keep commits focused" are usually fishing for this answer, not "I try to remember to only change one thing at a time."

Amend rewrites the most recent commit, snapshot or message or both, and produces a brand new hash even if only the message changed. Safe when that commit hasn't been pushed anywhere. Dangerous once anyone else has pulled the old version, because now two commits with two different hashes claim to be "the same" change, and whoever pulls next hits a divergent history. Fixing it after the fact means the other person needs a rebase onto the new hash, not a merge, which would otherwise duplicate the change.

git blame <file> shows which commit last touched each line, useful for finding who to ask about a weird piece of logic before you change it. The catch: a big formatting pass or a mass rename shows up as the "last change" on every line it touched, burying the actual author of the logic underneath a commit that only reindented the file. git blame --ignore-revs-file (Git 2.23+) skips a list of known noisy commits, hashes of reformats and renames, and blames past them to the change that actually matters.

bash
git blame -w app.js   # -w also ignores whitespace-only changes
echo "a1b2c3d reformat entire file with prettier" > .git-blame-ignore-revs
git blame --ignore-revs-file .git-blame-ignore-revs app.js

A fast-forward merge happens when the branch being merged in is a direct descendant of your current position, no divergence at all, so Git just moves your pointer forward with no new commit. A true merge happens when both branches have new commits since diverging, so Git creates a merge commit with two parents to represent the reconciliation.

bash
# force a real merge commit even when a fast-forward would work
git merge --no-ff feature-x

# git log --graph --oneline afterward:
*   a1b2c3d Merge branch 'feature-x'
|
| * f4e5d6c Add rate limiting to auth endpoint
* | 9c8b7a6 Fix flaky test in payments suite
|/
* 1a2b3c4 Initial commit

Teams that want an explicit record of every feature branch run with --no-ff by default. Teams optimizing for a linear history squash or rebase instead.

HEAD normally points at a branch name, which in turn points at a commit. Check out a specific commit hash, a tag, or someone else's PR directly, and HEAD points straight at that commit instead, with no branch name attached at all. That's detached HEAD. It's not dangerous by itself, you can look around, build, run tests. It gets dangerous the moment you commit while detached: those new commits belong to no branch, and switching away without creating one first leaves them unreachable, dangling until garbage collection eventually cleans them up.

bash
git checkout a1b2c3d          # detached HEAD, just looking around
git checkout -b hotfix-review # attach it to a real branch before committing

The fix if you already committed and only now noticed: git branch new-branch-name right where you are, before checking out anything else, which gives those orphaned-looking commits a name to live under.

Worktree lets one repository have several working directories checked out at once, each on a different branch, sharing the same object database and remote-tracking branches instead of duplicating the whole history to disk. The case it actually solves: a critical bug report comes in while you're mid-refactor with a dozen uncommitted files you don't want to stash yet. Cloning again means a second full history download and a completely separate set of remotes to keep in sync. Worktree gives you a second folder pointed at main, fix the bug there, push, done, and your half-finished refactor never got touched.

bash
git worktree add ../hotfix-dir main
cd ../hotfix-dir   # separate folder, same repo, different branch checked out
git worktree list
git worktree remove ../hotfix-dir

Merge takes two divergent histories and creates a new commit combining them, both original chains stay as they were, plus one merge commit on top. Rebase sets your commits aside, moves your branch to the tip of the target branch, then replays your commits on top one by one, each getting a new hash because its parent changed. Merge preserves what actually happened. Rebase rewrites the story to look sequential, a cleaner log at the cost of no longer being a literal record of events.

Never rebase commits that already exist outside your own repository, that anyone else may have pulled or built work on top of. Since rebase creates new commits to replace the old ones, anyone with the old commits now has a history that's diverged in a way git pull can't cleanly reconcile (Pro Git, chapter 3.6). Rebase your own local feature branch before opening a pull request all day long. Rebase main, or any branch others are actively pulling from, and you're rewriting history out from under them.

Interactive rebase opens an editable list of recent commits and lets you reorder, edit, drop, or combine them before replaying. squash combines a commit into the one before it and opens an editor for a new combined message. fixup does the same combination but silently discards the fixup commit's message.

bash
git rebase -i HEAD~4

pick f7e8d9a Add password strength validation
squash 3c4d5e6 Fix typo in validation error message
fixup 9a8b7c6 Remove leftover console.log
pick 1d2e3f4 Add tests for validation

Fixup is the one to reach for when a "fix a typo I just introduced" commit doesn't deserve its own line in the final history.

The classic case: a critical fix landed on main and needs to go out on a release branch too, but you don't want the forty unrelated commits that piled up since the branch forked. git cherry-pick <hash> grabs just that fix. When a cherry-pick conflicts, Git pauses mid-pick with conflict markers, same as a merge conflict, resolve them, git add, then git cherry-pick --continue, or --abort to bail out entirely.

All three buttons solve the same problem, a finished PR needs to land on main, but they leave different history behind. A plain merge keeps every commit from the branch plus adds one merge commit tying them together. "Rebase and merge" replays each of the branch's commits individually onto main with new hashes, no merge commit at all, a fully linear history. "Squash and merge" throws away the individual commits entirely and lands the whole PR as one new commit on main. Teams that don't care about commit-level history inside a PR, only that the PR as a whole is reviewable, tend to default to squash, since it also means a messy intermediate commit from mid-review never has to exist on main at all.

Revert creates a new commit that applies the inverse of a previous one, history stays intact, nothing gets rewritten or deleted. Reset moves the branch pointer backward, destructive if that pointer has already been shared. Rule of thumb: revert on anything public, reset only on your own local, unpushed work.

git checkout historically did two unrelated jobs at once: switching branches and discarding file changes, exactly the kind of overloaded command that occasionally leads someone to nuke uncommitted work while trying to switch branches. Git 2.23 split those apart.

bash
# old, overloaded
git checkout feature-x
git checkout -- app.js

# newer, split (Git 2.23+)
git switch feature-x
git restore app.js

checkout still works and isn't deprecated, most teams' muscle memory hasn't caught up yet, but interviewers do sometimes ask whether you know the split exists and why.

git clone --depth 1 pulls down only the most recent commit's snapshot instead of the entire history, which on a repository with years of commits and a lot of binary churn can turn a multi-minute clone into a few seconds. CI pipelines that just need to build and test the current commit are the obvious win. The trade-off shows up the moment you need history: git log only shows one commit, git blame can't trace further back, and bisect has nothing to search through, so shallow clones are wrong for any job that needs the past, only right for jobs that need the present.

bash
git clone --depth 1 https://github.com/org/repo.git
git fetch --unshallow   # fixes it after the fact, if you need full history later

A fork has its own origin (your copy), but nothing pointing at the project you forked from unless you add it yourself, conventionally as a remote named upstream. Fetch from upstream, merge or rebase its main branch into yours, then push to your own origin. Skip this for long enough and a fork drifts far enough behind that opening a pull request means resolving weeks of unrelated conflicts instead of just the ones from your actual change.

bash
git remote add upstream https://github.com/original-owner/repo.git
git fetch upstream
git checkout main
git merge upstream/main   # or: git rebase upstream/main
git push origin main

git merge --abort or git rebase --abort backs out entirely, returning your branch to exactly where it was before you started. Both only work while the operation is still in a conflicted, unfinished state, once you've committed a merge or run rebase --continue all the way through, there's nothing left to abort back to.

Gitflow runs long-lived develop and main branches plus per-feature and per-release branches with a defined merge order. It gives you a clear place for hotfixes and staged releases, at the cost of branches that can live for weeks and rack up painful conflicts by the time they land. Trunk-based development keeps everyone merging small, short-lived branches into main constantly, often behind feature flags, which keeps merges small but demands real discipline around flags and fast CI. My honest read: Gitflow made more sense when deploys were quarterly events; most teams shipping continuously in 2026 are better served by trunk-based, though regulated industries probably still earn Gitflow's staged structure.

Bisect runs a binary search across your history instead of a linear one. Tell it one commit that's good and one that's bad, and Git checks out the midpoint for you to test, then halves the remaining range again based on your answer, converging in roughly log-n steps instead of checking every commit.

bash
git bisect start
git bisect bad HEAD
git bisect good v2.3.0
# test, then mark: git bisect good  (or bad), repeat
git bisect reset

# or automate with a script that exits 0 (good) or 1 (bad):
git bisect run ./run-tests.sh

On a history with a few hundred commits between the last known-good release and today, bisect finds the culprit in eight or nine steps instead of testing every one.

git rm --cached untracks a file while leaving the actual file sitting on disk untouched, exactly what you want once a secret or a local config file got committed and needs to stop being tracked without deleting anyone's working copy of it. Pair it with adding the path to .gitignore in the same commit, otherwise the file just gets re-added the next time someone runs git add ..

bash
git rm --cached .env
echo ".env" >> .gitignore
git commit -m "Stop tracking .env, add to gitignore"

Worth saying out loud in an interview: this doesn't scrub the file from history, only from the current tip. If it held a real secret, that's a filter-repo job, not a rm --cached job.

A submodule embeds one Git repository inside another as a pinned reference, a specific commit hash, not a live link to whatever's on the submodule's main branch right now. It's the tool for a shared library that multiple independent projects depend on but that each project needs to lock to a known-good version rather than pulling in whatever changed there yesterday. The parent repo doesn't store the submodule's files at all, just the path and the exact commit it should be checked out at.

bash
git submodule add https://github.com/org/shared-lib.git libs/shared-lib
git commit -m "Add shared-lib as submodule, pinned"

A plain git clone only checks out the parent repository, it deliberately leaves submodule directories empty since populating them means cloning a second (or third, or fourth) repository too, which Git won't do unless asked. The folders exist, the pinned commit reference exists in the parent's history, but nothing's actually been fetched into them yet.

bash
git clone --recurse-submodules https://github.com/org/parent-repo.git
# or, after an already-existing plain clone:
git submodule update --init --recursive

This is the one submodule question that comes up almost every time, since the answer is short enough that interviewers mostly use it as a quick gut check on whether a candidate's actually worked with a repo that has any.

Hard questions

8

A plain git rebase main replays your current branch on top of wherever main is now. --onto adds a third point: replay a specific range of commits onto a different base entirely, skipping commits in between that you don't want carried along. The classic case is a feature branch accidentally created off another feature branch instead of off main: you want your commits, not the other feature's half-finished commits, sitting on top of main.

bash
# replay commits from wrong-base..feature-x onto main,
# leaving out everything wrong-base itself introduced
git rebase --onto main wrong-base feature-x

It's a command most people look up fresh every single time they need it, including engineers who use Git daily, because the argument order (new base first, then the old base to exclude, then the branch to move) doesn't stick in memory the way merge or reset does.

All three move your branch pointer back to a target commit. They differ in what happens to the staging area and working directory on the way there. --soft touches only HEAD, so undone changes sit staged, ready to recommit differently. --mixed (the default) also resets staging, so those changes appear as unstaged edits. --hard resets HEAD, staging, and the working directory, so the changes are gone entirely, not staged, not sitting as edits.

bash
git reset --soft HEAD~1   # undo the commit, keep changes staged
git reset --mixed HEAD~1  # undo the commit, keep changes as unstaged edits
git reset --hard HEAD~1   # undo the commit AND discard the changes

--hard is the one that ended the interview from this page's opening scene. Reset moves your local branch pointer only, so if that commit was already pushed and pulled elsewhere, everyone else's copy still has it, and their next push looks like an attempt to reintroduce a commit you just tried to erase.

git push --force overwrites the remote branch unconditionally, including any commits a teammate pushed since you last fetched, silently discarding their work. --force-with-lease checks the remote hasn't moved since your last fetch first, and refuses the push if someone else has already pushed in the meantime.

bash
# dangerous: overwrites the remote no matter what's there now
git push --force origin feature-x

# safer: refuses if the remote changed since your last fetch
git push --force-with-lease origin feature-x

Neither is safe on a shared branch like main under normal circumstances. On your own feature branch after an interactive rebase, --force-with-lease is the one worth defaulting to.

"Reuse recorded resolution": once enabled, Git remembers how you resolved a specific conflict and automatically applies the same resolution if the exact same conflict shows up again. It matters most on a long-running branch that gets rebased repeatedly against a fast-moving main, where the same two lines conflict on every single rebase because both branches keep touching them. Resolve it once with rerere on, and every subsequent rebase applies that same fix automatically instead of asking you to redo identical manual work.

bash
git config --global rerere.enabled true
# resolve a conflict normally once; rerere records the resolution
# next time the identical conflict appears, it's applied automatically

No, and this trips up candidates who reach for the reset-versus-revert answer here out of habit. Deleting the file in a new commit only removes it going forward, the old commit still exists in history with the secret sitting right there in its snapshot, retrievable by anyone with clone access via git show on that old hash. The actual fix rewrites history to remove the object entirely, with a purpose-built tool like git filter-repo or BFG Repo-Cleaner, then force-pushes the rewritten history and, separately and non-negotiably, rotates the leaked credential. Rewriting history doesn't un-leak a secret that's already been cloned somewhere; only rotation does that.

bash
# BFG example: strip a file matching this name from every commit
bfg --delete-files "credentials.json" my-repo.git
git push --force

Reflog is a local, per-repository log of everywhere HEAD and your branch refs have pointed, resets, rebases, checkouts, all of it, kept separately from actual commit history and never shared on push or pull.

bash
git reflog
# f7e8d9a HEAD@{0}: reset: moving to HEAD~1
# 3c4d5e6 HEAD@{1}: commit: Add validation for empty cart

git reset --hard 3c4d5e6   # recover the commit reset just "deleted"

This is the real answer to "I ran reset --hard and lost a commit, is it gone forever?" Almost never, if recent. The commit object still sits in the database even after nothing points to it; reflog gives you the hash so you can point a branch back at it.

Normally a merge has one clear common ancestor, so Git does a straightforward three-way merge against it. A criss-cross history happens when branch A merges branch B, and later branch B also merges branch A, so there isn't a single most recent common ancestor anymore. There are two or more candidates that are both valid merge bases, and neither is an ancestor of the other.

Git's current merge strategy, ort (which replaced the older recursive strategy in Git 2.34), handles this by first merging the multiple candidate bases together, essentially running an inner merge of the ancestors themselves, and using the result as a synthetic "virtual" base for the real merge. If that inner merge has its own conflicts, you can end up with confusing results: hunks that look unrelated to the change you actually made, or a conflict that vanishes if you swap which branch merges into which.

You can see the multiple bases yourself with git merge-base --all branchA branchB. If it prints more than one SHA, you're in criss-cross territory. In practice, teams avoid this by keeping merge direction one-way: only ever merge main into a long-lived feature branch to keep it current, and do the feature-into-main merge exactly once at the end, right before deleting the branch.

Every unreachable commit, whether dropped by a reset --hard, an amend, or a rebase, stays around as a loose object on disk. It isn't wiped immediately. What protects it in the short term is the reflog, which records where HEAD and branch tips pointed even after you've moved them somewhere else.

Reflog entries aren't permanent, though. By default Git expires entries that are still reachable after 90 days (gc.reflogExpire) and entries pointing to already-unreachable commits after just 30 days (gc.reflogExpireUnreachable). Once an entry expires, git gc, whether you run it manually or it auto-triggers once loose object counts cross a threshold, is free to prune the underlying object after its own grace period, which defaults to two weeks (gc.pruneExpire).

So the sequence is: a commit becomes unreachable, the reflog keeps a pointer to it for about 30 days, and gc gets license to delete the object roughly two weeks after that if nothing else references it. If a repo sits untouched for months, or someone runs git gc --prune=now right after a rebase, that safety window closes fast. Before it does, you can find the orphaned commit with git fsck --unreachable, which lists it as a dangling commit, and recover it with git cherry-pick or git branch recovered <sha>. The practical lesson is that reflog is a short-lived safety net, not a backup, and it has an expiration date.

How to prepare for a Git interview in 2026

Most Git interview questions reward hands-on recovery practice over syntax memorization. Almost every candidate can define rebase correctly; far fewer can walk through what happens to a shared branch when a rebase goes wrong, which is the actual follow-up interviewers ask. Build a small local repo, make a mess on purpose (rebase a branch you've already pushed somewhere, force-push over a teammate's commit in a throwaway remote, run reset --hard and recover with reflog) and fix your own damage. That teaches the recovery paths faster than memorizing them ever will.

Across mock interview sessions run through LastRoundAI's DevOps and backend tracks, the single most common stumble is exactly the reset-versus-revert scenario this page opened with: a candidate explains a command correctly in isolation, then can't reason about what happens to a second person's checkout once it's already been pushed. We don't have a clean percentage on how often that gap shows up, only that reviewers flag it more than any other single Git topic in our session notes.

The demand behind this isn't going anywhere. Software developers, quality assurance analysts, and testers held about 1.7 million jobs in 2024, with the BLS projecting 15 percent growth through 2034, much faster than average (BLS Occupational Outlook Handbook). Version control fluency gets tested at nearly every one of those roles, junior through staff, regardless of language.

Get the reps in before the real thing

Explaining reset versus revert calmly, out loud, while someone changes the scenario on you mid-answer is a different skill than reading the explanation once and nodding along. LastRoundAI's mock interview mode runs Git and version-control scenarios with real-time follow-up questions, not a static flashcard deck, and the free plan includes 15 credits a month that reset monthly rather than piling up unused.

Once your answers hold up under follow-up pressure, the slower part is usually just getting in front of enough teams. Auto-Apply queues tailored applications to backend, DevOps, and platform roles for your review, 10 a month on the free plan, up to 400 a month on the Ultimate plan, and every one sits in a review queue until you approve it. Nothing gets sent on your behalf without you seeing it first.

Questions about either product go to contact@lastroundai.com. That's the only inbox we check.

Leave a Reply

Your email address will not be published. Required fields are marked *