Git Interview Questions Every Developer Should Know
Git questions in interviews check whether you can undo mistakes safely, not whether you know every command. Six common questions are answered by running Git on a throwaway repository: merge versus rebase, the three reset modes, revert, recovering a lost commit, why .gitignore ignored nothing, and bisect.
GloryTecks Team
Training & Placement Faculty
Last updated: 9 October 2026
Git interview questions are rarely about obscure commands. They test whether you can work with others without losing anyone's work: how history is combined, how mistakes are undone, and how to find the change that broke something. The answers below come from running real Git commands on a small scripted repository, with fixed author names and dates so that every commit ID repeats exactly when you run it yourself.
The repository is a tiny model project: a model.py holding a decision threshold, a feature branch that changes it, and later a run of tuning commits.
import os
import shutil
import stat
import subprocess
REPO = os.path.abspath("demo-repo")
def wipe(path): # Git marks object files read-only, which blocks rmtree on Windows
for root, _, files in os.walk(path):
for name in files:
os.chmod(os.path.join(root, name), stat.S_IWRITE)
shutil.rmtree(path, ignore_errors=True)
wipe(REPO)
os.makedirs(REPO)
clock = iter(range(1_700_000_000, 1_800_000_000, 3600)) # one fake hour per commit, so hashes repeat
def git(*args, check=True):
when = f"{next(clock)} +0000"
env = {**os.environ, "GIT_AUTHOR_NAME": "Asha", "GIT_AUTHOR_EMAIL": "asha@example.com", "GIT_AUTHOR_DATE": when,
"GIT_COMMITTER_NAME": "Asha", "GIT_COMMITTER_EMAIL": "asha@example.com", "GIT_COMMITTER_DATE": when}
out = subprocess.run(["git", "-c", "core.autocrlf=false", "-c", "advice.detachedHead=false", *args],
cwd=REPO, env=env, capture_output=True, text=True)
if check and out.returncode:
raise RuntimeError(out.stderr)
return out.stdout.strip()
def commit(path, text, message):
with open(os.path.join(REPO, path), "w", newline="\n") as f:
f.write(text)
git("add", path)
git("commit", "-q", "-m", message)
git("init", "-q", "-b", "main")
commit("model.py", "THRESHOLD = 0.5\n", "first model")
git("switch", "-q", "-c", "feature")
commit("model.py", "THRESHOLD = 0.4\n", "lower threshold")
commit("notes.md", "why 0.4\n", "explain threshold")
git("switch", "-q", "main")
commit("README.md", "demo\n", "add readme")
print("G1 merge vs rebase")
git("switch", "-q", "-c", "rebased", "feature")
git("rebase", "-q", "main") # replay the feature commits on top of main
print(" rebase:", git("log", "--format=%s").replace("\n", " <- "))
git("switch", "-q", "main")
git("merge", "-q", "--no-edit", "feature")
print(" merge: ", git("log", "--format=%s").replace("\n", " <- "))
print(" parents of the merge commit:", len(git("log", "-1", "--format=%P").split()),
"| commit IDs of 'lower threshold' on feature vs rebased:",
git("rev-parse", "--short", "feature~1"), "vs", git("rev-parse", "--short", "rebased~1"))
print("\nG2 reset --soft, --mixed, --hard (undoing the last commit)")
git("switch", "-q", "main")
for mode in ("--soft", "--mixed", "--hard"):
git("branch", "-f", "scratch", "main")
git("switch", "-q", "scratch")
git("reset", "-q", mode, "HEAD~1")
status = ", ".join(git("status", "--short").splitlines()) or "(clean)"
print(f" {mode:<8} HEAD is now '{git('log', '-1', '--format=%s')}', status: {status}")
git("switch", "-q", "-f", "main")
print("\nG3 revert keeps history; reset rewrites it")
commit("model.py", "THRESHOLD = 0.9\n", "bad threshold")
git("revert", "--no-edit", "HEAD")
print(" after revert:", git("log", "-3", "--format=%s").replace("\n", " <- "), "| file:", open(os.path.join(REPO, "model.py")).read().strip())
print("\nG4 recovering a commit after reset --hard")
lost = git("rev-parse", "--short", "HEAD")
git("reset", "-q", "--hard", "HEAD~2")
print(" log no longer shows it:", lost not in git("log", "--format=%h"))
found = git("reflog", "--format=%h %gs").splitlines()[1]
print(" reflog entry:", found)
git("reset", "-q", "--hard", found.split()[0])
print(" restored HEAD:", git("log", "-1", "--format=%s"))
print("\nG5 .gitignore does not untrack a tracked file")
commit("secrets.env", "KEY=123\n", "oops, committed secrets")
with open(os.path.join(REPO, ".gitignore"), "w") as f:
f.write("*.env\n")
git("add", ".gitignore")
git("commit", "-q", "-m", "ignore env files")
print(" still tracked:", "secrets.env" in git("ls-files"), "| check-ignore says ignored:", bool(git("check-ignore", "--no-index", "secrets.env", check=False)))
git("rm", "-q", "--cached", "secrets.env")
git("commit", "-q", "-m", "stop tracking secrets")
print(" after rm --cached: tracked", "secrets.env" in git("ls-files"), "| still in history:", bool(git("log", "--format=%h", "--", "secrets.env")))
print("\nG6 git bisect run: which of 16 commits broke the check?")
with open(os.path.join(REPO, "check.py"), "w") as f:
f.write("import model\nraise SystemExit(0 if model.THRESHOLD < 0.6 else 1)\n")
git("add", "check.py")
git("commit", "-q", "-m", "add check")
good = git("rev-parse", "HEAD")
for i in range(1, 17):
commit("model.py", f"THRESHOLD = {0.40 + 0.01 * i:.2f}\n" if i < 11 else f"THRESHOLD = {0.60 + 0.01 * i:.2f}\n", f"tune {i}")
git("bisect", "start", "HEAD", good)
result = git("bisect", "run", "python", "-B", "check.py") # -B: never reuse a stale model.pyc
first_bad = next(l for l in result.splitlines() if "is the first bad commit" in l)
steps = result.count("running")
print(f" bisect ran the check {steps} times; first bad commit message: '{git('log', '-1', '--format=%s', first_bad.split()[0])}'")
git("bisect", "reset")
wipe(REPO)G1 merge vs rebase
rebase: explain threshold <- lower threshold <- add readme <- first model
merge: Merge branch 'feature' <- add readme <- explain threshold <- lower threshold <- first model
parents of the merge commit: 2 | commit IDs of 'lower threshold' on feature vs rebased: d4025d5 vs bfc1c48
G2 reset --soft, --mixed, --hard (undoing the last commit)
--soft HEAD is now 'add readme', status: M model.py, A notes.md
--mixed HEAD is now 'add readme', status: M model.py, ?? notes.md
--hard HEAD is now 'add readme', status: (clean)
G3 revert keeps history; reset rewrites it
after revert: Revert "bad threshold" <- bad threshold <- Merge branch 'feature' | file: THRESHOLD = 0.4
G4 recovering a commit after reset --hard
log no longer shows it: True
reflog entry: 69de4da revert: Revert "bad threshold"
restored HEAD: Revert "bad threshold"
G5 .gitignore does not untrack a tracked file
still tracked: True | check-ignore says ignored: True
after rm --cached: tracked False | still in history: True
G6 git bisect run: which of 16 commits broke the check?
bisect ran the check 4 times; first bad commit message: 'tune 11'G1. What is the difference between merge and rebase?
Both combine work from two branches; they leave different histories. Merging feature into main created a merge commit with two parents and kept both lines of history. Rebasing reapplied the feature commits on top of main (Git: rebase), giving one straight line: first model, readme, then the two feature commits. The detail interviewers look for is in the last line of G1: the rebased "lower threshold" commit has a new ID, bfc1c48 instead of d4025d5. Rebase does not move commits; it creates new ones.
The rule that follows: never rebase commits other people have already pulled, because their copies still point at the old IDs. Rebase your own local work; merge shared branches.
G2. What do reset --soft, --mixed and --hard do?
All three move the branch back to an earlier commit; they differ in what happens to the changes (Git: reset). Undoing the merge commit:
- soft kept the changes staged:
M model.py, A notes.md. - mixed, the default, kept them in the working tree but unstaged:
M model.py, ?? notes.md. - hard threw them away: the working tree was clean.
Use soft to redo a commit message or squash, mixed to restage selectively, and hard only when you are sure, or know about G4.
G3. How do you undo a commit that is already pushed?
With git revert, which records a new commit that reverses the effect of an earlier one (Git: revert). After a "bad threshold" commit set the value to 0.9, the revert commit sat on top of it and the file was back to 0.4. History still shows both the mistake and its undoing, which is exactly what a shared branch needs: nobody else's copy is invalidated.
G4. You ran reset --hard on the wrong commit. Is the work lost?
Usually not. After reset --hard HEAD~2, the revert commit disappeared from git log, but the reflog, which records where branch tips and HEAD have pointed (Git: reflog), still listed it. Resetting back to that entry restored it. The reflog is local and its entries expire eventually, so act soon, and remember that changes never committed at all cannot be recovered this way.
G5. Why is a file still tracked after adding it to .gitignore?
Because .gitignore only applies to untracked files; files already tracked are not affected (Git: gitignore). A committed secrets.env stayed tracked even after *.env was added, although check-ignore confirmed the pattern matched. git rm --cached stopped tracking it without deleting the file from disk. But the last line of G5 is the important one: the file is still in history. For a real secret, rotate the credential immediately, then consider rewriting history. Removing the file from the next commit does not un-leak it.
G6. How do you find which commit introduced a bug?
git bisect, a binary search over history (Git: bisect). Mark a known good commit and a known bad one, and Git checks out the midpoint for testing. With git bisect run and a script that exits 0 for good and 1 for bad, the whole search is automatic. Across 16 tuning commits, it ran the check 4 times and named "tune 11", the commit where the threshold first crossed 0.6. Sixteen commits need four tests; a thousand would need about ten.
One trap appeared while building this example. The check imported model.py, and Python reused a stale compiled copy from an earlier checkout, because the file's size and timestamp had not changed. Bisect blamed the wrong commit until the check ran with python -B, which skips the bytecode cache. A bisect script must give the same answer for the same code, every time.
G7. What is a detached HEAD?
HEAD normally points at a branch, and new commits move that branch forward. Check out a commit ID or a tag directly and HEAD points at the commit itself: detached. Bisect works this way. Commits made there belong to no branch and are easy to lose once you switch away, so create a branch with git switch -c before keeping any work.
How to answer Git questions
- Say whether the commits are shared before choosing rebase, reset or revert.
- Know where to recover from: the reflog for commits, nothing for uncommitted changes.
- Treat a committed secret as leaked, whatever you do to the repository afterwards.
- Automate bug hunts with bisect and a reliable check.
Version control is the base of the automation in CI/CD for machine learning and GitHub Actions for ML. Data and models need their own versioning, covered in data versioning with DVC, and credentials belong in a secret store, as in secrets management for ML pipelines. The wider round is in MLOps interview questions and answers.
What are the most common Git interview questions?
Should I use merge or rebase?
How do I undo the last commit in Git?
How do I remove a file from Git but keep it locally?
Version control for ML projects is part of the MLOps course.
Sources
- git-rebase documentation — Git project
- git-reset documentation — Git project
- git-revert documentation — Git project
- git-reflog documentation — Git project
- gitignore documentation — Git project
- git-bisect documentation — Git project
GloryTecks Team
Training & Placement Faculty
The GloryTecks faculty team mentors learners across Data Science, AI and Data Engineering, with a focus on real-time projects and placement readiness in Hyderabad.
Want to master Interview Questions?
Join GloryTecks’ expert-led MLOps training in Hyderabad with real-time projects and 100% placement support.
Related articles
SQL Interview Questions: Top 50 with Answers
Fifty SQL interview questions with answers checked on PostgreSQL, DuckDB and SQLite against one small practice schema: evaluation order, NULL logic, joins and fan-out, window functions, dates, and performance, including where the three engines disagree.
Python Interview Questions for Data Roles
The plain-Python coding tasks data-analyst and data-engineer interviews use, without pandas: counting, deduplicating, grouping, streaming large files, flattening JSON, joining, and the bugs interviewers plant. Each has a worked solution and the follow-up questions to expect.
Data Science Interview Questions and Answers 2026
A data science interview is several interviews in one: statistics, machine learning, coding, product sense and a take-home. This guide maps each round with model answers, then works a churn take-home on real telecom data, including the checks that separate a strong submission from a high score.