Prepare for Git interview questions grouped by experience level.
Git Interview Question & Answers
0-2 Years
Git is a distributed version control system that tracks changes to files over time, letting multiple people collaborate on the same codebase without overwriting each other's work. Every developer has a full copy of the project's history locally, which makes most operations fast and lets work continue even without network access.
Git is the version control software itself, run locally on a developer's machine to track changes. GitHub is a hosting service built around Git that adds a web interface, collaboration features like pull requests, and remote storage for repositories, but it is only one of several platforms that can host Git repositories.
A repository, often called a repo, is a directory that Git tracks, containing the project's files along with a hidden .git folder that stores the complete history of changes, branches, and configuration. A repository can exist locally on a machine or remotely on a hosting service.
A local repository lives on a developer's own machine and is where they make and commit changes. A remote repository lives on a server, often a hosting service like GitHub, and acts as a shared point developers push their changes to and pull others' changes from.
git init creates a new Git repository by adding a .git folder to the current directory, which sets up the internal data structures Git needs to start tracking changes. It is the command used to start version controlling a brand new project.
git clone copies an existing remote repository, including its full history and branches, down to the local machine as a new local repository. It also automatically sets up a connection back to the remote so the developer can push and pull changes.
git add stages changes, moving them from the working directory into the staging area to prepare them for the next commit. git commit takes everything currently staged and permanently records it as a new snapshot in the repository's history along with a message describing the change.
The staging area, also called the index, is an intermediate space where changes are collected before they become part of a commit. It lets a developer choose exactly which changes to include in a commit rather than committing every modified file at once.
git status shows the current state of the working directory and staging area, listing which files have been modified, which are staged and ready to commit, and which are untracked. It is the command developers run most often to understand what Git currently sees.
git log displays the commit history of the current branch, showing each commit's unique hash, author, date, and commit message in reverse chronological order by default. It can be filtered and formatted in many ways to focus on specific authors, dates, or files.
A commit is a saved snapshot of the project at a specific point in time, identified by a unique hash, along with metadata like the author, timestamp, and a message describing what changed. Commits form the chain that makes up a branch's history.
A branch is a lightweight, movable pointer to a specific commit, allowing developers to diverge from the main line of development and work on a feature or fix in isolation without affecting other branches until the work is merged back.
git branch lists the branches that exist in the current repository, and when given a name it creates a new branch pointing at the current commit. It does not switch to the new branch by itself, unlike git checkout with the -b flag.
git checkout switches the working directory to point at a different branch or commit, updating the files to match that branch's state. It has historically also been used to create new branches with the -b flag and to discard changes in specific files.
git checkout is an older, multi purpose command that handles both switching branches and restoring files, which can be confusing since it does several unrelated things. git switch was introduced as a more focused command that only handles switching branches, making its intent clearer.
git merge combines the history of another branch into the current branch, creating a new commit that has two parents if the branches have diverged, or simply moving the pointer forward if the merge can fast forward without conflicts.
A merge conflict happens when Git can't automatically combine changes because two branches modified the same lines of a file in different ways. Git marks the conflicting sections in the file and requires a developer to manually resolve them before completing the merge.
git pull fetches the latest changes from a remote repository and then merges them into the current local branch in one step. It is essentially a combination of git fetch followed by git merge against the tracked remote branch.
git push uploads local commits to a remote repository, updating the remote branch to match the local one. It is how changes made locally become visible and available to other collaborators working from the same remote.
git fetch downloads new commits and branch updates from a remote repository without merging them into the local branch, leaving the developer's current work untouched until they choose to merge or rebase. git pull does that fetch step and then immediately merges automatically.
A remote is a reference to another copy of the repository, usually hosted on a server, that a local repository can push to or pull from. The default remote created by cloning is conventionally named origin.
The .gitignore file tells Git which files or folders to exclude from version control, such as build artifacts, dependency folders, or local configuration files that shouldn't be shared. Files matching patterns in .gitignore are not tracked or shown as untracked in git status.
A commit hash is a unique 40 character SHA-1 identifier Git generates for every commit based on its content, metadata, and parent commit. It uniquely identifies that exact commit and is used to reference it in commands like git checkout or git diff.
git diff shows the differences between two states of the repository, such as between the working directory and the staging area, or between two commits. It displays exactly which lines were added, removed, or changed.
HEAD is a pointer that refers to the currently checked out commit or branch, representing the point in history the working directory currently reflects. When a developer switches branches, HEAD moves to point at that branch instead.
A detached HEAD state happens when HEAD points directly at a specific commit rather than at a branch, typically after checking out a commit by its hash. Any new commits made in this state aren't attached to a branch and can be lost unless a new branch is created from them.
Deleting a file directly from the file system removes it from the working directory but Git still tracks the old version and shows the file as deleted in git status. git rm both deletes the file and stages that deletion, so the removal is ready to be committed in one step.
A tag is a fixed pointer to a specific commit, typically used to mark release points like v1.0. Unlike a branch, a tag does not move as new commits are added, making it a stable reference to a particular point in history.
A lightweight tag is simply a name pointing directly at a commit with no extra metadata. An annotated tag is a full object in Git's database that stores the tagger's name, date, and a message, and it can also be cryptographically signed.
The working directory is the actual set of files on disk that a developer edits directly, reflecting the current state of whichever branch or commit is checked out. It sits alongside the staging area and the repository's committed history as one of Git's three main areas.
A commit message describes what changed and why, giving future readers, including the original author, context for understanding a change without having to read the full diff. A good commit message typically has a short summary line followed by more detail if needed.
The --depth option limits how much history git clone downloads, creating a shallow clone that only includes the most recent specified number of commits. It is useful for speeding up clones of large repositories when full history isn't needed.
A fork is a personal copy of someone else's repository created on a hosting platform like GitHub, allowing a developer to make changes independently without needing direct write access to the original repository. Changes can later be proposed back through a pull request.
A pull request is a feature of Git hosting platforms, not Git itself, that proposes merging changes from one branch or fork into another, giving reviewers a place to comment on the diff and discuss the change before it's merged.
git show displays detailed information about a specific Git object, most commonly showing the full diff and metadata of a particular commit when given its hash. It can also be used to inspect tags or other objects.
Origin is simply the conventional name given to the remote a repository was cloned from. Upstream is a commonly used name for an additional remote pointing at the original repository when working from a fork, distinguishing it from the developer's own fork, which stays as origin.
3-6 Years
I would open the conflicting files, look at the conflict markers Git inserts to see both versions, decide which changes to keep or how to combine them, then stage the resolved files and complete the merge with a commit. I would also communicate with whoever made the conflicting changes if the intent behind either side isn't clear.
I would use merge when I want to preserve the exact history of how the branch developed and when the branch is shared with others, and use rebase when I want a cleaner, linear history on a branch that's still private to me, since rebasing rewrites commit hashes and can cause problems for collaborators.
I would use git revert to create a new commit that undoes the changes, rather than rewriting history with something like reset or amend, since the commit is already shared and rewriting published history can break things for anyone else who has already pulled it.
I would check git reflog, which records where HEAD and branches have pointed recently even after a reset, find the commit hash from before the reset, and either create a new branch at that commit or reset back to it.
I would use an interactive rebase, git rebase -i, against the base branch to squash small fixup commits into their logical parent commits, reword unclear commit messages, and reorder commits if it improves the story of the change, being careful to only do this on a branch that hasn't been shared yet.
I would remove the file from version control going forward with git rm --cached, rotate the exposed credentials immediately since they should be considered compromised, and use a history rewriting tool to actually purge the file from past commits if the repository's history needs to stay clean.
I would establish a convention like feature branches created from a shared main branch, require pull requests with at least one review before merging, and keep main always deployable so anyone can branch off it at any time without inheriting half finished work.
I would ask the author whether the change could be split into smaller, logically separate pull requests, and if not, I would review it commit by commit rather than as one giant diff, focusing first on the overall approach before diving into line level detail.
I would mark a known good commit and the current known bad commit with git bisect start, good, and bad, then Git checks out commits in between for me to test, and I mark each as good or bad until it narrows down to the exact commit that introduced the issue.
I would use git stash to set aside the uncommitted changes without committing them, switch to the other branch to handle the urgent task, and later come back and run git stash pop to restore the work exactly where I left off.
I would identify logically independent pieces of the feature that can stand on their own, cherry pick or rebase those specific commits onto separate branches off main, and sequence the pull requests so each one builds cleanly on what's already been merged.
I would have each open their own pull request against main so conflicts, if any, surface during review rather than being resolved informally, and if their changes do conflict, I would have them coordinate directly on how to reconcile the overlapping parts.
I would add the relevant patterns to a shared .gitignore committed to the repository so it applies to everyone by default, and for genuinely sensitive files I would also add a pre-commit hook that blocks the commit if it detects a matching filename or pattern.
I would periodically rebase the feature branch onto the latest main, or merge main into the feature branch if it's shared with others, and resolve conflicts incrementally rather than letting the branches drift so far apart that a final integration becomes overwhelming.
I would use git cherry-pick to apply one specific commit from one branch onto another without bringing in the rest of that branch's history, which is useful for backporting a bug fix to a release branch without merging in unrelated in progress work.
I would run git fetch to see what's changed on the remote, then either merge or rebase those changes into my local branch before pushing again, rather than force pushing, unless I'm certain the remote history genuinely needs to be overwritten.
I would configure the hosting platform's branch protection settings to require pull requests for changes to main, require passing status checks before merge, and disable force pushes and direct pushes to that branch entirely so history can't be silently rewritten.
I would follow a consistent format with a short imperative summary line under about fifty characters, a blank line, and then more detailed explanation if needed, focusing on why the change was made rather than restating what the diff already shows.
I would move large binary assets out of the regular Git history and into a tool like Git LFS, which stores large files outside the main repository and only keeps lightweight pointers in Git itself, keeping clone times and repository size manageable.
I would document the branching strategy, commit message conventions, and pull request process in a contributing guide, pair with them on their first few commits to reinforce the conventions in practice, and point out any repository specific hooks or checks they should be aware of.
I would resolve the conflict in the currently paused commit, stage the resolved files, and run git rebase --continue to move to the next commit in the sequence, repeating as needed, or run git rebase --abort if I decide the rebase isn't worth continuing.
I would run git diff against the two branch tips, or more usefully git log with a range like main..feature to see the actual commits that are ahead, giving a clearer picture of what would be merged than a raw file diff alone.
I would check whether they ran git submodule update after pulling changes to the parent repository, since updating the parent repo alone doesn't automatically update submodule contents, and confirm the submodule is pointed at the correct commit reference the parent repository expects.
I would add a script to the .git/hooks/pre-commit file, or more commonly use a shared hook manager tool so the hook is version controlled and consistent across the team, that runs the linter and blocks the commit if it reports errors.
6-8 Years
I would define clear ownership boundaries within the monorepo so teams can move independently, adopt short lived feature branches merged frequently through trunk based development to avoid long lived divergence, and invest in strong CI gating so a broken change from one team doesn't block everyone else.
I would look at whether large binary files or generated artifacts were committed directly instead of through something like Git LFS, consider a history rewrite to strip out the worst offenders if the team agrees it's worth the disruption, and evaluate partial clone or shallow clone strategies for developers who don't need full history.
I would evaluate available conversion tools appropriate to the source system, run the migration in a test environment first to validate that history, authorship, and branches translate correctly, and plan a cutover window where the legacy system becomes read only to avoid divergence between the two.
I would maintain dedicated release branches per supported version, cherry pick critical fixes from main back into the relevant release branches rather than developing directly on them, and automate the backport process where possible to reduce the manual overhead of keeping multiple branches patched.
I would evaluate which repositories genuinely benefit from consolidation due to shared dependencies or coordinated releases, plan the migration to preserve history where practical, and pair the technical merge with updated CI and ownership tooling so the monorepo doesn't become an unmanageable free for all.
I would implement server side hooks or CI checks that validate commit message format and branch naming patterns before a merge is allowed, provide clear, actionable error messages when a check fails, and phase in enforcement gradually so existing teams have time to adapt their tooling.
I would create a hotfix branch directly from the exact commit currently in production rather than from the diverged release branch, apply the minimal fix there, deploy it, and separately plan how to reconcile that fix back into the ongoing release branch work.
I would use code owners configuration to require review from the security team on changes touching sensitive directories, combine that with branch protection rules that enforce those reviews, and periodically audit that the ownership mapping still reflects current team boundaries as the organization changes.
I would implement change detection so CI only builds and tests the packages or services actually affected by a given commit, cache dependencies and build artifacts aggressively, and periodically review the dependency graph to catch overly broad coupling that forces unnecessary rebuilds.
I would use a purpose built history rewriting tool designed for large repositories, coordinate a clear communication and cutover plan so every developer re-clones or resets their local copy afterward, and rotate any credentials that were exposed since the old commits may already be cached elsewhere.
I would look at metrics like how often feature flags are needed to hide incomplete work on main, how frequently CI on main breaks, and how long branches actually live in practice versus the stated policy, and use that evidence to decide whether the process needs tightening or more tooling support.
I would use a GitOps style approach where merging to an environment specific branch or updating a manifest in that branch triggers deployment, keep the actual deployed state reconcilable against that branch at any time, and restrict direct pushes so every change to production state has a traceable commit behind it.
8-10 Years
I would avoid mandating one rigid model for every team and instead set a small set of non negotiable principles, like keeping main deployable and requiring review before merge, while letting individual teams adapt branch naming and release cadence details to their own product's needs.
I would base the decision on actual coordination cost between the repositories, like how often changes need to land together across them, rather than repository count alone, and pilot consolidation with a smaller, willing group before mandating it organization wide.
I would set a reasonable service level expectation, like same business day for standard changes, track review turnaround as a metric visible to teams and their leads, and treat teams with consistently slow reviews as a coaching conversation rather than a punitive one.
I would weigh the value of a clean, readable main branch history that squash merging gives against the loss of granular commit level context, and generally favor squash merging for most teams while allowing exceptions for teams whose workflow genuinely depends on detailed history, like ones doing frequent bisects.
I would mandate automated secret scanning on every repository as a baseline requirement, define a clear, fast incident response process for when a secret is detected including mandatory rotation, and track repeat offenses by team as a signal that more upstream training or tooling is needed.
I would default to more open internal visibility to encourage cross team learning and reuse, while requiring an explicit, documented exception process for repositories that handle sensitive data or regulated code that must stay restricted to a specific team.
I would build custom tooling only where the organization's scale or workflow genuinely exceeds what off the shelf platform features can handle, since custom tooling carries a long term maintenance cost, and prefer configuring existing platform capabilities wherever they're sufficient.
I would set a soft guideline, like a week or two depending on the team's cadence, track branches that exceed it, and use that as a prompt for a conversation about whether the work should be broken down further or hidden behind a feature flag rather than a hard technical block.
I would restrict those capabilities by default at the organization level, requiring an explicit elevated role for anyone who genuinely needs them, and audit usage periodically since force pushes and branch deletions are rare enough in legitimate workflows that unusual patterns are worth reviewing.
I would define a small set of required checks every repository must run before merge, like tests and linting, provide a shared template teams can adopt rather than mandating a single rigid pipeline, and migrate teams incrementally rather than forcing an immediate switch.
I would set criteria for what counts as inactive, like no commits or deployments within a defined period, require owner confirmation before archiving to avoid surprising a team still quietly relying on it, and archive rather than delete so history remains recoverable if needed later.
I would communicate the change and rationale well ahead of enforcement, provide migration support and documentation, and set a realistic adoption window that accounts for teams with different release schedules rather than a single hard cutover date for everyone.
I would recognize that linear history mainly benefits tools like bisect and simplifies reading logs, while merge commits preserve more context about how work actually happened, and let this be a team level choice within the organization rather than mandating one approach everywhere.
I would inventory which repositories are actually active, identify natural groupings that would benefit from consolidation without forcing unrelated projects together, and prioritize the consolidation effort by where the coordination pain is actually being felt rather than pursuing it as a blanket cleanup.
10+ Years
I would sit down with them over a specific real pull request and walk through how to break it into logically separate commits and a clear description, rather than giving abstract advice, and follow up on their next few pull requests with direct, specific feedback until the pattern improves.
I would create practical, scenario based training material covering the situations people actually get stuck on, like resolving conflicts or recovering lost work, rather than an exhaustive command reference, and pair it with office hours or a channel where people can get quick help without feeling judged.
I would translate the technical pain into concrete numbers, like clone times costing developer hours weekly or CI slowdowns delaying releases, and present the cleanup as a productivity investment with a measurable payback rather than a purely technical nice to have.
I would anticipate that practices that work fine at a small scale, like informal branch naming or manual review assignment, start breaking down at scale, and proactively build out the tooling and conventions, like enforced code ownership and automated merge queues, before they become urgent pain points.
I would ground the discussion in the actual needs of the products involved, like release cadence and how many versions need concurrent support, rather than treating it as an abstract best practices debate, and be willing to allow different models for genuinely different contexts within the same organization.
I would help them frame the change around a concrete pain point the team already feels, like difficulty debugging with git bisect or unclear pull request history, so the practice change solves a problem people recognize rather than being an arbitrary rule imposed from outside.
I would push for that tooling to be documented and, where possible, simplified or replaced with well supported off the shelf alternatives, and make sure at least one other engineer is cross trained on maintaining it, treating single person dependency as an ongoing risk to manage rather than a one time fix.
I would not expect deep expertise with the organization's specific tooling on day one, and instead focus onboarding on the handful of workflows they'll use constantly, giving them a safe practice environment and a clear escalation path for when something goes wrong before they're fully comfortable.
I would have them quantify the actual time and error cost of the manual process before proposing automation, since I've seen engineers over invest in tooling for problems that occur too rarely to justify the build and maintenance cost, and coach them to weigh that against genuinely painful, frequent friction.
I would prioritize a structured handoff period where the departing engineer walks a successor through the reasoning behind key decisions, beyond just the mechanics, and make sure critical configuration and hooks are documented in a shared location rather than living only in that person's memory.
I would start by understanding why previous mandates failed, likely because they didn't account for real differences between teams, then co-design the new standard with representatives from a range of teams so it reflects actual constraints, and pilot it with willing teams before asking for broader adoption.
I would define a small, clearly justified set of organization wide requirements, like required CI checks and branch protection, while leaving day to day workflow choices like commit style or local branching habits to individual teams, since forcing uniformity beyond what's actually needed tends to generate resentment without real benefit.
I would treat it as a coordinated organizational change rather than a purely technical task, schedule it for a low activity window, communicate exact steps every engineer needs to take afterward well in advance, and have support available in real time during the cutover for anyone who runs into trouble.
I would treat this as a balance to revisit periodically rather than a decision made once, since the right amount of standardization shifts as the organization grows, and I would keep the bar for adding new mandatory constraints high enough that only genuine cross team pain points justify reducing team autonomy.




