{"id":1155,"date":"2026-07-14T14:44:05","date_gmt":"2026-07-14T09:14:05","guid":{"rendered":"https:\/\/lastroundai.com\/blog\/?post_type=iq&#038;p=1155"},"modified":"2026-07-19T10:54:07","modified_gmt":"2026-07-19T05:24:07","slug":"git","status":"publish","type":"iq","link":"https:\/\/lastroundai.com\/interview-questions\/git","title":{"rendered":"Git Interview Questions (2026): 30 Most Commonly Asked, With Answers"},"content":{"rendered":"<p>A backend candidate at a Series B fintech got asked one question in March 2026 that ended the interview early: &#8220;You just ran <code>git reset --hard HEAD~1<\/code> on main, and you&#8217;d already pushed that commit twenty minutes ago. Two teammates have pulled since then. What happens next?&#8221; He explained reset correctly. He had no idea what to do about the two teammates. That gap is what actually separates candidates who&#8217;ve used Git from candidates who&#8217;ve only used GitHub&#8217;s merge button.<\/p>\n<p>Here&#8217;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 <code>git log<\/code>. Merge produces an honest one. Both are defensible, and an interviewer who expects a strong opinion without also wanting the trade-off explained isn&#8217;t testing the right thing.<\/p>\n<p>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 (<a href=\"https:\/\/github.blog\/news-insights\/octoverse\/what-986-million-code-pushes-say-about-the-developer-workflow-in-2025\/\" target=\"_blank\" rel=\"noopener noreferrer\">GitHub Octoverse, 2025<\/a>). Every one of those pull requests eventually gets asked about in an interview somewhere.<\/p>\n<div class=\"iq-stats not-prose\"><div class=\"iq-stat\"><span class=\"iq-stat__value\">52<\/span><span class=\"iq-stat__label\">Questions<\/span><\/div><div class=\"iq-stat\"><span class=\"iq-stat__value\">git rebase -i<\/span><span class=\"iq-stat__label\">Core Command<\/span><\/div><div class=\"iq-stat\"><span class=\"iq-stat__value\">3<\/span><span class=\"iq-stat__label\">Reset Modes<\/span><\/div><div class=\"iq-stat\"><span class=\"iq-stat__value\">2<\/span><span class=\"iq-stat__label\">Merge Strategies<\/span><\/div><\/div>\n<h2>Git fundamentals: the three states, and what .git actually stores<\/h2>\n<p>Almost every loop opens here, even for candidates with five years of daily Git use. A candidate who describes Git as &#8220;a backup system for your code&#8221; usually can&#8217;t reason about anything downstream of that.<\/p>\n<div class=\"iq-dsec iq-dsec--easy\"><div class=\"iq-dsec__row\"><h2 class=\"iq-dsec__h\" id=\"easy\"><span class=\"iq-dsec__dot\" aria-hidden=\"true\"><\/span>Easy questions<\/h2><span class=\"iq-dsec__n\">20<\/span><\/div><div class=\"iq-dsec__bar\" aria-hidden=\"true\"><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What are Git&#039;s three states, and what actually moves a file between them?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Fundamentals<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>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 <code>git add<\/code> to go into the next commit), and committed (stored in the local repository&#8217;s object database). <code>git commit<\/code> moves everything staged into a new commit and clears the staging area. Candidates who skip staging entirely usually stumble on why <code>git commit -a<\/code> exists at all, since it&#8217;s a shortcut that bypasses staging for already-tracked files.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What actually happens when you run git init, and what&#039;s inside .git?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Fundamentals<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p><code>git init<\/code> creates a <code>.git<\/code> subdirectory with an empty object database, a <code>HEAD<\/code> file pointing at a branch that doesn&#8217;t exist yet, a repo-scoped <code>config<\/code> 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 <code>.git<\/code> and every commit, branch, and tag is gone, the working files left behind are just whatever the last checkout happened to leave.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What&#039;s the difference between HEAD~2 and HEAD^2?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Fundamentals<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>The tilde walks straight back through first parents: <code>HEAD~2<\/code> means &#8220;the grandparent of HEAD,&#8221; two commits back along whichever chain HEAD&#8217;s first parent follows. The caret picks a specific parent when a commit has more than one, which only happens at merge commits: <code>HEAD^2<\/code> means &#8220;the second parent of HEAD,&#8221; the tip of whatever branch got merged in, not two commits back at all. Candidates who&#8217;ve never had to dig through a merge commit&#8217;s history usually haven&#8217;t hit <code>^2<\/code> and assume it behaves like <code>~2<\/code>. On a repo with no merges the two even look interchangeable, which is exactly how the confusion sticks around.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">bash<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-bash\">\n\ngit log \u2013oneline -1 HEAD~2   # second commit back, first-parent line\n\ngit log \u2013oneline -1 HEAD^2   # second parent of a merge commit\n<\/code><\/pre><\/div><br \/>\n<\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What&#039;s the difference between git config --global and --local, and which one wins?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Fundamentals<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Git reads config from three layers: system-wide (rare, affects every user on the machine), global (<code>~\/.gitconfig<\/code>, your personal defaults across every repo), and local (<code>.git\/config<\/code>, 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&#8217;s contract email inside that one client&#8217;s repo, without editing global settings every time you switch projects.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">bash<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-bash\">\n\ngit config \u2013global user.email \u201cyou@personal.com\u201d\n\ngit config \u2013local user.email \u201cyou@clientwork.com\u201d   # overrides, this repo only\n\ngit config \u2013list \u2013show-origin   # see which file set each value\n<\/code><\/pre><\/div><br \/>\n<\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What&#039;s the point of the staging area? Why not commit straight from the working directory?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Staging<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>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, &#8220;commit&#8221; and &#8220;save everything I&#8217;ve changed&#8221; would be the same operation.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What makes a commit &#039;atomic,&#039; and why do reviewers push back on giant commits?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Commits<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>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 <code>git blame<\/code> useless (every line traces to one 40-file commit) and makes <code>git revert<\/code> dangerous, since reverting the fix also reverts the refactor whether you wanted that or not.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What is a Git branch, actually, under the hood?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Branching<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>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&#8217;t copy the codebase or history, both of which already live in the shared object database. That&#8217;s what makes branches close to free to create or delete, unlike version control systems that duplicate files per branch.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">Is it safe to delete a branch right after merging it?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Branching<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Once a branch&#8217;s commits are reachable from another branch, deleting the pointer itself with <code>git branch -d<\/code> doesn&#8217;t delete those commits, they&#8217;re still reachable from wherever they got merged into. The <code>-d<\/code> flag even refuses to delete a branch with unmerged commits, forcing <code>-D<\/code> if you really mean it.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What does git cherry-pick actually do?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Cherry-pick<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>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&#8217;s history comes along, just the one commit&#8217;s changes.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">How do you unstage a file without losing the change itself?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Undoing Changes<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p><code>git restore --staged &lt;file&gt;<\/code> (or the older <code>git reset &lt;file&gt;<\/code>) moves that file back from staged to modified. The change stays in the working directory exactly as it was, you&#8217;ve just told Git not to include it in the next commit yet.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">How do you discard uncommitted local changes to a single file?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Undoing Changes<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p><code>git restore &lt;file&gt;<\/code> (or <code>git checkout -- &lt;file&gt;<\/code> on older Git) throws away working directory edits and restores the file to match the last commit. There&#8217;s no undo here, the discarded edits aren&#8217;t in any commit or stash, so they&#8217;re genuinely gone.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">git reset --hard didn&#039;t clear everything, there are still extra files sitting around. What&#039;s left?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Undoing Changes<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Untracked files, ones Git was never told to track in the first place, survive <code>reset --hard<\/code> 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. <code>git clean<\/code> removes those, with <code>-n<\/code> to preview what would go before you commit to it and <code>-d<\/code> to also remove untracked directories, not just files.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">bash<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-bash\">\n\ngit clean -n -d   # dry run, lists what would be deleted, deletes nothing yet\n\ngit clean -fd     # actually deletes untracked files and directories\n<\/code><\/pre><\/div><\/p>\n<p>There&#8217;s no reflog safety net for this one. Untracked files were never committed anywhere, so <code>git clean -fd<\/code> is permanent the instant it runs.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What does git stash actually save, and what does it leave behind?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Stash<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>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 <code>-u<\/code> to include untracked ones, <code>-a<\/code> for ignored ones too.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">stash pop vs. stash apply, what&#039;s the real difference?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Stash<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p><code>git stash apply<\/code> reapplies the most recent stash and leaves that entry in the stash list. <code>git stash pop<\/code> 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&#8217;t lose it mid-resolve.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What&#039;s the difference between git fetch and git pull?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Remotes<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p><code>git fetch<\/code> downloads new commits and branches into your local object database and updates remote-tracking branches like <code>origin\/main<\/code>, but doesn&#8217;t touch your current branch. <code>git pull<\/code> is fetch immediately followed by a merge, or rebase with <code>--rebase<\/code>, into your current branch. Fetch is the safer default when you want to see what changed before deciding what to do about it.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What does git push -u actually set up, and why do you only need it once per branch?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Remotes<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>The <code>-u<\/code> (or <code>--set-upstream<\/code>) flag links your local branch to a specific remote branch, so afterward plain <code>git push<\/code> and <code>git pull<\/code> 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.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">bash<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-bash\">\n\ngit push -u origin feature-x   # first push: links feature-x to origin\/feature-x\n\ngit push                        # every push after that, no arguments needed\n<\/code><\/pre><\/div><br \/>\n<\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What do the &lt;&lt;&lt;&lt;&lt;&lt;&lt; markers in a conflicted file actually mean?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Conflicts<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Git couldn&#8217;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.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">bash<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-bash\">\n\n&lt;&lt;&lt;&lt;&lt;&lt;&lt; HEAD\n\nconst MAX_RETRIES = 3;\n\n=======\n\nconst MAX_RETRIES = 5;\n\n&gt;&gt;&gt;&gt;&gt;&gt;&gt; feature-x\n<\/code><\/pre><\/div><\/p>\n<p>Everything above the <code>=======<\/code> divider is your current branch&#8217;s version; everything below it, down to the <code>&gt;&gt;&gt;&gt;&gt;&gt;&gt;<\/code> line, is the incoming branch&#8217;s. Edit the file down to the correct result, delete all three marker lines, then <code>git add<\/code> to mark it resolved.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">Lightweight vs. annotated tags, what&#039;s the actual difference and why does it matter for releases?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Tags<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>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. <code>git describe<\/code> looks for annotated tags by default. Use annotated tags for anything that&#8217;s an actual release; lightweight ones are fine for throwaway local bookmarks.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">How do you delete a tag that was pushed by mistake, and why does deleting it locally not get rid of it everywhere?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Tags<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>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.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">bash<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-bash\">\n\ngit tag -d v2.4.0                  # delete locally\n\ngit push origin \u2013delete v2.4.0    # delete on the remote\n<\/code><\/pre><\/div><\/p>\n<p>Anyone who already pulled the tag before you deleted it keeps their local copy regardless, deleting a ref doesn&#8217;t reach into someone else&#8217;s machine.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">How does .gitignore actually work, and why doesn&#039;t it remove a file that&#039;s already tracked?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Ignore Rules<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>.gitignore tells Git which untracked files to leave alone when you run <code>git status<\/code> or <code>git add .<\/code>, nothing more. It has zero effect on a file Git is already tracking, adding a pattern after the fact doesn&#8217;t retroactively untrack anything, which is exactly why a <code>.env<\/code> file committed once by accident keeps showing up in every diff even after someone adds it to .gitignore the next day.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">bash<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-bash\">\n\nnode_modules\/\n\n*.log\n\n.env\n<\/code><\/pre><\/div><br \/>\n<\/div><\/div><\/div>\n<div class=\"iq-dsec iq-dsec--medium\"><div class=\"iq-dsec__row\"><h2 class=\"iq-dsec__h\" id=\"medium\"><span class=\"iq-dsec__dot\" aria-hidden=\"true\"><\/span>Medium questions<\/h2><span class=\"iq-dsec__n\">24<\/span><\/div><div class=\"iq-dsec__bar\" aria-hidden=\"true\"><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What is a Git commit, really, under the hood?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Object Model<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>A commit is an object in Git&#8217;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&#8217;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&#8217;t store history that way internally.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">How do you search commit history for a specific author, date range, or a string that used to exist in the code?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Fundamentals<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p><code>git log<\/code> takes filters most candidates never learn past the default view. <code>--author<\/code> narrows to one person, <code>--since<\/code> and <code>--until<\/code> narrow to a date window, <code>--grep<\/code> searches commit messages. The one that surprises people is <code>-S<\/code>, the &#8220;pickaxe,&#8221; 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.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">bash<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-bash\">\n\ngit log \u2013author=\u201dPriya\u201d \u2013since=\u201d2 weeks ago\u201d \u2013oneline\n\ngit log \u2013grep=\u201dfix.*race condition\u201d -i\n\ngit log -S\u201dMAX_RETRIES\u201d \u2013oneline   # commits that added or removed this string\n<\/code><\/pre><\/div><\/p>\n<p>That last one turns &#8220;who changed this constant and when&#8221; from a manual scroll through blame into a two-second search.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">Why do line endings cause diffs that show a whole file as changed when nobody touched it?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Fundamentals<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Windows tends to use CRLF line endings, macOS and Linux use LF, and if a repo doesn&#8217;t pin down which one to store, one developer&#8217;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. <code>core.autocrlf<\/code> controls the conversion on checkout and commit; <code>.gitattributes<\/code> pins it per file type so the behavior doesn&#8217;t depend on whoever happens to clone the repo next.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">bash<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-bash\">\n\n# .gitattributes, committed to the repo so everyone gets the same behavior\n\n* text=auto\n\n*.sh text eol=lf\n\n*.bat text eol=crlf\n<\/code><\/pre><\/div><\/p>\n<p>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.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What does git add -p actually let you do, and when do you reach for it?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Staging<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p><code>git add -p<\/code> (patch mode) walks through every changed hunk one at a time and asks whether to stage it, skip it, or split it further. It&#8217;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 &#8220;how do you keep commits focused&#8221; are usually fishing for this answer, not &#8220;I try to remember to only change one thing at a time.&#8221;<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">When is git commit --amend safe, and when does it bite you?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Commits<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>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&#8217;t been pushed anywhere. Dangerous once anyone else has pulled the old version, because now two commits with two different hashes claim to be &#8220;the same&#8221; 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.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What&#039;s git blame for, and why does it sometimes point at the wrong commit?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Commits<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p><code>git blame &lt;file&gt;<\/code> 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 &#8220;last change&#8221; on every line it touched, burying the actual author of the logic underneath a commit that only reindented the file. <code>git blame --ignore-revs-file<\/code> (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.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">bash<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-bash\">\n\ngit blame -w app.js   # -w also ignores whitespace-only changes\n\necho \u201ca1b2c3d reformat entire file with prettier\u201d &gt; .git-blame-ignore-revs\n\ngit blame \u2013ignore-revs-file .git-blame-ignore-revs app.js\n<\/code><\/pre><\/div><br \/>\n<\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What&#039;s the difference between a fast-forward merge and a real merge commit?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Merging<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>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.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">bash<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-bash\">\n\n# force a real merge commit even when a fast-forward would work\n\ngit merge \u2013no-ff feature-x\n# git log \u2013graph \u2013oneline afterward:\n\n*   a1b2c3d Merge branch \u2018feature-x\u2019\n\n|\n\n| * f4e5d6c Add rate limiting to auth endpoint\n\n* | 9c8b7a6 Fix flaky test in payments suite\n\n|\/\n\n* 1a2b3c4 Initial commit\n<\/code><\/pre><\/div><\/p>\n<p>Teams that want an explicit record of every feature branch run with <code>--no-ff<\/code> by default. Teams optimizing for a linear history squash or rebase instead.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What is &#039;detached HEAD&#039; state, and is it dangerous?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Branching<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>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&#8217;s PR directly, and HEAD points straight at that commit instead, with no branch name attached at all. That&#8217;s detached HEAD. It&#8217;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.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">bash<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-bash\">\n\ngit checkout a1b2c3d          # detached HEAD, just looking around\n\ngit checkout -b hotfix-review # attach it to a real branch before committing\n<\/code><\/pre><\/div><\/p>\n<p>The fix if you already committed and only now noticed: <code>git branch new-branch-name<\/code> right where you are, before checking out anything else, which gives those orphaned-looking commits a name to live under.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What does git worktree solve that just cloning the repo twice doesn&#039;t?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Branching<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>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&#8217;re mid-refactor with a dozen uncommitted files you don&#8217;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 <code>main<\/code>, fix the bug there, push, done, and your half-finished refactor never got touched.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">bash<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-bash\">\n\ngit worktree add ..\/hotfix-dir main\n\ncd ..\/hotfix-dir   # separate folder, same repo, different branch checked out\n\ngit worktree list\n\ngit worktree remove ..\/hotfix-dir\n<\/code><\/pre><\/div><br \/>\n<\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What&#039;s the actual difference between merge and rebase?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Merge vs. Rebase<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>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.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What&#039;s the &#039;golden rule of rebasing,&#039; and why does it exist?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Merge vs. Rebase<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>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&#8217;s diverged in a way <code>git pull<\/code> can&#8217;t cleanly reconcile (<a href=\"https:\/\/git-scm.com\/book\/en\/v2\/Git-Branching-Rebasing\" target=\"_blank\" rel=\"noopener noreferrer\">Pro Git, chapter 3.6<\/a>). Rebase your own local feature branch before opening a pull request all day long. Rebase <code>main<\/code>, or any branch others are actively pulling from, and you&#8217;re rewriting history out from under them.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What does interactive rebase let you do, and what&#039;s squash vs. fixup?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Merge vs. Rebase<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Interactive rebase opens an editable list of recent commits and lets you reorder, edit, drop, or combine them before replaying. <code>squash<\/code> combines a commit into the one before it and opens an editor for a new combined message. <code>fixup<\/code> does the same combination but silently discards the fixup commit&#8217;s message.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">bash<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-bash\">\n\ngit rebase -i HEAD~4\npick f7e8d9a Add password strength validation\n\nsquash 3c4d5e6 Fix typo in validation error message\n\nfixup 9a8b7c6 Remove leftover console.log\n\npick 1d2e3f4 Add tests for validation\n<\/code><\/pre><\/div><\/p>\n<p>Fixup is the one to reach for when a &#8220;fix a typo I just introduced&#8221; commit doesn&#8217;t deserve its own line in the final history.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">When would you actually reach for cherry-pick over merging or rebasing the whole branch?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Cherry-pick<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>The classic case: a critical fix landed on <code>main<\/code> and needs to go out on a release branch too, but you don&#8217;t want the forty unrelated commits that piled up since the branch forked. <code>git cherry-pick &lt;hash&gt;<\/code> grabs just that fix. When a cherry-pick conflicts, Git pauses mid-pick with conflict markers, same as a merge conflict, resolve them, <code>git add<\/code>, then <code>git cherry-pick --continue<\/code>, or <code>--abort<\/code> to bail out entirely.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">On GitHub, what&#039;s the actual difference between &#039;squash and merge,&#039; &#039;rebase and merge,&#039; and a plain merge commit?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Merge vs. Rebase<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>All three buttons solve the same problem, a finished PR needs to land on <code>main<\/code>, but they leave different history behind. A plain merge keeps every commit from the branch plus adds one merge commit tying them together. &#8220;Rebase and merge&#8221; replays each of the branch&#8217;s commits individually onto <code>main<\/code> with new hashes, no merge commit at all, a fully linear history. &#8220;Squash and merge&#8221; throws away the individual commits entirely and lands the whole PR as one new commit on <code>main<\/code>. Teams that don&#8217;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 <code>main<\/code> at all.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">When do you use revert instead of reset?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Undoing Changes<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>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.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What&#039;s the difference between checkout and the newer switch and restore commands?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Undoing Changes<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p><code>git checkout<\/code> 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.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">bash<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-bash\">\n\n# old, overloaded\n\ngit checkout feature-x\n\ngit checkout \u2014 app.js\n# newer, split (Git 2.23+)\n\ngit switch feature-x\n\ngit restore app.js\n<\/code><\/pre><\/div><\/p>\n<p><code>checkout<\/code> still works and isn&#8217;t deprecated, most teams&#8217; muscle memory hasn&#8217;t caught up yet, but interviewers do sometimes ask whether you know the split exists and why.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What&#039;s a shallow clone, and when does it actually help?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Remotes<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p><code>git clone --depth 1<\/code> pulls down only the most recent commit&#8217;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: <code>git log<\/code> only shows one commit, <code>git blame<\/code> can&#8217;t trace further back, and <code>bisect<\/code> 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.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">bash<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-bash\">\n\ngit clone \u2013depth 1 https:\/\/github.com\/org\/repo.git\n\ngit fetch \u2013unshallow   # fixes it after the fact, if you need full history later\n<\/code><\/pre><\/div><br \/>\n<\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">How do you keep a forked repo in sync with the original project you forked from?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Remotes<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>A fork has its own <code>origin<\/code> (your copy), but nothing pointing at the project you forked from unless you add it yourself, conventionally as a remote named <code>upstream<\/code>. 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.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">bash<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-bash\">\n\ngit remote add upstream https:\/\/github.com\/original-owner\/repo.git\n\ngit fetch upstream\n\ngit checkout main\n\ngit merge upstream\/main   # or: git rebase upstream\/main\n\ngit push origin main\n<\/code><\/pre><\/div><br \/>\n<\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">How do you abort a merge or rebase that&#039;s gone sideways?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Conflicts<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p><code>git merge --abort<\/code> or <code>git rebase --abort<\/code> 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&#8217;ve committed a merge or run <code>rebase --continue<\/code> all the way through, there&#8217;s nothing left to abort back to.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">Gitflow vs. trunk-based development, what&#039;s the real trade-off?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Workflows<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Gitflow runs long-lived <code>develop<\/code> and <code>main<\/code> 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 <code>main<\/code> 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&#8217;s staged structure.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">How does git bisect help you find the exact commit that introduced a bug?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Bisect<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Bisect runs a binary search across your history instead of a linear one. Tell it one commit that&#8217;s good and one that&#8217;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.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">bash<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-bash\">\n\ngit bisect start\n\ngit bisect bad HEAD\n\ngit bisect good v2.3.0\n\n# test, then mark: git bisect good  (or bad), repeat\n\ngit bisect reset\n# or automate with a script that exits 0 (good) or 1 (bad):\n\ngit bisect run .\/run-tests.sh\n<\/code><\/pre><\/div><\/p>\n<p>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.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">A file is already committed and now needs to be ignored going forward. What actually removes it from tracking without deleting it from disk?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Ignore Rules<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p><code>git rm --cached<\/code> 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&#8217;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 <code>git add .<\/code>.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">bash<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-bash\">\n\ngit rm \u2013cached .env\n\necho \u201c.env\u201d &gt;&gt; .gitignore\n\ngit commit -m \u201cStop tracking .env, add to gitignore\u201d\n<\/code><\/pre><\/div><\/p>\n<p>Worth saying out loud in an interview: this doesn&#8217;t scrub the file from history, only from the current tip. If it held a real secret, that&#8217;s a filter-repo job, not a <code>rm --cached<\/code> job.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What is a Git submodule, and what problem does it actually solve?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Submodules<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>A submodule embeds one Git repository inside another as a pinned reference, a specific commit hash, not a live link to whatever&#8217;s on the submodule&#8217;s main branch right now. It&#8217;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&#8217;t store the submodule&#8217;s files at all, just the path and the exact commit it should be checked out at.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">bash<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-bash\">\n\ngit submodule add https:\/\/github.com\/org\/shared-lib.git libs\/shared-lib\n\ngit commit -m \u201cAdd shared-lib as submodule, pinned\u201d\n<\/code><\/pre><\/div><br \/>\n<\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">Someone cloned a repo with submodules and the submodule folders are empty. What&#039;s missing?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Submodules<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>A plain <code>git clone<\/code> 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&#8217;t do unless asked. The folders exist, the pinned commit reference exists in the parent&#8217;s history, but nothing&#8217;s actually been fetched into them yet.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">bash<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-bash\">\n\ngit clone \u2013recurse-submodules https:\/\/github.com\/org\/parent-repo.git\n\n# or, after an already-existing plain clone:\n\ngit submodule update \u2013init \u2013recursive\n<\/code><\/pre><\/div><\/p>\n<p>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&#8217;s actually worked with a repo that has any.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-dsec iq-dsec--hard\"><div class=\"iq-dsec__row\"><h2 class=\"iq-dsec__h\" id=\"hard\"><span class=\"iq-dsec__dot\" aria-hidden=\"true\"><\/span>Hard questions<\/h2><span class=\"iq-dsec__n\">8<\/span><\/div><div class=\"iq-dsec__bar\" aria-hidden=\"true\"><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What does git rebase --onto do that a plain rebase can&#039;t?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Merge vs. Rebase<\/span><span class=\"iq-badge iq-badge--hard\">Hard<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>A plain <code>git rebase main<\/code> replays your current branch on top of wherever <code>main<\/code> is now. <code>--onto<\/code> adds a third point: replay a specific range of commits onto a different base entirely, skipping commits in between that you don&#8217;t want carried along. The classic case is a feature branch accidentally created off another feature branch instead of off <code>main<\/code>: you want your commits, not the other feature&#8217;s half-finished commits, sitting on top of <code>main<\/code>.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">bash<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-bash\">\n\n# replay commits from wrong-base..feature-x onto main,\n\n# leaving out everything wrong-base itself introduced\n\ngit rebase \u2013onto main wrong-base feature-x\n<\/code><\/pre><\/div><\/p>\n<p>It&#8217;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&#8217;t stick in memory the way <code>merge<\/code> or <code>reset<\/code> does.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What&#039;s the actual difference between reset --soft, --mixed, and --hard?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Undoing Changes<\/span><span class=\"iq-badge iq-badge--hard\">Hard<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>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. <code>--soft<\/code> touches only HEAD, so undone changes sit staged, ready to recommit differently. <code>--mixed<\/code> (the default) also resets staging, so those changes appear as unstaged edits. <code>--hard<\/code> resets HEAD, staging, and the working directory, so the changes are gone entirely, not staged, not sitting as edits.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">bash<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-bash\">\n\ngit reset \u2013soft HEAD~1   # undo the commit, keep changes staged\n\ngit reset \u2013mixed HEAD~1  # undo the commit, keep changes as unstaged edits\n\ngit reset \u2013hard HEAD~1   # undo the commit AND discard the changes\n<\/code><\/pre><\/div><\/p>\n<p><code>--hard<\/code> is the one that ended the interview from this page&#8217;s opening scene. Reset moves your local branch pointer only, so if that commit was already pushed and pulled elsewhere, everyone else&#8217;s copy still has it, and their next push looks like an attempt to reintroduce a commit you just tried to erase.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">push --force vs. push --force-with-lease, what&#039;s the actual danger being guarded against?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Remotes<\/span><span class=\"iq-badge iq-badge--hard\">Hard<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p><code>git push --force<\/code> overwrites the remote branch unconditionally, including any commits a teammate pushed since you last fetched, silently discarding their work. <code>--force-with-lease<\/code> checks the remote hasn&#8217;t moved since your last fetch first, and refuses the push if someone else has already pushed in the meantime.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">bash<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-bash\">\n\n# dangerous: overwrites the remote no matter what\u2019s there now\n\ngit push \u2013force origin feature-x\n# safer: refuses if the remote changed since your last fetch\n\ngit push \u2013force-with-lease origin feature-x\n<\/code><\/pre><\/div><\/p>\n<p>Neither is safe on a shared branch like <code>main<\/code> under normal circumstances. On your own feature branch after an interactive rebase, <code>--force-with-lease<\/code> is the one worth defaulting to.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What does git rerere do, and when is it worth turning on?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Conflicts<\/span><span class=\"iq-badge iq-badge--hard\">Hard<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>&#8220;Reuse recorded resolution&#8221;: 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 <code>main<\/code>, 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.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">bash<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-bash\">\n\ngit config \u2013global rerere.enabled true\n\n# resolve a conflict normally once; rerere records the resolution\n\n# next time the identical conflict appears, it\u2019s applied automatically\n<\/code><\/pre><\/div><br \/>\n<\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">A secret got committed and pushed. Does deleting the file in a new commit fix it?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Conflicts<\/span><span class=\"iq-badge iq-badge--hard\">Hard<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>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 <code>git show<\/code> on that old hash. The actual fix rewrites history to remove the object entirely, with a purpose-built tool like <code>git filter-repo<\/code> or BFG Repo-Cleaner, then force-pushes the rewritten history and, separately and non-negotiably, rotates the leaked credential. Rewriting history doesn&#8217;t un-leak a secret that&#8217;s already been cloned somewhere; only rotation does that.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">bash<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-bash\">\n\n# BFG example: strip a file matching this name from every commit\n\nbfg \u2013delete-files \u201ccredentials.json\u201d my-repo.git\n\ngit push \u2013force\n<\/code><\/pre><\/div><br \/>\n<\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What is git reflog, and how does it save you when you think a commit is gone forever?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Reflog<\/span><span class=\"iq-badge iq-badge--hard\">Hard<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>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.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">bash<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-bash\">\n\ngit reflog\n\n# f7e8d9a HEAD@{0}: reset: moving to HEAD~1\n\n# 3c4d5e6 HEAD@{1}: commit: Add validation for empty cart\ngit reset \u2013hard 3c4d5e6   # recover the commit reset just \u201cdeleted\u201d\n<\/code><\/pre><\/div><\/p>\n<p>This is the real answer to &#8220;I ran <code>reset --hard<\/code> and lost a commit, is it gone forever?&#8221; 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.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">Two branches have merged from each other more than once (a criss-cross merge). How does Git pick a merge base, and why can that cause weird conflicts?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Merge Strategy<\/span><span class=\"iq-badge iq-badge--hard\">Hard<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>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&#8217;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.<\/p>\n<p>Git&#8217;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 &#8220;virtual&#8221; 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.<\/p>\n<p>You can see the multiple bases yourself with git merge-base &#8211;all branchA branchB. If it prints more than one SHA, you&#8217;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.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">A commit was never deleted on purpose, but it&#039;s gone now and reflog doesn&#039;t show it either. What happened to it?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Garbage Collection<\/span><span class=\"iq-badge iq-badge--hard\">Hard<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Every unreachable commit, whether dropped by a reset &#8211;hard, an amend, or a rebase, stays around as a loose object on disk. It isn&#8217;t wiped immediately. What protects it in the short term is the reflog, which records where HEAD and branch tips pointed even after you&#8217;ve moved them somewhere else.<\/p>\n<p>Reflog entries aren&#8217;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).<\/p>\n<p>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 &#8211;prune=now right after a rebase, that safety window closes fast. Before it does, you can find the orphaned commit with git fsck &#8211;unreachable, which lists it as a dangling commit, and recover it with git cherry-pick or git branch recovered &lt;sha&gt;. The practical lesson is that reflog is a short-lived safety net, not a backup, and it has an expiration date.<\/p>\n<p><\/div><\/div><\/div>\n<h2>How to prepare for a Git interview in 2026<\/h2>\n<p>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&#8217;ve already pushed somewhere, force-push over a teammate&#8217;s commit in a throwaway remote, run <code>reset --hard<\/code> and recover with reflog) and fix your own damage. That teaches the recovery paths faster than memorizing them ever will.<\/p>\n<p>Across mock interview sessions run through LastRoundAI&#8217;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&#8217;t reason about what happens to a second person&#8217;s checkout once it&#8217;s already been pushed. We don&#8217;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.<\/p>\n<p>The demand behind this isn&#8217;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 (<a href=\"https:\/\/www.bls.gov\/ooh\/computer-and-information-technology\/software-developers.htm\" target=\"_blank\" rel=\"noopener noreferrer\">BLS Occupational Outlook Handbook<\/a>). Version control fluency gets tested at nearly every one of those roles, junior through staff, regardless of language.<\/p>\n<h2>Get the reps in before the real thing<\/h2>\n<p>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. <a href=\"\/products\/mock-interviews\">LastRoundAI&#8217;s mock interview mode<\/a> 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.<\/p>\n<p>Once your answers hold up under follow-up pressure, the slower part is usually just getting in front of enough teams. <a href=\"\/products\/auto-apply\">Auto-Apply<\/a> 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.<\/p>\n<p>Questions about either product go to contact@lastroundai.com. That&#8217;s the only inbox we check.<\/p>\n<div class=\"iq-sources not-prose\"><div class=\"iq-sources__title\">Sources &amp; further reading<\/div><ul class=\"iq-sources__list\"><li><a href=\"https:\/\/github.blog\/news-insights\/octoverse\/what-986-million-code-pushes-say-about-the-developer-workflow-in-2025\/\" target=\"_blank\" rel=\"nofollow noopener\">GitHub Octoverse 2025: developer workflow report<\/a><\/li><li><a href=\"https:\/\/git-scm.com\/book\/en\/v2\/Git-Branching-Rebasing\" target=\"_blank\" rel=\"nofollow noopener\">Pro Git, chapter 3.6: The Perils of Rebasing<\/a><\/li><li><a href=\"https:\/\/www.bls.gov\/ooh\/computer-and-information-technology\/software-developers.htm\" target=\"_blank\" rel=\"nofollow noopener\">BLS Occupational Outlook Handbook: Software Developers<\/a><\/li><\/ul><\/div>\n","protected":false},"excerpt":{"rendered":"<p>A backend candidate at a Series B fintech got asked one question in March 2026 that ended the interview early: &#8220;You just ran git reset &#8211;hard HEAD~1 on main, and you&#8217;d already pushed that commit twenty minutes ago. Two teammates have pulled since then. What happens next?&#8221; He explained reset correctly. He had no idea&#8230;<\/p>\n","protected":false},"author":3,"featured_media":1701,"comment_status":"open","ping_status":"closed","template":"","meta":{"_kad_post_transparent":"","_kad_post_title":"","_kad_post_layout":"","_kad_post_sidebar_id":"","_kad_post_content_style":"","_kad_post_vertical_padding":"","_kad_post_feature":"","_kad_post_feature_position":"","_kad_post_header":false,"_kad_post_footer":false,"_kad_post_classname":"","footnotes":""},"tags":[],"class_list":["post-1155","iq","type-iq","status-publish","has-post-thumbnail","hentry"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v27.8 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Git Interview Questions (2026) | LastRoundAI<\/title>\n<meta name=\"description\" content=\"Practice 30 real Git interview questions for 2026: branching, rebase vs merge, reset\/revert, stash, and conflict resolution, with clear answers.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/lastroundai.com\/interview-questions\/git\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Git Interview Questions (2026) | LastRoundAI\" \/>\n<meta property=\"og:description\" content=\"Practice 30 real Git interview questions for 2026: branching, rebase vs merge, reset\/revert, stash, and conflict resolution, with clear answers.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/lastroundai.com\/interview-questions\/git\" \/>\n<meta property=\"og:site_name\" content=\"LastRound AI\" \/>\n<meta property=\"article:modified_time\" content=\"2026-07-19T05:24:07+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/lastroundai.com\/blog\/wp-content\/uploads\/2026\/07\/iq-git-og.png\" \/>\n\t<meta property=\"og:image:width\" content=\"1200\" \/>\n\t<meta property=\"og:image:height\" content=\"630\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data1\" content=\"17 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/lastroundai.com\\\/interview-questions\\\/git\",\"url\":\"https:\\\/\\\/lastroundai.com\\\/interview-questions\\\/git\",\"name\":\"Git Interview Questions (2026) | LastRoundAI\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/lastroundai.com\\\/interview-questions\\\/git#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/lastroundai.com\\\/interview-questions\\\/git#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/iq-git-og.png\",\"datePublished\":\"2026-07-14T09:14:05+00:00\",\"dateModified\":\"2026-07-19T05:24:07+00:00\",\"description\":\"Practice 30 real Git interview questions for 2026: branching, rebase vs merge, reset\\\/revert, stash, and conflict resolution, with clear answers.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/lastroundai.com\\\/interview-questions\\\/git#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/lastroundai.com\\\/interview-questions\\\/git\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/lastroundai.com\\\/interview-questions\\\/git#primaryimage\",\"url\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/iq-git-og.png\",\"contentUrl\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/iq-git-og.png\",\"width\":1200,\"height\":630,\"caption\":\"Git interview questions \u2014 LastRoundAI\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/lastroundai.com\\\/interview-questions\\\/git#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/lastroundai.com\\\/blog\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Interview Questions\",\"item\":\"https:\\\/\\\/lastroundai.com\\\/interview-questions\"},{\"@type\":\"ListItem\",\"position\":3,\"name\":\"Git Interview Questions (2026): 30 Most Commonly Asked, With Answers\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/#website\",\"url\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/\",\"name\":\"LastRound AI\",\"description\":\"Interview Assistant prep, tech careers and AI tools\",\"publisher\":{\"@id\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/#organization\",\"name\":\"LastRound AI\",\"url\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/lastroundai-transprant-logo-optimized-BxEo2Wtq.png\",\"contentUrl\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/lastroundai-transprant-logo-optimized-BxEo2Wtq.png\",\"width\":400,\"height\":400,\"caption\":\"LastRound AI\"},\"image\":{\"@id\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/#\\\/schema\\\/logo\\\/image\\\/\"}}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Git Interview Questions (2026) | LastRoundAI","description":"Practice 30 real Git interview questions for 2026: branching, rebase vs merge, reset\/revert, stash, and conflict resolution, with clear answers.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/lastroundai.com\/interview-questions\/git","og_locale":"en_US","og_type":"article","og_title":"Git Interview Questions (2026) | LastRoundAI","og_description":"Practice 30 real Git interview questions for 2026: branching, rebase vs merge, reset\/revert, stash, and conflict resolution, with clear answers.","og_url":"https:\/\/lastroundai.com\/interview-questions\/git","og_site_name":"LastRound AI","article_modified_time":"2026-07-19T05:24:07+00:00","og_image":[{"width":1200,"height":630,"url":"https:\/\/lastroundai.com\/blog\/wp-content\/uploads\/2026\/07\/iq-git-og.png","type":"image\/png"}],"twitter_card":"summary_large_image","twitter_misc":{"Est. reading time":"17 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/lastroundai.com\/interview-questions\/git","url":"https:\/\/lastroundai.com\/interview-questions\/git","name":"Git Interview Questions (2026) | LastRoundAI","isPartOf":{"@id":"https:\/\/lastroundai.com\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/lastroundai.com\/interview-questions\/git#primaryimage"},"image":{"@id":"https:\/\/lastroundai.com\/interview-questions\/git#primaryimage"},"thumbnailUrl":"https:\/\/lastroundai.com\/blog\/wp-content\/uploads\/2026\/07\/iq-git-og.png","datePublished":"2026-07-14T09:14:05+00:00","dateModified":"2026-07-19T05:24:07+00:00","description":"Practice 30 real Git interview questions for 2026: branching, rebase vs merge, reset\/revert, stash, and conflict resolution, with clear answers.","breadcrumb":{"@id":"https:\/\/lastroundai.com\/interview-questions\/git#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/lastroundai.com\/interview-questions\/git"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/lastroundai.com\/interview-questions\/git#primaryimage","url":"https:\/\/lastroundai.com\/blog\/wp-content\/uploads\/2026\/07\/iq-git-og.png","contentUrl":"https:\/\/lastroundai.com\/blog\/wp-content\/uploads\/2026\/07\/iq-git-og.png","width":1200,"height":630,"caption":"Git interview questions \u2014 LastRoundAI"},{"@type":"BreadcrumbList","@id":"https:\/\/lastroundai.com\/interview-questions\/git#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/lastroundai.com\/blog"},{"@type":"ListItem","position":2,"name":"Interview Questions","item":"https:\/\/lastroundai.com\/interview-questions"},{"@type":"ListItem","position":3,"name":"Git Interview Questions (2026): 30 Most Commonly Asked, With Answers"}]},{"@type":"WebSite","@id":"https:\/\/lastroundai.com\/blog\/#website","url":"https:\/\/lastroundai.com\/blog\/","name":"LastRound AI","description":"Interview Assistant prep, tech careers and AI tools","publisher":{"@id":"https:\/\/lastroundai.com\/blog\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/lastroundai.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/lastroundai.com\/blog\/#organization","name":"LastRound AI","url":"https:\/\/lastroundai.com\/blog\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/lastroundai.com\/blog\/#\/schema\/logo\/image\/","url":"https:\/\/lastroundai.com\/blog\/wp-content\/uploads\/2026\/06\/lastroundai-transprant-logo-optimized-BxEo2Wtq.png","contentUrl":"https:\/\/lastroundai.com\/blog\/wp-content\/uploads\/2026\/06\/lastroundai-transprant-logo-optimized-BxEo2Wtq.png","width":400,"height":400,"caption":"LastRound AI"},"image":{"@id":"https:\/\/lastroundai.com\/blog\/#\/schema\/logo\/image\/"}}]}},"_links":{"self":[{"href":"https:\/\/lastroundai.com\/blog\/wp-json\/wp\/v2\/iq\/1155","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/lastroundai.com\/blog\/wp-json\/wp\/v2\/iq"}],"about":[{"href":"https:\/\/lastroundai.com\/blog\/wp-json\/wp\/v2\/types\/iq"}],"author":[{"embeddable":true,"href":"https:\/\/lastroundai.com\/blog\/wp-json\/wp\/v2\/users\/3"}],"replies":[{"embeddable":true,"href":"https:\/\/lastroundai.com\/blog\/wp-json\/wp\/v2\/comments?post=1155"}],"version-history":[{"count":4,"href":"https:\/\/lastroundai.com\/blog\/wp-json\/wp\/v2\/iq\/1155\/revisions"}],"predecessor-version":[{"id":1748,"href":"https:\/\/lastroundai.com\/blog\/wp-json\/wp\/v2\/iq\/1155\/revisions\/1748"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/lastroundai.com\/blog\/wp-json\/wp\/v2\/media\/1701"}],"wp:attachment":[{"href":"https:\/\/lastroundai.com\/blog\/wp-json\/wp\/v2\/media?parent=1155"}],"wp:term":[{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/lastroundai.com\/blog\/wp-json\/wp\/v2\/tags?post=1155"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}