Git Interview Prep
Practical Git concepts for interviews, daily development, and team workflows.
Git is a distributed version control system that tracks changes in your code over time so you can go back, collaborate, and never lose work.
Think of Git like a time machine for your code. Every time you save a "snapshot" (called a commit), Git remembers exactly what every file looked like at that moment. Mess something up? Roll back. Want to try a crazy idea without breaking the main code? Create a branch.
The "distributed" part means everyone on your team has the full history on their own machine — not just a connection to one central server. So even if the server dies, nobody loses any work.
Git was created by Linus Torvalds in 2005 — the same guy who made Linux — specifically because he was frustrated with how slow and restrictive existing tools were for managing the Linux kernel.
- Git is a VCS (Version Control System), not a platform — GitHub/GitLab are platforms built on top of Git
- It's distributed — every developer has the full repo copy locally
- Git tracks changes, not full file copies (it's smart about storage)
- Core concepts: commits, branches, merges, remotes
Most candidates say "version control tool." Go further: "Git uses a DAG (Directed Acyclic Graph) of commit objects internally, which is why branching is so cheap — it's just a pointer to a commit, not a copy of all files."
Git is the tool (runs on your machine). GitHub is a website that hosts Git repositories in the cloud and adds collaboration features on top.
Git is like the engine. GitHub is like the car dashboard — it makes the engine easier to use and shows you everything nicely. You can use Git completely without GitHub (just local repos), but you can't use GitHub without Git under the hood.
GitHub adds things Git doesn't have: pull requests, issue tracking, Actions (CI/CD), project boards, a web UI to browse code, and social features like stars and forks. Alternatives to GitHub include GitLab, Bitbucket, and Azure DevOps — they all wrap around Git.
- Git = CLI tool, local, no internet needed for most operations
- GitHub = cloud hosting + collaboration layer (owned by Microsoft)
- Pull Requests, Issues, Actions — all GitHub features, not Git
- GitLab / Bitbucket are GitHub alternatives, all use Git underneath
- Saying "I push to Git" when you mean "I push to GitHub." Sounds minor, but interviewers notice.
- Thinking you need internet for git commit, git log, git branch — you don't. Only push/pull/fetch need internet.
A repository (repo) is a folder that Git is tracking — it contains your project files plus the entire history of every change ever made.
When you run git init in a folder, Git creates a
hidden .git directory inside it. That
.git folder IS the repository — it stores every
commit, branch, tag, and config. The files you see in the folder
are just your working copy.
There are two kinds: local repos (on your machine) and remote
repos (on GitHub/GitLab/server). They sync through push and
pull. You can have multiple remote repos — like
origin pointing to your GitHub fork and
upstream pointing to the original project.
-
The
.gitfolder = the actual repo. Delete it and you lose all Git history - Local repo: on your machine. Remote repo: on a server
-
git initcreates a new repo.git clonecopies an existing one - A repo can have multiple remotes (origin, upstream, etc.)
git fetch downloads changes but doesn't touch your
code. git pull downloads AND immediately merges
them into your current branch.
Imagine you're working on a Google Doc with a team, but you
turned off auto-sync. git fetch is like checking
"what did others change?" without applying anything.
git pull is like applying all those changes to your
own doc right now.
git pull is literally just a shortcut for
git fetch followed by git merge. In
teams, many senior engineers prefer git fetch +
manual review before merging, so they know exactly what's coming
in before it hits their branch.
-
git fetch: safe, non-destructive, just updates remote-tracking branches -
git pull: convenient but can cause unexpected merge commits -
git pull --rebase: fetches + rebases instead of merging (cleaner history) - Use fetch when you want to review before merging — good practice in teams
Mention git pull --rebase — it avoids creating a
merge commit for every pull, keeping history linear. Many teams
set this as default:
git config --global pull.rebase true.
Merge keeps both histories and creates a merge commit. Rebase moves your branch's commits on top of another branch, giving a clean, linear history — but rewrites commit history.
Imagine you're writing a book with a friend. Merge is like: "Here's my chapters, here's yours, here's a note saying we combined them on Tuesday." Rebase is like: "Let me rewrite my chapters as if I had started writing after you finished yours" — so the final result looks like a single clean story, with no "combination note" in the middle.
Merge is safer and more honest — it shows the real history of
what happened. Rebase makes history look cleaner and easier to
read with git log, but it literally rewrites commit
hashes. That's why the golden rule of rebase is:
never rebase commits that have already been pushed to a
shared branch.
In practice: teams use merge for integrating feature branches into main (preserving history), and rebase for keeping a feature branch up to date with main (before opening a PR).
- Merge: non-destructive, safe for shared branches, creates a merge commit
- Rebase: rewrites history, cleaner log, NEVER do on shared/public branches
-
Interactive rebase (
git rebase -i) lets you squash, edit, reorder commits before a PR - Golden rule: rebase only local, unpushed commits
git rebase -i HEAD~N lets you modify the last N
commits. You can squash multiple commits into one, reorder
them, change commit messages, or drop commits entirely. Very
useful before code review.
- Rebasing a branch that others are working on will cause them to have conflicting history — serious team headache.
- Rebase changes commit hashes — even if the code is identical, it's technically a new commit.
A branch is just a lightweight pointer to a specific commit. It lets you work on a separate version of the code without touching the main codebase.
Think of the main branch as the "live" train track. When you create a branch, you're laying a new side track from a specific point. You can work on that side track as long as you want, and when your feature is ready, you merge it back into the main track.
Because a branch is literally just a file containing a commit hash (40-character string), creating a branch is basically free and instant. This is very different from SVN (an older VCS) where branching was expensive and slow. In Git, most teams create a new branch for every single feature or bug fix.
-
Default branch is usually called
main(used to bemaster) -
git branch feature-xyzcreates a branch,git checkout -b feature-xyzcreates AND switches to it - Branches are cheap — just pointers, no file duplication
-
You can switch between branches instantly using
git checkoutorgit switch
A branch is just a file in
.git/refs/heads/ containing a 40-char commit SHA.
That's why creating or deleting a branch is O(1) in Git — it's
essentially creating or deleting a single tiny file.
A Pull Request is a formal request to merge your branch into another. It creates a space for code review, discussion, and automated checks before the code goes live.
Without PRs, developers could push anything directly to main. That's chaos in a team. PR is the "checkpoint" — your code needs a second pair of eyes before it can join the main codebase. It's standard in every real-world team.
PRs let reviewers leave comments, suggest changes, approve, or request changes. CI/CD pipelines (like GitHub Actions) run tests automatically on every PR, so bugs get caught before merging. It also creates a documented history of why changes were made.
- PRs are a GitHub/GitLab feature, not a Git feature — Git itself doesn't have PRs
- GitLab calls them "Merge Requests (MRs)" — same concept
- A PR links a source branch to a target branch. Reviewers approve, then it gets merged
- PRs can trigger CI/CD: automated tests, linting, deployments to staging
Git stash temporarily saves your uncommitted changes so you can switch branches or pull updates — without making a commit or losing your work.
Imagine you're halfway through a feature, and your manager says
"quick, fix this prod bug!" You can't commit half-done work. You
can't throw it away. git stash is like a drawer
where you shove your half-finished work, switch to fix the bug,
then come back and pull your work out of the drawer with
git stash pop.
You can have multiple stashes. git stash list shows
them all. git stash pop applies the latest and
removes it from stash. git stash apply applies it
but keeps it in the stash (useful when applying the same changes
to multiple branches).
-
git stash— stash all tracked modified files -
git stash -u— also stash untracked (new) files -
git stash pop— apply top stash and remove it -
git stash apply stash@{2}— apply a specific stash entry git stash drop— delete a stash entry
-
By default, stash doesn't include untracked files. Use
git stash -uor--include-untrackedto include them. - Stashes are local — they don't get pushed to remote.
git reset moves the branch pointer backward
(rewrites history). git revert creates a new commit
that undoes a previous commit (safe for shared branches).
Think of git reset as going back in time and
pretending commits never happened. The commits are gone (or
detached). It's like erasing pages from a book. Safe for local
work — dangerous if you've already pushed.
git revert is the opposite: you add a new page to
the book that says "undo everything on page 37." The original
page 37 is still there. It's transparent, safe for team repos,
and doesn't rewrite history.
Reset has three modes: --soft (uncommit but keep
changes staged), --mixed (default — uncommit,
unstage, keep in working directory),
--hard (uncommit AND delete changes from working
directory — dangerous!).
-
git reset --soft HEAD~1— undo last commit, keep changes staged -
git reset --mixed HEAD~1— undo last commit, unstage changes (default) -
git reset --hard HEAD~1— undo last commit AND delete changes (dangerous!) -
git revert HEAD— create a new undo-commit, history stays intact - Rule: use revert on shared/public branches, reset only on local-only branches
git reflog you can find the commit
hash before the reset and do
git reset --hard <old-hash>. Reflog keeps a
local history of HEAD movements for about 90 days. This is a
lifesaver.
-
git reset --hardafter already pushing — forces you togit push --force, which can overwrite teammates' work on the same branch. - Many beginners use reset where they should use revert — and break the shared branch history.
Mentioning git reflog as the escape hatch for bad
resets immediately separates you from candidates who don't know
it. Reflog tracks all HEAD movements locally — even after
--hard resets.
A merge conflict happens when two branches modify the same part of the same file in different ways, and Git can't automatically decide which version to keep.
Git is pretty smart about merging. If you changed line 5 and your teammate changed line 50, it handles that automatically. But if you both changed line 5? Git throws up its hands and says "I don't know which one you want — you decide." That's a conflict.
Git marks the conflicting section in the file with
<<<<<<<, =======,
and >>>>>>> markers. Above the
======= is your change. Below is theirs. You have
to manually pick one (or combine them) and remove the markers.
<<<<<<< HEAD (your branch) const greeting = "Hello World"; ======= const greeting = "Hey there!"; >>>>>>> feature/update-greeting (their branch) // You manually choose one and delete the markers: const greeting = "Hello World"; // keep yours // Then: git add . && git commit
You open the conflicted file, manually edit it to have the correct final version, remove Git's conflict markers, then stage and commit the resolved file.
Step 1: Git tells you which files have conflicts after
git merge or git pull. Run
git status to see them listed under "Unmerged
paths."
Step 2: Open each conflicted file. Look for the
<<<<<<< markers. Edit the file
to look exactly how you want the final version to be — you're
not restricted to choosing one side, you can blend both if
needed.
Step 3: After fixing the file, run
git add <filename> to mark it as resolved. Do
this for all conflicted files. Then run
git commit to finalize the merge.
Modern editors like VS Code show conflicts with a nice UI —
"Accept Current," "Accept Incoming," "Accept Both." Tools like
git mergetool open a three-way diff viewer.
- 1.
git status— identify conflicted files - 2. Open file, find markers, decide final content, delete markers
- 3.
git add <file>— mark as resolved - 4.
git commit— complete the merge -
5. Optionally use
git mergetoolor VS Code for visual help
Local is the repo on your machine. Remote is the repo on a server (like GitHub). You sync between them using push and pull.
Local repo lives in the .git folder on your laptop.
You can commit, branch, and log without any internet. Remote
repo is hosted online — it's the shared source of truth for the
whole team.
When you git push, you send your local commits to
remote. When you git pull, you bring remote commits
down. A project can have multiple remotes —
origin is just the conventional name for the
primary one.
-
origin= default name for your remote (set when you clone or add a remote) git remote -v— see all configured remotes-
git remote add upstream <url>— add a second remote - You can work offline locally; just push/pull when you have internet
.gitignore is a file that tells Git which files and folders to completely ignore — they won't be tracked, staged, or pushed.
Not everything in your project folder belongs in the repo.
node_modules/ (huge, auto-generated),
.env (has secret API keys),
__pycache__/ (Python compiled files),
dist/ (build output) — all of these should be
ignored.
You put patterns in .gitignore and Git pretends
those files don't exist. The .gitignore file itself
should be committed to the repo so all teammates use the same
rules. A good starting point:
gitignore.io
generates .gitignore files for your specific tech
stack.
# Dependencies node_modules/ # Environment secrets .env .env.local # Build outputs dist/ build/ # OS junk .DS_Store Thumbs.db # Python __pycache__/ *.pyc
-
Adding a file to
.gitignoreafter it's already been committed doesn't remove it. You need to rungit rm --cached <file>first. -
Never commit
.envfiles with real secrets — people forget this and push credentials to public GitHub repos.
Clone copies a repo to your local machine. Fork copies a repo to your GitHub account (creating your own cloud copy) — then you typically clone your fork.
Fork is a GitHub/GitLab concept, not a Git command. When you fork a public project (like an open-source library), you get your own copy in your GitHub account where you have full push access. Then you clone THAT fork to your machine, make changes, and send a Pull Request to the original project.
Clone is a pure Git operation — it just downloads a repo to your machine. The origin remote is set to whatever URL you cloned from.
- Fork = server-side copy (GitHub account → GitHub account)
- Clone = local copy (remote server → your machine)
- Open source workflow: Fork → Clone fork locally → Push to your fork → PR to original
- Direct team members usually just clone the main repo (no fork needed)
HEAD is a pointer to your current position in the repo — usually pointing to the latest commit on your current branch.
You can think of HEAD as "you are here" on the Git map. It's a
file in .git/HEAD that usually contains a reference
to a branch name (like ref: refs/heads/main). When
you make a new commit, HEAD moves forward to that new commit.
When you switch branches, HEAD updates to point to that branch.
HEAD~1 means "one commit before HEAD."
HEAD~3 means "three commits back." This syntax is
used heavily with reset, log, and rebase commands.
- HEAD normally points to a branch, which points to a commit
-
HEAD~1= parent commit,HEAD~2= grandparent git logshows HEAD's current position- Detached HEAD = HEAD points directly to a commit, not a branch
git add . stages all changed files in the current
directory. git add <file> stages only that
specific file.
git add . is convenient but can accidentally stage
files you didn't mean to commit (debug logs, temporary test
files). git add <file> gives you precise
control. In professional teams, many engineers use
git add -p (patch mode) to stage changes chunk by
chunk — this lets you make one clean commit even if you changed
multiple unrelated things in the same session.
-
git add .— all changed files in current and subdirectories -
git add -A— all changes including deleted files across whole repo -
git add -p— interactive mode, stage specific chunks/hunks -
git statusbefore adding — know what you're staging
Detached HEAD happens when HEAD points directly to a commit instead of a branch. You can browse and even commit, but those commits will be lost if you switch away without creating a branch.
Normally: HEAD → branch → commit. In detached HEAD: HEAD → commit (no branch in between). It's like stepping off the main track onto a platform — you can look around, even write things down, but if you leave without marking where you are, you'll never find it again.
This happens when you
git checkout <commit-hash> or
git checkout v1.0.0 (a tag). It's useful for
inspecting old code. But if you make new commits here and then
git checkout main, those commits become unreachable
— "dangling commits" that Git will eventually garbage-collect.
Fix: before switching away, run
git branch my-save-branch to create a branch
pointing to where you are. Now it's safe.
-
Triggered by:
git checkout <commit-hash>orgit checkout <tag> - Safe for browsing old code. Risky if you start committing
-
Fix:
git branch rescue-branchto save commits, then checkout back to main - Git explicitly warns you when you enter detached HEAD state
Detached HEAD commits aren't immediately deleted —
git reflog keeps them accessible for ~90 days. So
even if you accidentally left detached HEAD without creating a
branch, you can recover those commits via reflog.
Teams use structured workflows — most commonly Git Flow or Trunk-Based Development — to coordinate how branches are created, reviewed, and merged without chaos.
Feature Branch Workflow (most common): Everyone branches off main for their feature, pushes when done, opens a PR, gets code reviewed, then merges back. Main is always deployable.
Git Flow: Has separate main,
develop, feature/*,
release/*, and hotfix/* branches. Used
in projects with scheduled release cycles. More structure, more
overhead.
Trunk-Based Development: Everyone pushes small,
frequent commits directly to main (or short-lived
feature branches that merge daily). Preferred by Google,
Facebook, Netflix. Requires good CI/CD and feature flags. Moves
fast, less merge pain.
- Feature Branch: branch off main → work → PR → review → merge — simple and universal
- Git Flow: structured with develop/release branches — good for release cycles
- Trunk-Based: small frequent commits to main — fast, needs CI/CD + feature flags
- PRs always go through at least one reviewer in real teams
Knowing the difference between Git Flow and Trunk-Based Development — and being able to say which one you've used and why — shows genuine team experience. Most candidates only know "we used branches and PRs."
Depends on whether the commit is local or already pushed. For local: use git reset. For pushed: use git revert to avoid rewriting shared history.
If you just committed locally and want to undo it:
git reset --soft HEAD~1 undoes the commit but keeps
your changes staged.
git reset --mixed HEAD~1 (default) unstages them
too but keeps files.
git reset --hard HEAD~1 deletes the changes
entirely.
If it's already pushed to a shared branch:
git revert HEAD creates a new "undo commit" on top.
Everyone on the team can pull this cleanly without history
conflicts. Never git push --force on shared
branches unless you're absolutely sure and have team agreement.
-
Local only, keep changes staged:
git reset --soft HEAD~1 -
Local only, unstage changes:
git reset --mixed HEAD~1 -
Local only, delete changes:
git reset --hard HEAD~1 -
Already pushed, shared branch:
git revert HEAD -
Already pushed, your own branch:
git reset + git push --force-with-lease
-
git push --forceon a shared branch can overwrite teammates' commits. Use--force-with-leaseinstead — it fails if someone else has pushed since your last fetch.
Systematically isolate whether the issue is a merge conflict that wasn't resolved correctly, a dependency difference, an environment difference, or a regression introduced by the merged branch.
This is a classic real-world scenario. First, look at what was
actually merged. Run git log --merge to see commits
that caused the conflict, or
git diff HEAD~1 HEAD to see what changed in the
latest merge commit.
Second, check for "resolved" conflicts where someone chose the
wrong side. These are the sneakiest bugs — Git shows the
conflict as resolved but the logic is now broken. Search the
diff for lines around
<<<<<<< markers that might've
been removed but left incorrect code.
Third, use git bisect — an incredibly powerful
command that does a binary search through your commit history to
find the exact commit that introduced the bug. You run
git bisect start, mark the current commit as bad
(git bisect bad), mark a known-good older commit as
good (git bisect good <hash>), and Git checks
out commits in between for you to test. Takes O(log n) steps to
find the culprit.
-
git diff main..feature-branch— see all changes that came in - Look for incorrectly resolved conflicts — logic broken silently
-
git log --oneline --graph— visualize the merge structure -
git bisect— binary search to find the exact breaking commit - Check environment: env vars, config files, dependencies (package-lock.json changes)
git bisect automates binary search through git
history to find which commit introduced a bug. You mark a
"bad" commit and a "good" commit, and Git checks out the
midpoint. You test and mark it good or bad. Repeat until you
find the culprit. Much faster than checking each commit
manually.
git show <merge-commit-hash> shows the
diff. Or git diff HEAD~1 HEAD for the latest. For
a merge commit with two parents,
git diff HEAD^1 HEAD^2 shows the diff between the
two merged branches.
Knowing git bisect and being able to explain it
clearly — with the analogy of binary search — is something very
few junior/mid engineers can do. It genuinely impresses senior
interviewers because it shows you debug systematically, not
randomly.
Ready to test Git?
Lock in branching, reset/revert, rebase, stash, and collaboration workflows.