The 60 Git questions interviewers actually ask, with direct answers, runnable commands, and what the interviewer is listening for. Grouped for freshers, intermediate, and experienced rounds.
60 questions with answersKey Takeaways
Git is a distributed version control system created by Linus Torvalds in 2005 to manage Linux kernel development. It tracks changes to a set of files over time, and because it's distributed, every developer's clone is a complete repository with the full history, not a thin working copy tied to a central server. Git stores content as compressed snapshots identified by SHA-1 (now moving to SHA-256) hashes, and a branch is just a lightweight movable pointer to a commit, which is why creating and switching branches costs almost nothing. In interviews, Git questions probe how well you understand the model: the difference between the working directory, staging area, and repository; what merge and rebase actually do; and how to recover when something goes wrong. This page collects the 60 questions that come up most, each with a direct answer and runnable commands. The official Git reference documentation from the Git project is the canonical source for exact behavior, and increasingly the first coding round runs as a live AI interview, so pair this bank with our AI interview preparation guides for the format side.
Watch: Learn Git: Full Course for Beginners
Video: Learn Git: Full Course for Beginners (freeCodeCamp.org, YouTube)
Test yourself and earn a certificate
10 quick questions. Score 70%+ to download your Git certificate.
The fundamentals every entry-level round checks. If any answer here surprises you, that's your study list.
Git is a distributed version control system that tracks changes to files over time. It lets many people work on the same codebase in parallel, keeps a full history of every change, and makes it possible to go back to any earlier state.
Teams use it because it's distributed: every clone is a complete repository with the full history, so you can commit and branch offline, and there's no single server whose outage stops all work. Branching is cheap, which makes feature branches and code review practical.
Key point: A one-line definition plus one concrete benefit (offline commits, cheap branching) beats reciting a feature list. This is a warm-up; the key signal is how you organize an answer.
Watch a deeper explanation
Video: Git Explained in 100 Seconds (Fireship, YouTube)
Git is the version control tool that runs on your machine and tracks changes. GitHub is a hosting service for Git repositories that adds a web interface, pull requests, issues, and access control on top.
You can use Git with no GitHub at all, pushing to any remote (GitLab, Bitbucket, a bare repo on a server). GitHub is one popular place to host the remote, not part of Git itself.
Key point: Mixing these up is a common junior tell. State plainly that Git is the tool and GitHub is a host, and you clear the bar.
In a distributed system, every developer's clone contains the entire repository, including the full history, not just the latest files. Compare that to a centralized system like Subversion, where the history lives only on a server and each working copy is a thin snapshot tied to it.
The practical effect: you can commit, branch, diff, and browse history with no network connection, and any clone can act as a backup or become the new central copy if the main server dies.
Key point: Contrasting it with a centralized system in one sentence is what the question needs. The offline-work benefit is the concrete payoff to name.
A repository is the project's storage: all your files plus a hidden .git folder that holds the complete history, branches, tags, and configuration. Everything Git tracks lives in that .git directory, which is the single thing that turns an ordinary folder into a version-controlled project.
You create one with git init in a new project, or get a copy of an existing one with git clone. Delete the .git folder and you have plain files with no history.
git init my-project # start a new repository
cd my-project
ls -a # you will see a .git directoryKey point: Pointing out that the whole repository lives in .git shows you understand where history is stored, not just how to run commands.
git init creates a brand new, empty repository in the current directory. git clone copies an existing repository (usually from a remote URL) including its full history and sets up the remote for you.
Use init when you're starting fresh. Use clone when you want a local copy of a project that already exists somewhere.
git init # new empty repo here
git clone https://github.com/user/repo.git # copy an existing oneKey point: A crisp 'init starts fresh, clone copies an existing repo with its history' is all this needs. It's a warm-up.
The working directory is your actual files on disk. The staging area (also called the index) is where you assemble the exact set of changes for the next commit. The repository is the committed history stored in .git.
The flow is: edit files (working directory), git add to move chosen changes into staging, git commit to record staging into the repository. The staging area is what lets you commit some changes and leave others for later.
| Area | What it holds | Command to move forward |
|---|---|---|
| Working directory | Your edited files | git add |
| Staging area (index) | Changes queued for next commit | git commit |
| Repository (.git) | Committed history | git push (to a remote) |
Key point: Drawing the three-area flow, even verbally, is the answer interviewers hope for. The staging area is the concept juniors most often miss.
Watch a deeper explanation
Video: How Git Works: Explained in 4 Minutes (ByteByteGo, YouTube)
git add moves changes from your working directory into the staging area, choosing what goes into the next commit. git commit records everything staged as a new snapshot in the repository, with a message describing the change.
A commit is permanent history with a unique hash, an author, a timestamp, and a pointer to its parent. Nothing leaves your machine until you push.
git add index.js styles.css # stage specific files
git add . # stage everything changed
git commit -m "Add login form"Key point: Stress that add and commit are two separate steps for a reason: staging lets you commit part of your work. That's the insight juniors miss.
git status shows the current state of your working directory and staging area: which files are staged, which are modified but not staged, and which are untracked. It also tells you the current branch and whether you're ahead of or behind the remote.
It's the command you run constantly to see where you stand before adding, committing, or pushing.
Key point: Saying you run git status constantly, before every add and commit, indicates good habits rather than memorized theory.
git log shows the commit history, newest first, with each commit's hash, author, date, and message. Flags shape the output: --oneline for a compact view, --graph to see branch structure, and -n to limit the count.
For what changed in each commit, add -p (the patch). For a quick summary of who touched a file, git blame is the tool.
git log --oneline --graph --all # compact, with branch structure
git log -p src/app.js # history with diffs for one fileKey point: Knowing --oneline and --graph exist, not memorizing every flag, is the bar. Mentioning git blame as the per-line companion is a bonus.
A branch is a lightweight, movable pointer to a commit. Creating a branch just writes a new pointer, so it's instant and cheap, which is why Git encourages a branch per feature or fix.
The default branch is usually called main (older repos use master). Work happens on branches, and finished work merges back into the main line.
git branch # list branches
git branch feature-login # create a new branch
git switch feature-login # move onto it (or: git checkout)Key point: The phrase 'a branch is just a movable pointer to a commit' is what the key point is. It explains why branching is instant.
git switch -c name creates a new branch and moves onto it in one step. The older equivalent is git checkout -b name. To just move to an existing branch, use git switch name.
Newer Git splits the old, overloaded git checkout into git switch (for branches) and git restore (for files), which is worth mentioning as awareness.
git switch -c feature-x # create and switch (modern)
git checkout -b feature-x # same thing (older syntax)
git switch main # switch to an existing branchKey point: Knowing both git switch -c and git checkout -b covers new and older codebases. Naming the switch/restore split indicates current.
A commit is a snapshot of your entire tracked project at a moment in time, plus metadata: the author, a timestamp, a message, and a pointer to the parent commit (or commits, for a merge). Each commit has a unique hash derived from its content.
Because a commit points to its parent, the history forms a chain (technically a directed graph). Git stores snapshots, not diffs, though it compresses them so identical content isn't duplicated.
Key point: Saying 'Git stores snapshots, not diffs' is a small detail that separates people who understand the model from people who just run commands.
A remote is a named reference to another copy of the repository, usually on a server. origin is the conventional name for the remote you cloned from. You push commits to a remote to share them and pull to get others' work.
A repo can have several remotes. git remote -v lists them with their URLs.
git remote -v # list remotes
git remote add upstream https://github.com/org/repo.gitKey point: Knowing origin is just a conventional name, not something special, and that a repo can have several remotes, shows you understand remotes as pointers.
git push sends your local commits up to a remote branch, sharing your work with the rest of the team. git pull does the opposite: it brings remote commits down and integrates them into your current branch, so you get everyone else's work. Push is outgoing, pull is incoming.
Push is outgoing, pull is incoming. Under the hood pull is a fetch (download) followed by a merge or rebase.
git push origin main # send local commits up
git pull origin main # fetch and integrate remote commitsKey point: Adding that pull is a fetch plus a merge under the hood sets up the fetch-vs-pull follow-up before it's asked.
git fetch downloads new commits and refs from the remote but doesn't touch your working branch or your files. git pull does a fetch and then immediately merges (or rebases) those downloaded changes into your current branch. So pull is really fetch plus an automatic integration step you don't get to review first.
Fetch is the safe, look-before-you-leap option: you can inspect what changed with git log origin/main before deciding to merge.
Key point: Saying you prefer fetch then review over blind pull indicates careful. Interviewers like candidates who inspect before integrating.
A .gitignore file lists patterns for files and folders Git should not track, like build output, dependencies such as node_modules, log files, and secrets. Git skips any matching untracked file so it never lands in a commit by accident, which keeps generated and sensitive files out of the repository.
One catch: .gitignore only affects untracked files. If a file is already tracked, adding it to .gitignore does nothing until you untrack it with git rm --cached.
# .gitignore
node_modules/
*.log
.env
dist/Key point: The follow-up is often 'why is my ignored file still showing up?'. Have the already-tracked answer and git rm --cached ready.
git diff shows line-by-line changes. With no arguments it shows unstaged changes (working directory versus staging). git diff --staged shows staged changes versus the last commit, and git diff branchA branchB compares two branches.
It's how you review exactly what you're about to commit before committing it.
git diff # unstaged changes
git diff --staged # what is staged for the next commit
git diff main..dev # differences between two branchesKey point: The distinction between git diff (unstaged) and git diff --staged trips people up. Getting it right signals you actually review before committing.
Cloning is a Git operation: you copy an existing repository, with its full history, down to your local machine. Forking is a host feature on GitHub or GitLab: you create your own server-side copy of someone else's repository under your account, which you can then clone and push to freely.
The typical open-source flow forks the upstream repo, clones your fork locally, and opens pull requests back to the original. Forking gives you a copy you can push to when you don't have write access to the original.
Key point: Naming that fork is a host feature and clone is a Git operation is the clean answer. The open-source contribution flow is the natural follow-up.
HEAD is a pointer to your current location in history, almost always the tip of the branch you have checked out. When you commit, HEAD (and its branch) move forward to the new commit.
HEAD~1 means one commit before HEAD, HEAD~2 means two before, and so on. A detached HEAD means you've checked out a specific commit directly rather than a branch.
Key point: Understanding HEAD~1 notation is what makes reset and rebase answers land later. Interviewers ask this early to set those up.
For changes in the working directory, git restore file undoes them (older syntax: git checkout -- file). To unstage a file you already added, use git restore --staged file. To wipe all untracked files, git clean -fd.
Be careful: discarding uncommitted changes can't be recovered by Git, because they were never committed.
git restore app.js # throw away working-directory edits
git restore --staged app.js # unstage but keep the edits
git clean -fd # remove untracked files and foldersKey point: Flagging that discarded uncommitted work can't be recovered shows caution. Interviewers watch for whether you treat destructive commands carefully.
You get the full repository: every commit in the history, all branches and tags recorded as remote-tracking refs, and the complete .git directory, plus a working copy of the default branch checked out on disk. It's a complete copy, not a thin snapshot, which is exactly what makes Git distributed.
Because you have the whole history, you can work entirely offline. The clone also records where it came from as the remote named origin.
Key point: Saying 'the whole history, not just the latest files' reinforces the distributed model. That phrase is what this question is really checking.
A short imperative summary line (about 50 characters, like 'Fix null check on login'), a blank line, then a body explaining why the change was made if it isn't obvious. Present tense, imperative mood, matches Git's own conventions.
Good messages make history readable and make git log, git blame, and code review far more useful months later. Interviewers who ask this care about team habits, not syntax.
Key point: This is a culture question in disguise. The imperative-mood summary line and a 'why' body signal you've worked on a team, not just solo projects.
A tracked file is one Git already knows about, because it was committed or staged at least once. An untracked file is new and Git isn't following it yet, so it won't be included in a commit until you git add it.
git status lists untracked files separately. .gitignore controls which untracked files Git stops bothering you about.
Key point: Interviewers use this to lead into the .gitignore 'why is my file still tracked' gotcha. Have that follow-up ready.
git checkout is the old, overloaded command that both switched branches and restored files, which confused people. Newer Git split it into two clearer commands: git switch changes branches, and git restore discards or unstages file changes.
checkout still works and does both jobs, but switch and restore make your intent obvious. Knowing the split shows you keep up with Git, and it prevents the classic accident of checking out a file when you meant a branch.
git switch main # change branch (was: git checkout main)
git restore app.js # discard file edits (was: git checkout -- app.js)
git restore --staged app.js # unstageKey point: switch and restore replaced the overloaded checkout.
For candidates with working experience: merging, rebasing, undoing mistakes, and the judgment questions that separate users from understanders.
Both integrate changes from one branch into another. Merge creates a new merge commit that ties the two histories together, preserving the true branch shape. Rebase replays your branch's commits on top of another branch, producing a linear history but rewriting each commit's hash.
Use merge to keep an honest record of what happened, especially on shared branches. Use rebase to tidy up local, unpushed work into a clean line before sharing. The golden rule: never rebase commits others have already pulled.
# integrate main into your feature branch
git switch feature
git merge main # merge commit, preserves history
# or
git rebase main # linear history, rewrites your commits| Merge | Rebase | |
|---|---|---|
| History shape | Branched, with a merge commit | Linear |
| Commit hashes | Preserved | Rewritten |
| Safe on shared branches | Yes | No, only local commits |
| Best for | Honest, shared history | Cleaning up local work |
Key point: The rule is: never rebase commits that others have already pulled.
Watch a deeper explanation
Video: Git MERGE vs REBASE: Everything You Need to Know (ByteByteGo, YouTube)
git reset moves the current branch pointer to an earlier commit, effectively rewriting history from that point. git revert leaves history intact and creates a new commit that undoes the changes of a target commit.
The rule of thumb: use revert on shared or pushed branches because it doesn't rewrite history, and reset for local commits nobody has seen. Reverting is safe; resetting a public branch forces everyone else into a painful recovery.
git revert abc123 # new commit that undoes abc123 (safe on shared)
git reset --hard HEAD~1 # rewrites history, drops the last commit (local)Key point: 'Which would you use on a branch teammates already pulled?' Answer revert, and explain that reset would rewrite shared history.
All three move the branch pointer, but they differ in what they do to the staging area and working directory. --soft moves the pointer and keeps everything staged. --mixed (the default) unstages the changes but keeps them in the working directory. --hard discards the changes entirely.
So --soft is safest (undo the commit, keep the changes ready to recommit), and --hard is the destructive one that throws work away.
| Mode | Branch pointer | Staging area | Working directory |
|---|---|---|---|
| --soft | Moved | Kept (staged) | Kept |
| --mixed (default) | Moved | Reset (unstaged) | Kept |
| --hard | Moved | Reset | Discarded |
Key point: Interviewers love this one because --hard is where people lose work. Naming what each mode touches, staging and working directory, is the full answer.
When Git can't automatically combine two changes to the same lines, it marks the file with conflict markers (<<<<<<<, =======, >>>>>>>) and pauses the merge. You open each conflicted file, decide what the final content should be, remove the markers, then stage the resolved files and finish the merge.
The steps: git status to see conflicted files, edit each one, git add them, then git commit (or git merge --continue). git merge --abort backs out entirely.
git status # lists conflicted files
# edit files, remove <<<<<<< ======= >>>>>>> markers
git add resolved-file.js
git commit # completes the merge
# or bail out:
git merge --abortKey point: Walk the steps calmly: see conflicts, edit, add, commit. Interviewers watch for whether conflicts fluster you.
git stash saves your uncommitted changes onto a stack and reverts your working directory to a clean state, so you can switch branches or pull without committing half-done work. git stash pop reapplies the most recent stash and removes it from the stack.
Common use: you're mid-change and need to jump to another branch for a quick fix. Stash, switch, fix, switch back, pop. git stash list shows saved stashes.
git stash # save changes, clean the working dir
git switch main # do something else
git switch feature
git stash pop # bring the changes back
git stash list # see all stashesKey point: The 'quick context switch' use case is the answer the question needs. Knowing pop removes the stash while apply keeps it is a nice detail.
git cherry-pick applies the changes from one specific commit onto your current branch, creating a new commit with the same changes but a new hash. It's how you grab a single fix from one branch without merging the whole branch.
Common use: a hotfix landed on main and you need just that commit on a release branch. Overuse creates duplicate commits and confusing history, so reach for merge or rebase when you want a whole branch.
git switch release-1.2
git cherry-pick abc123 # apply just commit abc123 hereKey point: The back-port-a-hotfix use case is the one the practical case is. the duplicate-commit downside matters.
git commit --amend replaces the most recent commit. With staged changes, it folds them into that commit; with -m, it rewrites the message. Either way it creates a new commit with a new hash, so only amend commits you haven't pushed.
Common uses: you forgot to add a file, or the message had a typo. If the commit is already pushed and shared, amending forces a rewrite others must deal with, so avoid it there.
git add forgotten-file.js
git commit --amend --no-edit # add the file to the last commit
git commit --amend -m "Better message"Key point: The 'only amend unpushed commits' caveat is the point. Amending gives a new hash, so doing it after a push forces a rewrite on everyone.
A fast-forward merge happens when the target branch hasn't diverged: the branch you're merging is directly ahead of it in a straight line, so Git just slides the pointer forward to the newer commit with no merge commit created. The history stays perfectly linear, as if the work had happened in sequence.
If the branches have both moved on (diverged), a fast-forward isn't possible and Git creates a merge commit instead. You can force a merge commit even when fast-forward is possible with --no-ff, which some teams prefer to keep feature branches visible in history.
git merge feature # fast-forwards if possible
git merge --no-ff feature # always create a merge commitKey point: Explaining when a fast-forward is possible (no divergence) versus when Git must make a merge commit is the depth here. --no-ff is the team-policy angle.
A tracking branch is a local branch linked to a remote branch, so Git knows where plain git push and git pull should go and can report how far ahead or behind you are. When you clone, your local main tracks origin/main automatically.
For a new local branch, set the upstream on first push with git push -u origin branch-name. After that, git push and git pull work with no arguments.
git push -u origin feature-x # set upstream, then plain push/pull work
git branch -vv # show tracking relationshipsKey point: Knowing git push -u sets the upstream so later plain push and pull just work is the practical takeaway interviewers check for.
Use git revert, which creates a new commit that reverses the changes of the target commit, then push that new commit. Because revert adds to history instead of rewriting it, everyone who already pulled the original commit stays in sync with no forced re-clone or reset on their side.
Avoid reset and force-push on a shared branch: rewriting pushed history breaks everyone else's clone and is the classic way to cause a team-wide mess. Revert is the safe answer.
git revert abc123 # inverse commit
git push origin main # safe, no history rewriteKey point: This is a trap for reset. the question needs to hear revert plus the reason: reset rewrites shared history.
git rebase -i lets you rewrite a series of commits: reorder them, edit messages, squash several into one, split a commit, or drop it. You get a to-do list of commits and mark each with an action (pick, squash, reword, drop, edit).
It's for cleaning up local, unpushed work before opening a pull request: squash noisy 'wip' commits into a coherent story. Never run it on commits others have pulled, because it rewrites hashes.
git rebase -i HEAD~3 # rewrite the last three commits
# in the editor: pick / squash / reword / drop each commitKey point: The 'clean up local work before a pull request' framing plus the 'never on shared commits' caution together read as someone who's used it for real.
Squashing combines several commits into one. You typically do it with an interactive rebase (mark commits as squash) or with a squash merge when landing a pull request, so a messy series of work-in-progress commits becomes a single clean commit on the main branch.
The benefit is a readable history: one commit per logical change instead of a dozen tiny steps. The cost is losing the granular detail, so squash local noise, not commits that carry meaningful separate changes.
Key point: Naming the trade-off, readable history versus lost granular detail, is what turns a definition into judgment. the key signal is that balance.
A detached HEAD means HEAD points directly at a commit instead of a branch, which happens when you check out a specific commit hash or tag. You can look around and even commit, but those commits belong to no branch and can be lost.
To keep work done there, create a branch: git switch -c new-branch. To just leave without saving, switch back to a real branch. If you already made commits and left, git reflog helps you find them.
git checkout abc123 # now in detached HEAD
git switch -c fix-branch # save your position as a branchKey point: The key insight is that commits made in a detached HEAD belong to no branch and can be lost. Reaching for git switch -c to save them is the right move.
A tag marks a specific commit, usually a release like v1.0.0. A lightweight tag is just a name pointing at a commit. An annotated tag is a full object with a message, tagger, and date, and can be signed, which is why releases use annotated tags.
Tags don't move as you commit, unlike branches. Push them explicitly with git push --tags, since a normal push doesn't send tags.
git tag v1.0.0 # lightweight
git tag -a v1.0.0 -m "Release 1.0.0" # annotated
git push origin v1.0.0Key point: The gotcha worth naming: tags don't push with a normal git push. Forgetting git push --tags is a common release-day surprise.
main is your local branch, the one you commit to. origin/main is a remote-tracking branch: your local snapshot of where main was on the remote the last time you fetched. It only updates when you fetch or pull.
That's why after a fetch you can compare with git log main..origin/main to see incoming commits before merging. origin/main is read-only from your side; you update the real remote by pushing.
Key point: Understanding that origin/main only updates on fetch explains a lot of 'my branch looks out of date' confusion. That's the insight here.
Locally, git branch -d name deletes a branch that's already been merged, while the capital -D forces deletion of one that hasn't. On the remote, git push origin --delete name removes the branch there. Deleting locally and deleting on the remote are separate actions, so cleaning up after a merged pull request usually needs both.
Deleting a local branch doesn't touch the remote, and vice versa, so cleaning up after a merged pull request usually means doing both.
git branch -d feature-x # delete merged local branch
git branch -D feature-x # force-delete unmerged
git push origin --delete feature-x # delete on the remoteKey point: Knowing -d only deletes merged branches while -D forces it is a safety detail. So is remembering local and remote deletion are separate steps.
git blame shows, line by line, which commit and author last changed each line of a file. It's how you trace when and why a specific line was introduced, then jump to that commit for the full context.
It's a diagnostic tool, not an accusation despite the name. Pair it with git log to read the full story behind a puzzling line.
git blame src/auth.js # who last touched each line
git blame -L 40,60 src/auth.js # limit to a line rangeKey point: Framing blame as a diagnostic 'when and why was this line added' tool, not fault-finding, reads well. Pairing it with git log is the natural next step.
Plain git pull fetches remote commits and merges them into your branch, which can create a merge commit when both sides have new work. git pull --rebase fetches and then replays your local commits on top of the remote ones, keeping history linear with no extra merge commit.
Many teams prefer --rebase for pulling to avoid a clutter of tiny merge commits. The trade-off is the usual rebase caution, though here it only rewrites your own unpushed commits, so it's safe.
git pull --rebase origin main # replay local commits on top, linear historyKey point: Noting that --rebase here only rewrites your own unpushed commits, so it's safe, shows you understand exactly why the usual rebase caution doesn't bite.
Git config has three levels. System config applies to every user on the machine (--system). Global config applies to your user across all repos (--global, stored in ~/.gitconfig). Local config applies to one repository (--local, the default, stored in .git/config).
More specific levels override broader ones, so a per-repo setting beats your global one. Setting your name and email globally is the usual first step after installing Git.
git config --global user.name "Asha Rao"
git config --global user.email "asha@example.com"
git config --local user.email "asha@work.com" # override in this repoKey point: The 'more specific overrides broader' rule is the point. The per-repo email override is a real use case, separate work and personal identities.
Use git reflog, which records every position HEAD has been at, including commits you 'lost' with a bad reset, rebase, or branch deletion. Find the commit's hash in the reflog, then bring it back with git reset --hard <hash>, git cherry-pick <hash>, or by creating a branch at it.
This works because Git doesn't immediately delete unreachable commits; garbage collection removes them only after a grace period, so you usually have days to recover.
git reflog # find the lost commit's hash
git switch -c recovered abc123 # recreate a branch at itRecovering a lost commit with reflog
This works before garbage collection removes unreachable commits, which is usually a grace period of about 30 days.
Key point: git reflog in practice matters. It's the first tool a confident engineer reaches for after a bad reset.
advanced rounds probe internals, workflow design, and production scars. Expect every answer here to draw a follow-up.
Git is a content-addressable store built on four object types: blobs (file contents), trees (directory listings pointing to blobs and other trees), commits (a tree snapshot plus parent, author, and message), and tags (annotated references). Every object is keyed by the SHA hash of its content.
Because objects are addressed by content hash, identical content is stored once, and a commit is a cheap pointer to a tree of pointers. Branches and tags are just refs: files containing a commit hash. That model is why branching is instant and history is tamper-evident.
| Object | Represents | Points to |
|---|---|---|
| Blob | File contents | Nothing (leaf) |
| Tree | A directory | Blobs and subtrees |
| Commit | A snapshot in time | One tree, parent commit(s) |
| Tag | An annotated marker | A commit (usually) |
Key point: Naming the four object types and that everything is content-addressed by SHA is the production signal. Many candidates use Git for years without knowing this.
Watch a deeper explanation
Video: Git Tutorial for Beginners: Learn Git in 1 Hour (Programming with Mosh, YouTube)
Each commit's hash is computed from its content, including its parent's hash, so the hash chains all the way back. Changing any historical commit changes every hash after it, which makes history tamper-evident and gives every object a unique, content-based ID.
Git historically used SHA-1. Because SHA-1 collisions became practical to construct, the Git project added SHA-256 repositories and collision detection for SHA-1. Most repos still run SHA-1 with the hardened detection, and interoperability tooling for the transition is ongoing.
Key point: The tamper-evident chaining (each hash includes the parent's) is the senior point. Awareness of the SHA-256 transition, without overclaiming, is a bonus.
Rebase-based workflows give a clean, linear history that's easy to read and bisect, at the cost of rewriting commits, so they require discipline (never rebase shared branches) and can hide the real timeline of parallel work. Merge-based workflows record exactly what happened, which is honest and safer for less experienced teams, but produce a busier graph.
A common middle path: rebase or squash locally to tidy a feature branch, then merge it into main with a merge commit (or a squash merge) so main stays readable while individual work stays honest. The right call depends on team seniority and how much you value a linear history for tooling like bisect.
Key point: There's no single right answer; The technical decision depends on whether you weigh team seniority, tooling, and honesty of history. The middle-path answer usually lands best.
git bisect finds the commit that introduced a bug with a binary search over history. You mark a known-bad commit and a known-good one, and Git checks out the midpoint; you test and mark it good or bad, and it halves the range each time until it pins the exact culprit.
For a thousand commits it takes about ten steps instead of a linear scan. You can automate it fully by giving bisect a test script, so it runs the whole search hands-off.
git bisect start
git bisect bad # current commit is broken
git bisect good v1.0 # this old tag worked
# test each checkout, mark good/bad, until Git names the culprit
git bisect run ./test.sh # or automate it with a scriptCommits to check: linear scan vs git bisect
Worst-case number of commits you must test to find a regression in a history of N commits. Bisect halves the range each step, so it scales as log2(N).
Key point: Bisect is a favorite because it rewards understanding, not memorization. Mentioning git bisect run for full automation is the depth marker.
Git hooks are scripts Git runs automatically at points in its workflow: pre-commit (run linters or tests before a commit), commit-msg (validate the message format), pre-push (block a push that fails checks), and server-side hooks like pre-receive for enforcing policy centrally.
Client-side hooks live in .git/hooks and don't travel with a clone, so teams manage them with tools like pre-commit or Husky that install shared hooks. Server-side hooks are the real enforcement point, since client hooks can be skipped.
Key point: The gotcha interviewers probe: client-side hooks aren't enforced because they don't clone and can be bypassed. Enforcement has to be server-side or in CI.
The reflog records every update to HEAD and to branch tips: every commit, checkout, reset, rebase, and merge, with the previous position. It's local and separate from the commit history, which is why it can rescue commits that a reset or rebase made unreachable from any branch.
Unreachable commits survive until garbage collection prunes them, typically after a grace period of around 30 days, so the reflog gives you a real window to recover. It's the single most useful recovery tool in Git.
git reflog # HEAD's full movement history
git reset --hard HEAD@{2} # jump back to a prior HEAD positionKey point: Knowing the reflog is local and separate from history, and that unreachable commits survive until gc prunes them, is the depth that makes this The production-ready answer.
Force-pushing (git push --force) overwrites the remote branch with your local history, which is fine on a personal feature branch you alone use, for example after cleaning up commits with an interactive rebase. It's dangerous on shared branches because it can erase commits teammates pushed.
Use git push --force-with-lease instead: it refuses to overwrite if the remote has commits you haven't seen, protecting against clobbering someone else's work. And protect main and release branches so force-pushes are blocked entirely.
git push --force-with-lease # safer: aborts if the remote moved unexpectedlyKey point: Saying --force-with-lease instead of --force, and 'protect shared branches', is the answer that indicates production-scarred.
A submodule embeds one repository inside another as a pinned reference: the parent records a specific commit of the child, and the child stays its own repo you clone and update separately. A subtree merges another repo's contents directly into a subdirectory of yours, so there's nothing extra to clone.
Submodules keep clear boundaries and exact version pins but add workflow friction (extra init and update steps, easy to forget). Subtrees are simpler for consumers but make pulling upstream changes and pushing back more manual. Both solve vendoring; pick by who else consumes the shared code.
| Submodule | Subtree | |
|---|---|---|
| Storage | Reference to another repo | Contents merged in |
| Extra clone steps | Yes (init and update) | No |
| Version pinning | Explicit commit pin | Merged into history |
| Consumer friction | Higher | Lower |
Key point: the question needs the trade-off, not a preference. 'Submodules pin exact versions but add friction; subtrees are simpler for consumers' is the framing that scores.
Git stores and diffs large binaries poorly, so committing big media files or datasets bloats the repository permanently, because the history keeps every version of every file forever. Every person who clones the repo then downloads all of that weight, which is why unmanaged binaries make a repo slow and heavy over time.
Git LFS (Large File Storage) replaces large files in Git with small text pointers and stores the actual content on a separate LFS server, downloading blobs on demand. It keeps the repo lightweight while still versioning large assets. For files already committed, cleaning them out means rewriting history.
Key point: Explaining why binaries bloat history (every version is kept forever) before naming LFS shows you understand the problem, not just the tool's name.
You have to rewrite history, because the secret exists in past commits, not just the latest one. git filter-repo (the modern tool, replacing the slower filter-branch) rewrites every affected commit to strip the file or string, then you force-push and everyone re-clones or resets.
Two things must follow: rotate the secret, because it's already exposed in any clone or fork made before the rewrite, and invalidate caches on the host. Rewriting history alone doesn't un-leak a credential.
git filter-repo --path secrets.env --invert-paths # strip the file from all history
# then force-push, and rotate the secret regardlessKey point: The senior half of this answer is 'rotate the secret anyway'. Rewriting history doesn't undo the exposure to anyone who already cloned.
For a typical merge Git does a three-way merge: it finds the common ancestor (the merge base) of the two branch tips, then compares each branch against that base. Changes that don't overlap combine automatically; changes to the same lines from both sides produce a conflict Git can't decide.
The merge base is why Git is good at merging: it looks at what each side changed relative to a shared starting point, not just the two end states. Understanding this explains why conflicts happen exactly where both branches edited the same region.
Key point: The words 'merge base' and 'three-way' are what the key signal is. Explaining that conflicts arise only where both sides touched the same lines seals it.
GitFlow uses long-lived develop and release branches plus feature, release, and hotfix branches, which suits scheduled releases but adds overhead. GitHub Flow is lighter: short feature branches off main, opened as pull requests, merged and deployed continuously. Trunk-based development keeps everyone committing to main (or very short-lived branches) behind feature flags, optimizing for continuous integration.
The trend for teams doing continuous delivery is toward trunk-based or GitHub Flow, because long-lived branches accumulate painful merges. GitFlow still fits products with versioned, scheduled releases. The right choice follows your release cadence, not fashion.
| Strategy | Branch lifetime | Best fit |
|---|---|---|
| GitFlow | Long-lived develop and release | Scheduled, versioned releases |
| GitHub Flow | Short feature branches | Continuous deployment web apps |
| Trunk-based | Very short or none | High-frequency CI with feature flags |
Key point: Tying the choice to release cadence, not naming a favorite, is the strong move. The 'long-lived branches accumulate painful merges' point is worth stating.
git gc (garbage collection) cleans and compresses the repository: it packs loose objects into compressed packfiles, deltifies similar objects so only differences are stored, and prunes unreachable objects past their grace period. Git runs it automatically in the background at times, and you can trigger it manually.
Packing is why a repo with long history stays reasonable: Git stores deltas between similar objects rather than full copies of each version. Pruning is also why the reflog matters, because it's what keeps 'lost' commits reachable until gc removes them.
Key point: Connecting gc's pruning to why the reflog eventually stops rescuing commits shows you understand how the pieces fit, which is exactly the production signal here.
Cherry-pick when you need one or a few specific commits, not a whole branch: back-porting a single bug fix to a release branch, or pulling one useful commit out of an abandoned branch. Merge and rebase move a branch's whole set of commits.
The cost of overusing cherry-pick is duplicate commits (the same change appears with different hashes on two branches), which can confuse later merges. So it's a targeted tool, not a general integration strategy.
Key point: The duplicate-commit downside is what separates a thoughtful answer from 'cherry-pick copies a commit'. Say when you'd reach for merge or rebase instead.
Compare them against their merge base. git log --oneline main..feature shows commits on feature but not main, and swapping the range shows the other direction; if both have unique commits, they've diverged. git status also reports 'ahead by X, behind by Y' for a branch and its upstream.
To sync a diverged feature branch you either merge main into it (a merge commit, honest history) or rebase it onto main (linear, rewrites your commits). Choose merge if the branch is shared, rebase if it's yours alone and you want a clean line.
git log --oneline main..feature # commits on feature not in main
git log --oneline feature..main # commits on main not in feature
git merge-base main feature # their common ancestorKey point: Understanding the A..B range syntax and the merge base is the mechanical depth here. Then choosing merge or rebase by whether the branch is shared shows judgment.
git worktree lets one repository have multiple working directories checked out at once, each on a different branch, sharing the same object store. Instead of stashing and switching, you get a second folder with another branch checked out.
It's useful for running a long build on one branch while coding on another, reviewing a pull request without disturbing your current work, or comparing two branches side by side. It's lighter than a second clone because history isn't duplicated.
git worktree add ../hotfix-dir hotfix # check out hotfix in a separate folder
git worktree list # see all worktreesKey point: Worktrees are a lesser-known feature, so knowing they share one object store (lighter than a second clone) is a strong signal of real Git depth.
Git won because it's distributed and fast where older systems were centralized and slow. In a centralized system like Subversion (SVN), the history lives on one server, so you need a network connection to commit, view history, or branch, and a server outage stops everyone. Git gives each clone the full history, so commits, diffs, log, and branch work offline, and branching is cheap enough to make one per feature. The trade-offs are real: Git's model has a steeper learning curve, and it handles large binary files poorly without extensions like Git LFS, where a centralized system with file locking sometimes fits better.
| System | Model | Branching | Watch out for |
|---|---|---|---|
| Git | Distributed | Cheap, local, fast | Steeper learning curve, large binaries |
| Subversion (SVN) | Centralized | Server-side, heavier | Needs the server online to commit |
| Mercurial | Distributed | Cheap, local | Smaller ecosystem than Git today |
| Perforce | Centralized | Server-side | Cost, but strong on huge binary assets |
Prepare in layers, and practice in a real repository. Most Git rounds mix quick concept checks with 'what command would you run to...' scenarios and at least one recovery question, so rehearse doing, not just reciting.
The typical Git interview flow
Earlier rounds increasingly run as AI coding interviews. The concepts on this page are what gets probed at every stage.
10 questions, about 6 minutes. Score 70% or higher to earn a shareable certificate.
Hyring builds the AI Coding Interviewer that runs live technical rounds in 20+ languages for 5,000+ hiring teams. These Git questions and scoring notes reflect what actually gets asked and evaluated inside the interviews we host.
See how the AI Coding Interviewer works