
Git interview questions for frontend developers are rarely about memorizing commands. They are about what you do when something has already gone wrong: a bad commit is on the remote, a rebase ate two days of work, a regression appeared somewhere in the last few hundred commits, or an .env file with a live key was pushed to a public repository. This guide works through 13 such questions. Each answer gives the command, what Git actually does, and the trap that separates a confident answer from a correct one, with a link to the Git or vendor documentation it rests on.
Git behavior has changed in ways that make older answers wrong. git pull no longer quietly merges divergent branches, git restore and git switch are no longer experimental, and the npm documentation page that told you how to fix lockfile conflicts only exists for npm 6. The answers below use current documentation, checked against Git 2.56.0, the latest release listed on git-scm.com at the time of writing, and they say which version introduced a behavior where that matters.
Git is close to universal among developers. In the Stack Overflow Developer Survey 2022, which added a version control section that year, 93.87% of respondents and 96.65% of professional developers reported using Git. Because nearly everyone uses it, knowing git commit says little. What separates answers is judgment about shared history, recovery, and the parts of Git that touch a frontend codebase specifically: lockfiles, environment files that get inlined into a client bundle, and pre-commit tooling such as Husky and lint-staged. There is no published data on how often Git comes up in frontend interviews, so treat these as the questions worth being ready for, not a prediction of what you will be asked.
Both get main's changes into your branch. A merge adds a merge commit and keeps your commits as they were. A rebase replays your commits on top of the new base, which gives a linear history but creates new commits. The rule that decides it is in the Pro Git book's section on the perils of rebasing: "Do not rebase commits that exist outside your repository and that people may have based work on." The reason follows directly: "When you rebase stuff, you're abandoning existing commits and creating new ones that are similar but different."
So rebasing your own unshared feature branch onto main is fine. Rebasing a branch that a teammate has pulled and built on forces them to reconcile duplicate commits.
If you rebase a branch you already pushed, you need a force push, and the flag matters:
git fetch origingit rebase origin/maingit push --force-with-lease --force-if-includes
The git push documentation says plain --force "can cause the remote repository to lose commits." --force-with-lease refuses the push unless the remote branch is still where your remote-tracking branch says it is. It has a documented weakness: it "interacts very badly with anything that implicitly runs git fetch on the remote" in the background, such as an editor that fetches on a timer, because the fetch silently updates what you are comparing against. --force-if-includes, added in Git 2.30.0, closes that gap by checking that the remote tip has actually been integrated locally. On its own it is a no-op, so it is always paired with --force-with-lease.
git fetch downloads commits and updates remote-tracking branches such as origin/main. It never touches your working branch. The git pull documentation describes pull as running git fetch and then integrating the fetched branch into the current one.
The part many answers get wrong is the default. Older Git merged divergent branches without asking. Git 2.27.0 added a warning when no preference was configured, and since Git 2.33.1 a pull on divergent branches with no configuration stops with:
hint: You have divergent branches and need to specify how to reconcile them.fatal: Need to specify how to reconcile divergent branches.
The current documentation describes --ff-only as the behavior "when no method for reconciling divergent histories is provided." You choose a strategy with configuration:
git config --global pull.rebase false # mergegit config --global pull.rebase true # rebasegit config --global pull.ff only # fast-forward only, fail otherwise
A senior answer names a choice and the reason. pull.ff only is the conservative option: a pull never creates a merge commit or rewrites anything, and when it fails you decide explicitly. Note that the documentation calls pull.rebase "a possibly dangerous operation", which is the shared-history rule from Question 1 again.
The three commands operate at different levels:
| Command | What it changes | Safe on a pushed, shared branch? |
|---|---|---|
git revert <commit> | Adds a new commit that undoes an earlier one | Yes, history is only added to |
git reset <commit> | Moves the branch pointer, and with --hard the files | No, it rewrites history others may have |
git restore <path> | Restores files in the working tree or index | Not a history operation at all |
For a pushed commit on a shared branch, the answer is git revert. The git revert documentation describes it as recording "some new commits to reverse the effect of some earlier commits." Nobody else's history changes.
git reset "changes which commit HEAD points to", per the git reset documentation. The default is --mixed, which keeps your file changes unstaged. --hard overwrites files and "may overwrite untracked files." Use it on local commits you have not pushed.
git restore is for files, not commits. It was introduced in Git 2.23.0 alongside git switch, and Git 2.51.0's release notes declared both "no longer experimental", so answers that call them experimental are out of date.
Reverting a merge has a trap worth naming. You need -m to say which parent is the mainline, and the documentation warns that reverting a merge "declares that you will never want the tree changes brought in by the merge." If the feature branch is fixed and merged again later, its earlier changes will not come back. You have to revert the revert first.
Commits are rarely gone immediately. The reflog records where branch tips and HEAD have been in your local repository, including positions that no branch points to any more:
git reflog# a1b2c3d HEAD@{0}: reset: moving to HEAD~3# 9f8e7d6 HEAD@{1}: commit: Add cart total roundinggit branch recovered 9f8e7d6
HEAD@{2} means "where HEAD used to be two moves ago." Commands such as reset, rebase and merge can also record the previous branch tip in ORIG_HEAD, which may help with recovery. However, later operations can overwrite it. Use git reflog to identify the commit you want to recover, and create a branch pointing to it before making further changes. Avoid running git reset --hard unless you also intend to discard your current tracked working-tree changes.
Two limits make this a real answer rather than "the reflog has everything". The reflog is local: it does not exist on the remote or in a teammate's clone. And it expires. Per the git config documentation, gc.reflogExpire defaults to 90 days, but gc.reflogExpireUnreachable defaults to 30 days, and that is the one that applies to commits you abandoned with an amend, reset or rebase.
Use git bisect, which "uses a binary search algorithm to find which commit in your project's history introduced a bug." You mark one known good commit and one known bad one, and Git checks out the midpoint each time:
git bisect startgit bisect bad # current commit has the buggit bisect good v4.2.0 # this release did not# test, then mark each step: git bisect good / git bisect badgit bisect reset # back to where you started
A binary search over 200 commits needs about 8 steps, since 2 to the power of 8 is 256. The stronger answer automates it. git bisect run executes a script at each step, and the exit code decides the verdict. The documentation says to exit 0 when the code is good, and with "a code between 1 and 127 (inclusive), except 125" when it is bad. The special code 125 means the commit "cannot be tested" and is skipped, which matters in a frontend repository where some intermediate commits do not build:
git bisect run sh -c 'npm ci || exit 125; npx vitest run src/cart.test.ts'
A failed install exits 125 and skips the commit. A failing test exits non-zero and marks it bad. Writing the failing test first, as a small reproduction, is most of the work, and it leaves you with a regression test once the bisect is done.
Do not resolve it by hand, and do not delete the lockfile and regenerate it from scratch, which can silently upgrade every dependency within its semver range. Resolve the package.json conflict first, because that is the file that states intent, then let the package manager rebuild the lockfile from it.
What each tool documents differs:
package.json conflicts, then run npm install and "npm will automatically resolve any conflicts for you and write a merged package lock." Current npm documentation (v7 through v12) no longer has that section. Current npm still parses conflicted lockfiles through its parse-conflict-json dependency, whose README describes resolving JSON with Git merge conflicts "if possible". So the workflow still works, but cite it carefully.yarn install --immutable, the usual CI mode, Yarn may not edit files, not even to resolve conflicts.After regenerating, read the lockfile diff before committing. The point is to confirm that only the packages from the two branches changed. Why the lockfile matters at all, and what npm ci does with it, is covered in GreatFrontEnd's npm and package management interview questions.
Because .gitignore only applies to untracked files. The gitignore documentation says so in its first lines: "Files already tracked by Git are not affected." If dist/ or .env.local was committed before the ignore rule existed, Git keeps tracking it.
The same page gives the fix: "use git rm --cached to remove the file from the index."
git rm -r --cached distgit commit -m "Stop tracking build output"
--cached removes the path from the index only, and the git rm documentation confirms that working tree files "will be left alone." Two follow-ups show you understand the consequences. The file still exists in every earlier commit, so if it was a secret, this does not remove it (see the next question). And teammates who pull this commit will have the file deleted from their working tree, because Git sees the removal as a change, so warn them first if it is a local config file they need.
Rotate the key first. GitHub's guide to removing sensitive data from a repository says that for a secret, "as a first step you need to revoke and/or rotate that secret," and that rewriting history afterwards "may not be warranted." Once a key has been pushed, assume it has been copied. Rewriting history does not reach clones, forks, or cached views.
Then decide whether to rewrite history. If necessary, follow GitHub's sensitive-data removal procedure using git-filter-repo in a fresh clone. For a file named .env, the core commands are:
git-filter-repo --sensitive-data-removal --invert-paths --path .env# Re-add origin if git-filter-repo removed it.git remote add origin <repository-url># After reviewing the rewritten history and coordinating with collaborators:git push --force --mirror origin
Run git remote add only if origin is missing. A mirror push can overwrite remote branches and tags, so verify the rewritten references before pushing.
The same guide lists what this does not fix: the data can stay reachable "in any clones or forks", through cached views of the old commit hashes, and through pull requests that reference them. Removing cached views needs GitHub Support, which only assists when rotation cannot mitigate the risk. Collaborators then have to rebase rather than merge, or an old clone puts the file back.
The frontend-specific half of the answer is that a key can leak without ever being committed. Build tools inline some environment variables into the JavaScript you ship. Vite's environment variables guide says variables prefixed with VITE_ "will be exposed in client-side source code after Vite bundling" and should not contain API keys. Next.js does the same for the NEXT_PUBLIC_ prefix, which its environment variables guide describes as inlining a value "into the js bundle that is delivered to the client." A secret behind either prefix is public no matter what .gitignore says.
Prevention is the last step. GitHub push protection blocks pushes that contain recognized secrets. The user-level setting is on by default for pushes to public repositories, while repository-level push protection is off by default and requires GitHub Secret Protection.
The common setup is Husky to install the hook and lint-staged to run tools only on staged files:
npm install --save-dev husky lint-stagednpx husky init
npx husky init creates .husky/pre-commit and sets up the prepare script in package.json, so the hook is installed on npm install. Husky works through Git's core.hooksPath setting, which Git 2.9.0 introduced, and points Git at a hooks directory that is committed to the repository. That matters because Git's default hooks directory is not shared: the Pro Git chapter on Git hooks says "client-side hooks are not copied when you clone a repository."
{"lint-staged": {"*.{ts,tsx}": ["eslint --fix", "prettier --write"],"*.css": "prettier --write"}}
Finally, update .husky/pre-commit to run lint-staged:
npx lint-staged
Now, each commit runs ESLint and Prettier on matching staged files. Full tests and type checking can run in CI.
Then the second half of the question. A hook runs on the developer's machine and can be skipped. The git commit documentation says --no-verify will "bypass the pre-commit and commit-msg hooks," and Husky documents HUSKY=0 to disable its hooks. Pro Git's advice is direct: if the intent is to enforce a policy, "you'll probably want to do that on the server side." So the same lint, type check and tests run in CI as a required status check, and the local hook is a fast feedback loop, not the gate. Keeping the hook fast, by linting only staged files and leaving the full test suite to CI, is what stops people from reaching for --no-verify in the first place. GreatFrontEnd's senior testing interview questions cover what that CI test layer should contain.
For cleanup, interactive rebase on your own unshared branch. In the editor, squash folds a commit into the one before it and combines the messages, while fixup folds it in and discards its message. You can prepare this while working with git commit --fixup=<commit>, which creates a commit titled fixup! ..., and then:
git rebase -i --autosquash origin/main
--autosquash moves each fixup! commit next to its target and marks it for you, per the git rebase documentation. Setting rebase.autoSquash makes that the default.
The merge strategy is a team decision with real trade-offs, and GitHub's page on merge methods states them:
| Method | Resulting history | What you give up |
|---|---|---|
| Merge commit (default) | Every branch commit plus a merge commit | A linear log |
| Squash and merge | One commit per pull request | Per-commit history and authorship detail inside the pull request |
| Rebase and merge | Each commit replayed onto the base, no merge commit | Original commit hashes. GitHub creates new ones, updates committer information, and drops empty commits |
A reasoned answer ties the choice to how the history is used. Squash suits small pull requests where one commit per change makes git revert and git bisect simple. A merge commit suits large branches whose individual commits are meaningful. If the team wants to enforce linear history, GitHub's "require linear history" branch protection rule allows only squash or rebase merges, per its protected branches documentation.
git cherry-pick applies the change from one commit as a new commit on the current branch:
git switch release/4.2git cherry-pick -x 3c9d2e1
The detail that shows experience is -x. The git cherry-pick documentation says it appends "(cherry picked from commit …)" to the message, and recommends it when cherry-picking between publicly visible branches, for example "backporting a fix to a maintenance branch for an older release." Six months later, anyone can trace the release commit back to its source. The same page says not to use it when picking from a private branch, because the reference would point at a commit nobody else can see.
Two follow-ups are worth having ready. The new commit has a different hash, so the same change now exists twice, which is expected for a backport. And if the fix depends on earlier commits that are not on the release branch, the pick may conflict or, worse, apply cleanly and break, so run the release branch's tests before tagging.
Either works. They trade simplicity for isolation.
git stash saves your uncommitted changes and cleans the working tree. Untracked files are not included unless you pass -u (--include-untracked), so a new component file you have not added yet stays in the working tree and follows you onto the hotfix branch, per the git stash documentation. If git stash pop hits a conflict, the entry is not removed from the stash list, so nothing is lost.
git worktree gives you a second working directory attached to the same repository, so both branches are checked out at once:
git worktree add ../shop-hotfix -b hotfix/cart-total origin/maincd ../shop-hotfix && npm ci
Nothing in your feature directory is touched, and a long-running dev server there keeps running. The cost on a frontend project is a second node_modules install, and the git worktree documentation still notes that "multiple checkout in general is still experimental" and that submodule support is incomplete. Git also refuses to check out the same branch in two worktrees unless you force it. A practical answer: stash for a five-minute interruption, worktree when the fix needs its own install, dev server or review time.
git blame shows the last commit that touched each line, and after a repository-wide Prettier run that commit is the formatter for nearly every line. Git 2.23.0 added the fix. The git blame documentation describes --ignore-rev as ignoring a revision "as if the change never happened", so lines it changed are blamed on the previous commit that touched them.
echo "4f1a2b3c... # Prettier 3 reformat" >> .git-blame-ignore-revsgit config blame.ignoreRevsFile .git-blame-ignore-revs
The file takes full commit hashes, one per line, with # comments. GitHub's blame view reads a .git-blame-ignore-revs file in the repository root automatically, per its documentation on viewing files.
Blame answers who last touched a line, which is often not the question. Two git log options from the git log documentation go further. git log -S'formatPrice(' finds commits that changed the number of occurrences of a string, which is how you find when a function call was added or removed. git log -L :formatPrice:src/utils/price.ts traces the history of one function across every commit that changed it. Reading the commit message and the linked pull request then tells you why.
git pull merges by default. Since Git 2.33.1, a pull on divergent branches without configuration stops and asks.git reset --hard on a pushed branch when git revert was the safe tool.VITE_ or NEXT_PUBLIC_, which ships it to every browser.--no-verify and must be backed by CI.Do I need to memorize Git command flags for frontend interviews? No. Know what each command does to history and to the working tree, and when it is safe on a shared branch. Exact flags can be looked up. The difference between revert and reset cannot be guessed under pressure.
Merge or rebase: which is correct? Neither, in general. Rebase your own unshared work for a clean history. Merge, or rebase with care, when others have built on the branch. Then name the team's merge method and why.
Is git switch the same as git checkout? For changing branches, git switch covers the branch half of git checkout, and git restore covers the file half. Both have been stable since Git 2.51.0.
What should a frontend repository put in .gitignore? At least node_modules, build output such as dist or .next, local environment files, and editor or OS files. Commit the lockfile.
Practise the recovery questions in a throwaway repository rather than reading about them. Make five commits, hard reset three of them away and recover them from the reflog. Introduce a bug in commit 30 of 40 and find it with git bisect run and a one-line test. Create a conflicting package.json change on two branches and resolve the lockfile with npm install. Set up Husky and lint-staged, then commit with --no-verify and watch the hook not run. Each exercise takes minutes and turns an answer you have memorized into one you have seen happen. For the dependency side of frontend tooling, GreatFrontEnd's frontend build tools interview questions pair well with this list, and the guide to a strong frontend developer GitHub profile covers how your public repositories read to a reviewer.
Git interview questions for frontend developers test whether you can protect shared history and recover when something goes wrong. Rebase only what nobody else has built on and force-push with a lease. Configure git pull instead of relying on a default that has changed. Revert on shared branches and reset only locally. Recover lost work from the reflog before it expires, and find regressions with git bisect run. Let the package manager rebuild a conflicted lockfile, and rotate a leaked key before rewriting history. Treat hooks as feedback and CI as enforcement, and know the cost of each merge method. If you can explain the command, what it does to history, and the trap, for each of the 13 questions here, you are answering at the level these questions are looking for.
npm package manager interview questions rarely appear by name. Here is what lockfiles, npm ci, semver ranges and install scripts really do, with worked examples.
Frontend build tools interview questions on Vite, webpack, bundling, tree shaking, content hashing, source maps and chunking, with real configs and answers.
Senior testing developer interview questions on flaky tests, mocking, coverage, and E2E architecture, with worked answers and real, sourced examples throughout.
Build a reviewable frontend developer GitHub profile with a focused README, proof-led pinned repositories, project READMEs, demos, and cleanup checks.