Real Git interview questions with detailed answers on branching, merge vs rebase, reset vs revert, conflicts and recovering lost commits.
50questions with answers
Git is a distributed version control system: every developer's clone contains the full project history, not just the latest files. A centralized system like SVN keeps the history on one server and clients only hold a working copy.
What this changes in practice:
push and pull) instead of every commit going straight to the server.A common mistake is mixing up Git with GitHub. Git is the tool that tracks changes. GitHub, GitLab and Bitbucket are hosting services built around Git repositories, adding pull requests, permissions and CI.
They are the three places a change moves through before it is saved. The working directory holds your files as you edit them, the staging area (index) holds the changes you chose for the next commit, and the repository holds saved commits.
The staging area lets you build clean, focused commits. For example, git add -p lets you stage only some of the changes in a file.
Useful checks: git diff shows unstaged changes, and git diff --staged shows what will go into the next commit. The common confusion is thinking git commit saves everything you edited. It saves only what is staged (unless you use commit -a, which stages tracked files for you).
A branch is just a lightweight movable pointer to a commit, and HEAD is a pointer to the branch (or commit) you are currently on. Creating a branch does not copy any files, which is why it is instant.
When you commit on a branch, the branch pointer moves forward to the new commit. HEAD follows it because it points at that branch.
git switch -c feature/login # create a branch and move to itgit branch # list branches, current one marked with *Think of a branch as a sticky note on a commit, and HEAD as "you are here". If HEAD points straight at a commit instead of a branch, you are in a "detached HEAD" state (see the later question on it).
git fetch downloads new commits from the remote and updates your remote-tracking branches (like origin/main) without touching your own branches or files. git pull is git fetch followed by an automatic merge (or rebase) into your current branch.
git fetch origingit log HEAD..origin/main --oneline # see what is new before you integrate itgit merge origin/main # integrate when readyfetch is the safe option because it never changes your working files, so you can inspect incoming changes first. pull is convenient but can create a merge commit or conflicts right away.
Many teams set git config --global pull.rebase true (or use git pull --rebase) so that local commits are replayed on top of the remote ones, which keeps history linear.
git stash saves your uncommitted changes on a temporary shelf and restores a clean working directory, so you can switch tasks without making a half-finished commit.
git stash push -u -m "wip: login form" # -u also stashes untracked filesgit switch hotfix/payment# ... fix and commit ...git switch -git stash listgit stash pop # re-apply and remove from the stashpop applies the stash and deletes it, while apply applies it and keeps it, which is useful if you want to apply the same changes in more than one place.
Traps: the stash is local and easy to forget, so old stashes pile up. Popping can cause conflicts if the code changed meanwhile. By default, untracked and ignored files are not stashed unless you add -u or -a.
A .gitignore file lists patterns of files that Git should not track, such as build output, logs, node_modules and local config. It only affects untracked files. If a file was already committed, Git keeps tracking it, even after you add it to .gitignore.
To stop tracking a file but keep it on disk:
git rm --cached config/local.envecho "config/local.env" >> .gitignoregit commit -m "Stop tracking local env file"Other useful details:
.gitignore file, and !pattern re-includes something.core.excludesFile (for editor files and OS junk).git check-ignore -v <file> shows which rule ignores a file.Remember that .gitignore does not remove a secret from history. If a secret was committed, it must be rotated (see the later question on leaked secrets).
A good commit does one logical thing, and its message explains why the change was made, not only what changed. The diff already shows what changed.
A widely used format:
Why this matters: git log, git blame and git bisect are only useful when commits are small and well described. A commit titled "misc fixes" containing ten unrelated changes is hard to review, hard to revert, and hard to cherry-pick.
Keep commits atomic: each commit should build and pass tests, so you can revert or bisect to any one of them safely. Some teams use the Conventional Commits style (feat:, fix:) to automate changelogs and versioning.
A lightweight tag is only a name pointing to a commit. An annotated tag is a full Git object that also stores the tagger, date, a message, and can be signed.
git tag v1.0.0 # lightweightgit tag -a v1.0.0 -m "Release 1.0.0" # annotatedgit tag -s v1.0.0 -m "Signed release" # annotated and GPG-signedgit push origin v1.0.0 # tags are not pushed by defaultUse annotated tags for releases, because they record who released it and when, and tools like git describe prefer them. Lightweight tags suit temporary or private markers.
A common trap: git push does not send tags. You push one with git push origin <tag> or all with git push --tags. Also, tags do not move when you commit, unlike branches, which is why they are good for fixed release points.
A clone copies a repository to your machine, a fork is a copy of a repository on the hosting service under your own account, and a branch is a separate line of development inside one repository.
git clone <url> is a Git command. It downloads the full history and sets origin to the source.When working with a fork, add the original as a second remote, usually named upstream, to pull in its latest changes:
git remote add upstream https://github.com/original/project.gitgit fetch upstreamA merge conflict happens when Git cannot automatically combine two sets of changes, usually because both branches changed the same lines of a file, or one branch deleted a file that the other edited.
Git marks the conflicting area in the file:
<<<<<<< HEADtimeout = 30=======timeout = 60>>>>>>> feature/retryEverything between <<<<<<< and ======= is your current branch's version, and the part up to >>>>>>> comes from the branch being merged. You edit the file to the correct final result, remove the markers, and mark it as resolved.
Conflicts are normal and not a sign that something went wrong. They are more common when branches live for a long time, when people reformat shared files, and when many people edit the same file. Small, frequent merges reduce them.
A remote is a named link to another copy of the repository, usually on a hosting service. origin is the default name Git gives to the repository you cloned from. A remote-tracking branch such as origin/main is a local, read-only bookmark that records where that branch was on the remote the last time you fetched.
git remote -v # list remotes and their URLsgit fetch origin # update origin/main, origin/feature, ...git push -u origin feature/login # push and set the upstream branchgit branch -vv # show which remote branch each local branch tracksThe -u option stores the upstream, so later git pull, git push and git status know which branch to compare with and can say "ahead 2, behind 1".
The common misunderstanding is that origin/main is a live view of the server. It is only as fresh as your last git fetch.
A few options turn the default long output into something readable. The most useful ones show a compact graph, filter by author or date, and show the changes themselves.
git log --oneline --graph --decorate --all # compact picture of all branchesgit log -p -- path/to/file # history of one file with the diffsgit log --stat -5 # last 5 commits with files changedgit log --author="Priya" --since="2 weeks ago"git log --grep="timeout" # search commit messagesgit log --first-parent main # follow only the main line, hide merged detailgit shortlog -sn # commits per authorAlso useful: git show <sha> to see one commit in full.
A tip for interviews: say that you read history to understand why code looks the way it does. Good commit messages make these commands valuable, while a history full of "fix" and "wip" commits makes them useless.
merge joins two branches by creating a merge commit that keeps the real history, while rebase replays your commits on top of another branch to produce a straight line of history. Rebase rewrites your commits, so they get new SHAs.
| Merge | Rebase | |
|---|---|---|
| History | Keeps branches, adds a merge commit | Linear, looks like work happened in sequence |
| Original commits | Unchanged | Rewritten (new SHAs) |
| Safe on shared branches | Yes | No |
| Conflict handling | Once, in the merge commit | Possibly once per replayed commit |
Typical use: rebase your own local feature branch on the latest main to keep it up to date and tidy, then merge it (often by pull request).
The golden rule: never rebase commits that others have already pulled. Rewriting shared history gives your teammates diverged branches and duplicated commits.
All three move the current branch to another commit. They differ in what happens to the staging area and the working directory afterwards.
| Mode | Branch pointer | Staging area | Working directory |
|---|---|---|---|
--soft |
Moves | Kept (changes stay staged) | Kept |
--mixed (default) |
Moves | Reset (changes unstaged) | Kept |
--hard |
Moves | Reset | Reset (changes discarded) |
Example: to undo the last commit but keep the changes ready to commit again, use git reset --soft HEAD~1. To redo it from scratch with files unstaged, use --mixed.
--hard is the dangerous one, because it throws away uncommitted work and that work cannot be recovered. Committed work is different: even after reset --hard, the old commits usually remain in the reflog for a while (see the reflog question).
git revert is safe on shared branches because it adds a new commit that undoes an earlier one, keeping history intact. git reset moves the branch pointer backwards and rewrites history, so it should only be used on commits that have not been pushed.
git revert abc1234 # new commit that reverses abc1234git reset --hard HEAD~1 # moves the branch back and discards the last commitIf you reset a branch that others have pulled, their copies still contain the removed commits. When they push or pull, the old commits come back or the histories conflict.
Rule of thumb:
revert.reset or commit --amend is fine.The revert commit also leaves a clear record of what was undone and why, which is useful for audits and incident reviews.
git cherry-pick applies the changes from a specific commit onto your current branch as a new commit with a different SHA. It lets you take one fix without merging the whole branch it lives on.
git switch release/1.4git cherry-pick -x 9fceb02 # -x records the original SHA in the messageGood uses: backporting a bug fix from main to a release branch, or rescuing one useful commit from an abandoned branch.
Traps:
If you are picking many commits, a merge or a rebase is usually cleaner. You can pick a range with git cherry-pick A..B.
Understand both sides, edit the file to the correct result, mark it resolved, and continue the operation. Then test the result.
git status lists the files with conflicts ("both modified").<<<<<<< and >>>>>>>.git add <file> to mark it resolved.git commit for a merge, git rebase --continue during a rebase, or git cherry-pick --continue.If things go wrong, you can start again with git merge --abort or git rebase --abort.
Do not blindly pick "ours" or "theirs" for a whole file. When you do not understand the other side, ask the person who wrote it. A visual merge tool (git mergetool) helps with larger conflicts.
Use git revert to add a commit that undoes the bad change, then push normally. This is the safe way on a shared branch because nobody's history is rewritten.
git revert <bad-sha>git push origin mainFor a range of commits, use git revert A..B. For a merge commit, you need to pass -m (see the question on reverting merges).
Rewriting history with reset and a force push is only acceptable on a private branch nobody else uses, and even then you should use --force-with-lease.
If the bad commit is already deployed, the fastest fix for users is usually rolling back the deployment to the previous build. The revert commit then keeps the repository consistent with what is running.
Special case: if the commit contains a secret, a revert is not enough, because the secret is still in history. That needs rotation and history cleanup.
git commit --amend replaces the most recent commit with a new one, letting you fix its message or add forgotten changes. The risk is that it rewrites history: the amended commit gets a different SHA.
git add forgotten-file.txtgit commit --amend --no-edit # add the file, keep the messagegit commit --amend -m "Better message"If the original commit was not pushed, this is perfectly safe. If it was already pushed to a shared branch, amending creates a diverged history, and you would need a force push, which can upset teammates.
For an older commit (not the last one), use interactive rebase: git rebase -i HEAD~3 and mark the commit with edit or reword.
A habit that avoids the problem: amend freely before you push, and after pushing prefer a new "fixup" commit or a revert.
They are three ways to bring a pull request into the target branch, and each gives a different history. The right choice depends on how much you value the detailed history versus a clean one.
| Strategy | Result on main | Good for | Downside |
|---|---|---|---|
| Merge commit | All commits plus a merge commit | Keeping full history and context | Noisy history with many small commits |
| Squash and merge | One new commit for the whole PR | Clean, readable history; easy to revert one PR | Loses individual commits and authorship detail |
| Rebase and merge | Each commit replayed, no merge commit | Linear history that keeps each commit | Commits get new SHAs; needs well-formed commits |
Teams that want one revertable unit per change often choose squash. Teams that write careful, atomic commits may prefer rebase. Whatever you choose, apply it consistently, and be aware that after a squash merge, the original feature branch should be deleted: reusing it causes duplicate-looking conflicts.
They differ in how many long-lived branches exist and how fast code reaches production. Choose the simplest model that fits your release process.
main and develop, plus feature, release and hotfix branches. It suits software with formal versioned releases (installed or mobile software), but it is heavy and slows continuous delivery.main is always deployable, work happens on short-lived feature branches, and each merges by pull request, then deploys. Simple and popular for web services.main (the trunk) in very small changes, at least daily. Unfinished work hides behind feature flags. It gives the fastest feedback and fits CI/CD best.For continuous delivery, GitHub Flow or trunk-based works best, because long-lived branches delay integration and make merges painful. The trap is picking Git Flow only because it is familiar, then fighting it when you deploy several times a day.
A protected branch is a branch with rules enforced by the hosting platform, so that nobody can change it in risky ways, even by accident. They are the main guard for production code.
Common rules:
CODEOWNERS) for sensitive paths.Why it matters: it turns team agreements into enforced rules, and gives an audit trail through pull requests.
Weak spots to mention: administrators can often bypass rules unless you restrict that, and protection applies on the server only. Local hooks can be skipped, so important checks must run in CI as well.
git bisect does a binary search through history: you tell Git one good commit and one bad commit, and it checks out the commit in the middle for you to test, halving the range each time.
git bisect startgit bisect bad # current commit has the buggit bisect good v2.3.0 # this older tag was fine# Git checks out a middle commit; test it, then run:git bisect good # or: git bisect bad# repeat until Git prints the first bad commitgit bisect resetWith 1,000 commits between good and bad, you need only about 10 tests.
You can automate it with a script that exits 0 for good and non-zero for bad:
git bisect run ./test-for-bug.shSmall, atomic commits that each build make bisect far more effective. Commits that do not compile can be skipped with git bisect skip.
--force overwrites the remote branch with your version no matter what is there. --force-with-lease does the same only if the remote branch is still exactly where you last saw it, so it refuses to overwrite commits you have not seen.
git push --force-with-lease origin feature/loginWhen you need it: after you rebased or amended commits on your own branch that was already pushed, the histories differ and a normal push is rejected.
Why --force-with-lease is safer: if a teammate pushed a new commit to the same branch meanwhile, the push fails instead of silently deleting their work.
It is not perfect. If your editor or tool runs git fetch in the background, your local view of the remote updates and the "lease" check passes even though you have not looked at the new commits. For maximum safety, you can pin the expected commit with --force-with-lease=<branch>:<sha>.
Never force push to protected shared branches like main.
Detached HEAD means HEAD points directly at a commit instead of a branch. It happens when you check out a tag, a commit SHA or a remote-tracking branch such as origin/main.
In this state you can look around and even commit, but those new commits belong to no branch. When you switch away, they become unreachable and will eventually be garbage collected.
To keep your work:
git switch -c rescue-branch # create a branch at the current commitTo leave without keeping anything: git switch main (or git switch -).
If you already switched away and lost the commits, find them with git reflog, then create a branch at that SHA.
Detached HEAD is not an error. It is useful for inspecting an old release or testing a tag. Just create a branch before you make changes you want to keep.
git checkout was the old all-purpose command for two different jobs: changing branches and restoring files. Newer Git splits those jobs into git switch (branches) and git restore (files), which is clearer and safer.
git switch main # change branchgit switch -c new-branch # create and changegit restore file.txt # discard unstaged changes in a filegit restore --staged file.txt # unstage a file (keep the edits)git restore --source=HEAD~2 file.txt # take the file from an older commitgit reset is different: it moves the branch pointer (and optionally the index and working tree). It works on the history position, while restore works on files.
Warning: git restore file.txt discards your uncommitted edits to that file permanently, because they were never saved in any commit. Check with git diff first.
A merge commit has two parents, so Git needs to know which side to keep. Use -m 1 to keep the first parent (usually the branch you merged into, such as main).
git revert -m 1 <merge-commit-sha>The gotcha: after the revert, Git still remembers that the feature branch was merged. If you fix the feature and merge the same branch again, Git thinks those commits are already included and brings in only the new commits, so the original changes seem to be missing.
Ways to handle it:
Interviewers like this question because it checks that you understand that Git tracks commits and ancestry, not just file contents.
Interactive rebase lets you edit a series of commits before sharing them: reorder, combine, rename, edit or drop them. It is how you turn messy work-in-progress commits into a clean history for review.
git rebase -i HEAD~4Git opens a list of the last four commits, and you change the word at the start of each line:
pick: keep the commit as it is.reword: keep it, change the message.squash: merge into the previous commit and combine messages.fixup: merge into the previous commit and discard this message.edit: stop so you can change the commit.drop: delete the commit.A neat workflow: make small git commit --fixup <sha> commits during review, then run git rebase -i --autosquash main to fold them into the right commits automatically.
Only do this on commits you have not shared (or on your own feature branch), because it rewrites history. If it goes wrong, git rebase --abort returns you to the start.
Two dots compare the tips of both branches directly, while three dots compare feature against the common ancestor (merge base) of the two. Three dots show only what the feature branch added.
git diff main..feature # difference between the two branch tipsgit diff main...feature # changes made on feature since it branched from maingit log main..feature # commits in feature that are not in mainExample: if main has moved on with other people's work, main..feature mixes their changes (shown as removals) with yours, which is confusing. main...feature gives you what a pull request would show: only your changes.
Note that for git log, the meaning of .. is different and very useful: it lists commits reachable from the right side but not from the left. It is the usual way to ask "what would I be pushing?" or "what is in this release but not in the last one?".
A fast-forward merge happens when the target branch has no new commits since the feature branch was created, so Git simply moves the branch pointer forward and no merge commit is made.
git merge feature # fast-forwards if possiblegit merge --no-ff feature # always creates a merge commitgit merge --ff-only feature # fails unless a fast-forward is possible--no-ff: creates a merge commit even when a fast-forward is possible, which keeps a visible record that a group of commits formed one feature, and makes it easy to revert the whole feature with a single revert.--ff-only: enforces a linear history, and is a good setting for git pull so it never creates accidental merge commits.If the branches have diverged, a real merge (or a rebase first) is needed.
Use git blame for who last changed each line, and git log -S (or -G) to find the commit where a piece of text appeared or disappeared.
git blame -L 40,60 app/config.py # who last touched lines 40 to 60git log -S"MAX_RETRIES" --oneline # commits that added or removed this stringgit log -p --follow -- app/config.py # full history of a file, across renamesblame shows only the last change to each line. The last change may just be a formatting commit, not the real origin. Use git blame -w to ignore whitespace, or list noisy commits in a file and point to it with git config blame.ignoreRevsFile .git-blame-ignore-revs.
Then read the commit message and pull request to understand why the change was made. The goal is not to find someone to blame, but to understand the reason, which is why good commit messages matter.
Git hooks are scripts that Git runs automatically at certain points, such as before a commit or before a push. Common examples are pre-commit (run lint, format or secret scanning), commit-msg (check the message format) and pre-push (run tests).
They live in .git/hooks, which is not versioned and not shared when someone clones the repository. To share them across a team:
.githooks/) and set git config core.hooksPath .githooks.pre-commit tool, which installs checks from a config file in the repository.Key limitation: client-side hooks can be skipped with git commit --no-verify, so they are a convenience, not a security control. Anything that must be enforced (tests, secret scanning, policy) should also run in CI or as a server-side check.
Hooks should be fast. Slow hooks make people skip them.
git worktree lets you check out several branches at the same time in separate folders, all sharing one repository (one .git object store). You do not need to stash or to clone the repository a second time.
git worktree add ../hotfix hotfix/payment # new folder with that branch checked outgit worktree listgit worktree remove ../hotfix # clean up when finishedgit worktree prune # remove records of deleted foldersGood uses: fixing an urgent bug while a long build or test run happens on your feature branch, reviewing a pull request in its own folder, or comparing two versions side by side.
Compared with alternatives:
Limits: one branch can be checked out in only one worktree at a time, and forgotten worktrees can keep branches "busy", which causes confusing errors when you try to delete or switch them.
A signed commit carries a cryptographic signature that proves it was created by someone who holds a particular private key. Git records the author name and email as plain text that anyone can set to anything, so without a signature you cannot be sure who really made a commit.
git config --global user.signingkey <KEY_ID>git config --global commit.gpgsign true # sign every commitgit commit -S -m "Fix retry logic"git tag -s v1.2.0 -m "Signed release"git log --show-signature -1Git supports GPG keys, and also SSH keys for signing (gpg.format ssh, available in newer Git versions). Hosting platforms show a "Verified" badge when the signature matches a key on the author's account, and protected branches can require signed commits.
Teams use this for supply-chain security and audits: it makes impersonation harder.
Limits to mention: a signature proves who, not that the code is safe. Rebasing or amending creates new commits that must be signed again, and a squash merge made on the platform is signed by the platform's key, not the author's.
Git reads settings from three main levels, and the more specific level wins: system (all users on the machine), global (your user, in ~/.gitconfig) and local (one repository, in .git/config). There is also a per-worktree level.
git config --list --show-origin # every setting and which file it comes fromgit config --global user.email "me@example.com"git config user.email "me@company.com" # local override inside one repositoryTo avoid committing to work repositories with your personal email, use conditional includes in ~/.gitconfig:
[includeIf "gitdir:~/work/"] path = ~/.gitconfig-workThe file ~/.gitconfig-work then sets user.email and, if needed, a signing key for every repository under ~/work/.
Settings worth knowing: pull.rebase, init.defaultBranch, core.editor, rerere.enabled, push.autoSetupRemote and aliases.
A classic mistake: commits made with the wrong email show up under the wrong person on the hosting platform, and that is annoying to fix because it means rewriting history.
Git LFS (Large File Storage) replaces large files in your repository with tiny pointer files, and stores the real content on a separate LFS server. Git history stays small, and the big files are downloaded only for the version you check out.
git lfs installgit lfs track "*.psd" # writes the rule into .gitattributesgit add .gitattributes design.psdgit commit -m "Add design file with LFS"Pitfalls interviewers like to hear:
git lfs migrate import to rewrite history (this needs coordination).Rule of thumb: LFS is for occasional large assets (design files, models, binaries), not for build output, which should not be in Git at all.
The likely cause is mixed line endings: Windows uses CRLF (\r\n) and Linux and macOS use LF (\n). When files are checked in with different endings, diffs become noisy, and a shell script with CRLF fails on Linux because the interpreter name ends with a hidden \r.
Fix it in the repository, so that it does not depend on each person's settings:
# .gitattributes* text=auto*.sh text eol=lf*.bat text eol=crlf*.png binaryThen renormalise existing files once:
git add --renormalize .git commit -m "Normalize line endings"Personal settings that help: core.autocrlf=true on Windows (convert to LF on commit) and core.autocrlf=input on macOS and Linux.
A .gitattributes file is the preferred solution because it is shared with the repository. Commit the normalisation as its own commit, because it touches many lines and would otherwise hide real changes in git blame.
It means your local branch has commits that the remote does not have, and the remote has commits that you do not have. Neither side contains the other, so a simple push or fast-forward pull is impossible.
First look at both sides:
git fetch origingit log --left-right --oneline main...origin/main # < = local only, > = remote onlyThen choose how to combine them:
git pull (or git merge origin/main) creates a merge commit that joins the two lines.git pull --rebase replays your local commits on top of the remote ones for a linear history. This is fine when your commits are not shared.git branch backup, then git reset --hard origin/main.Do not "fix" it with git push --force: that overwrites the remote commits and deletes your teammates' work. The usual cause is that someone pushed while you were committing, or that you amended or rebased commits that were already pushed.
Use git reflog, which records every place HEAD has been. Commits are rarely truly gone: they usually still exist in the object store for a while, even if no branch points to them.
git reflog # find the SHA from before the mistakegit branch recovered <sha> # make a branch at that commit# or restore the current branch to it:git reset --hard <sha>For a deleted branch, the reflog shows the last commit you were on it, and you recreate the branch there. If the commits were never on HEAD (for example, a stash that was dropped), git fsck --lost-found can list "dangling" commits.
Limits:
git gc can then delete the objects.Rotate the secret immediately, because once it has been pushed you must assume it is compromised. Cleaning Git history is a second step and does not make the old key safe again.
Order of actions:
git filter-repo or BFG Repo-Cleaner, then force push the rewritten branches.pre-commit scanner, and short-lived credentials with environment variables or a secrets manager.A normal git revert or deleting the line in a new commit does nothing for security, because the key is still readable in the old commit.
Find the old commit SHA, then restore the branch to it or branch from it. The commits still exist in the repository's object store and in other people's clones.
Where to find the lost commits:
git reflog or look at git log origin/branch@{1}, because your remote-tracking branch still points to the old tip until you fetch.Then recreate the branch: git branch restore-point <sha>, and push it or merge it.
Prevention:
--force-with-lease instead of --force.The first thing to do is tell people to stop fetching or pulling on that branch until the old SHA is saved.
If the rebase is still in progress, run git rebase --abort to return the branch exactly to how it was before. If it already finished but the result is wrong, go back with the reflog.
git rebase --abort # while the rebase is in progress # if it already finished:git reflog # find the entry "rebase (start)" or the line before itgit reset --hard ORIG_HEAD # often works right after a rebase or merge# or: git reset --hard <sha-from-reflog>ORIG_HEAD is a reference Git sets before risky operations such as rebase, merge and reset, so it points to where you were.
Habits that make this less scary:
git branch backup/my-feature.git config rerere.enabled true so Git remembers how you resolved a conflict and reuses that fix automatically.If the rebase is the wrong tool (huge divergence, many people involved), a merge gives you one set of conflicts to solve.
The usual cause is a large history, often because big binary files (builds, videos, datasets, database dumps) were committed. Git stores every version of every file forever, so large files that changed often make the repository grow permanently.
Options, depending on the problem:
git clone --depth 1 fetches only the latest commit. This is excellent for CI pipelines.git clone --filter=blob:none fetches file contents on demand.git filter-repo (this rewrites history, so it needs coordination and a re-clone by everyone).git gc, and better still, not committing generated files at all (use artifact storage).A trap: only deleting the big file in a new commit does not shrink the repository, because it stays in history.
Branch from the exact code that is running in production, fix it there, release that, and then bring the fix back into main. Do not build the hotfix from main if main holds unreleased work.
git switch -c hotfix/payment-timeout v2.3.0 # branch from the production tag# fix, test, commitgit tag -a v2.3.1 -m "Hotfix payment timeout"git push origin hotfix/payment-timeout v2.3.1# deploy v2.3.1, then bring the fix back to main:git switch maingit cherry-pick -x <fix-sha> # or merge the hotfix branchWhy this works: the hotfix contains only the fix, so you ship a tested, minimal change.
Important points:
main (and any active release branch), or the bug returns in the next release.main always releasable and hiding unfinished features behind feature flags.A monorepo keeps many projects in one repository, while a polyrepo gives each project its own. Neither is better in general; the choice depends on team size, tooling and how tightly the projects are coupled.
| Monorepo | Polyrepo | |
|---|---|---|
| Sharing code | Easy, one commit changes library and users together | Needs versioning and publishing packages |
| Refactoring across projects | Atomic change in one pull request | Many pull requests and coordination |
| Access control | Harder, needs path-based rules (like CODEOWNERS) |
Simple, per repository |
| CI speed | Needs smart tooling to build only what changed | Naturally small and fast per repo |
| Repository size | Can become huge, needs sparse and partial clones | Small repositories |
| Ownership and autonomy | Shared standards, risk of tight coupling | Teams move independently, risk of drift |
For a DevOps view: a monorepo needs path-based CI triggers and caching, or every change rebuilds everything. A polyrepo needs a strategy for shared pipeline templates and dependency updates across many repositories.
The trap is choosing a monorepo without the tooling to support it.
Git is a content-addressable store: it saves everything as objects, and each object is named by a SHA hash of its content. There are four object types, and three do most of the work: blobs, trees and commits.
Key consequences: Git stores snapshots, not diffs (though it compresses similar objects in pack files). Identical files share one blob. Changing anything about a commit, including its parent, changes its SHA, which is why rebase produces new commits. Branches and tags are only small files holding a SHA.
A submodule is a link from your repository to a specific commit of another repository, so you can include external code while keeping its history separate. Subtrees copy the other project's files into your repository. A package manager downloads released versions of a dependency.
| Approach | How it works | Main drawback |
|---|---|---|
| Submodule | Stores a pointer to a commit in another repository | Easy to forget to update or init; confusing for beginners |
| Subtree | Copies the code and history into a folder | Larger repository; more complex to push changes back |
| Package manager | Installs versioned releases | Needs publishing and versioning |
Submodule habits:
git clone --recurse-submodules <url>git submodule update --init --recursivegit submodule update --remote # move to the latest upstream commitA common error: after pulling, the submodule folder is empty or at the wrong commit, because you must update submodules separately. CI jobs also need the flag to fetch them.
Prefer a package manager when the dependency has proper releases. Use submodules when you need a pinned source dependency and you control both repositories.
Make branches smaller and shorter, and integrate more often. Painful merges are caused by large differences that grow over time, so the fix is to reduce the time between integrations.
Practical steps:
main often (at least daily) so conflicts stay small and fresh.main, instead of keeping it hidden on a branch.CODEOWNERS to improve coordination.git config rerere.enabled true so repeated conflicts are resolved automatically.The trap is treating it as a Git skill problem. It is mostly a workflow problem: a better merge tool does not fix a branch that is three months old.
A squash merge creates a brand new commit on main that contains the combined changes. The original commits of your branch are not ancestors of main, so Git does not know they were already merged. When you continue on the old branch and merge again, Git sees all those old commits as new work and tries to apply them a second time, which produces duplicates and conflicts.
The best fix is to move only your new commits onto an up-to-date main with rebase --onto:
git fetch origingit rebase --onto origin/main <last-squashed-commit-of-your-branch> my-branchEverything after <last-squashed-commit-of-your-branch> is replayed on top of origin/main. Another option is to create a fresh branch from main and cherry-pick your new commits.
Prevention: after a squash merge, delete the branch and start the next piece of work from the latest main. Many hosting platforms can delete the branch automatically after merge. If you want to keep working on a long branch, a normal merge or rebase merge avoids the problem.
Fetch the other repository into the target, allow unrelated histories, and merge, ideally after moving its files into a subdirectory so that they cannot clash with existing files. Copying the files would lose all history.
A clean approach:
# 1. In a fresh clone of the repository to import, move everything into a foldergit clone <url-of-service-b> service-b-tmp && cd service-b-tmpgit filter-repo --to-subdirectory-filter services/service-b # 2. In the target repository, bring it in and mergecd ../target-repogit remote add service-b ../service-b-tmpgit fetch service-bgit merge --allow-unrelated-histories service-b/maingit remote remove service-bA simpler option for one-off imports is git subtree add --prefix=services/service-b <url> main.
What to plan for:
git log --follow and git blame still work, because the history is kept.CODEOWNERS and secrets all need updating.filter-repo only on a fresh clone, since it rewrites history.The merge needs the flag because the two histories have no common ancestor, and Git refuses to join them by default.
Yes. They cover the topics interviewers commonly ask DevOps, SRE and developer candidates, from basic commands to recovery scenarios. Each answer is written in plain language with the reasoning behind it.
No. Interviewers care more about whether you understand how Git works (commits, branches, history rewriting) and can recover from mistakes safely. Know the common commands, and be able to explain what they change.
Branching strategies, merge versus rebase, reset versus revert, protected branches, and recovering from mistakes such as force pushes and leaked secrets. These come up often because they affect releases and team safety.