Git & CI/CD interview questions & answers
Version control and delivery: Git internals, branching strategies, merge vs rebase, resolving conflicts, and CI/CD pipelines and deployment strategies.
Official reference: Git docs & Pro Git
Top 58 Git & CI/CD interview questions most asked first
1.What is the difference between Git and GitHub (or GitLab)?easy
Git is a distributed version control system: a tool that records the history of your project in a repository on your own machine. Every clone holds the full history, so you can commit, branch, diff and read the log completely offline.
GitHub and GitLab are hosting platforms built around Git. They store a shared remote copy of the repository and add collaboration features that Git itself doesn't have:
- pull requests (merge requests on GitLab) and code review
- issues, permissions and branch protection
- CI/CD: GitHub Actions, GitLab CI/CD
- releases, package and container registries
Git doesn't depend on them at all: you can push to any server over SSH or HTTPS, or to a plain directory. A good way to say it: Git is the engine, GitHub and GitLab are services where teams park and review what the engine produces.
What interviewers listen for- Git is a distributed version control tool
- Every clone has the full history, works offline
- GitHub/GitLab host remotes and add collaboration
- Pull requests and CI are platform features, not Git
Likely follow-up: What does "distributed" mean compared with SVN? · What is the difference between a fork and a clone?
2.Explain the working tree, the staging area (index) and the repository. Why does Git have a staging area?easy
Git keeps your files in three places:
- Working tree: the files you actually see and edit on disk.
- Staging area (index): a snapshot of what the next commit will contain.
git addcopies the current content of a file into it. - Repository: the
.gitdirectory, holding every commit, tree and blob plus the refs that point at them.git committurns the index into a new commit.
The staging area lets you build a commit deliberately instead of committing everything that changed. If you fixed a bug and also refactored something unrelated, you can stage and commit them separately, even individual hunks with
git add -p, which makes history easier to review, revert and bisect.The matching diffs:
git diffshows working tree vs index, andgit diff --stagedshows index vs the last commit.What interviewers listen for- Working tree is what you edit
- Index is the snapshot of the next commit
.gitstores commits, trees, blobs and refs- Staging lets you craft focused commits (
git add -p)
Likely follow-up: What does
git add -plet you do? · How do you unstage a file without losing the change?3.What is the difference between
git mergeandgit rebase, and when would you use each?midBoth bring changes from one branch into another, but they record history differently.
Merge joins two lines of history. If the target hasn't moved since you branched, Git just moves the pointer forward (a fast-forward). Otherwise it creates a merge commit with two parents. Nothing is rewritten, so it's always safe on shared branches, but history can become a tangle of merge bubbles.
Rebase takes your commits and replays them one by one on top of another base, creating new commits with new SHAs. The result is a linear history, as if you'd started from the latest
main.The golden rule: don't rebase commits that other people already have, such as a shared branch, because you'd rewrite history they built on.
In practice: rebase your own feature branch onto
mainto stay current and tidy it up before review; merge (or squash-merge via the PR) to integrate into shared branches.git switch feature git rebase main # replay feature's commits on top of main git switch main git merge feature # now just a fast-forward git merge --no-ff feature # force a merge commit even if FF is possibleWhat interviewers listen for- Merge preserves history; may create a two-parent merge commit
- Fast-forward just moves the branch pointer
- Rebase rewrites commits with new SHAs, linear history
- Golden rule: never rebase shared, published commits
Likely follow-up: What does
--no-ffdo and why would a team want it? · What happens to conflicts during a rebase?4.What is the difference between
git fetchandgit pull? What doesgit pull --rebasedo?easygit fetchdownloads new commits from a remote and updates your remote-tracking branches such asorigin/main. It never touches your local branches or working tree, so it's always safe: you can inspect what changed before deciding what to do.git pullis fetch plus integrate: after fetching, it merges the upstream branch into your current branch, or rebases onto it if configured. If your branch can simply fast-forward, it does that. If both sides have new commits and you haven't configured a preference, modern Git stops with "Need to specify how to reconcile divergent branches" and asks you to setpull.rebaseorpull.ff.git pull --rebasereplays your local unpushed commits on top of the fetched upstream instead of creating a merge commit, which avoids noisy "Merge branch 'main' of ..." commits.Also note:
git statussaying "up to date" only reflects your last fetch.What interviewers listen for- Fetch updates remote-tracking branches only
- Pull = fetch + merge (or rebase)
--rebasereplays local commits, no merge commit- Divergent pull without config errors in modern Git
Likely follow-up: What does
pull.ff onlydo? · Why mightgit statussay you are up to date when you are not?5.Explain
git reset --soft,--mixedand--hard. How is that different fromgit revert?midgit resetmoves the current branch pointer to another commit. The mode decides what happens to the index and working tree:--soft: only the branch moves; the undone changes stay staged. Handy for re-doing or combining the last few commits.--mixed(the default): the index is reset too, so changes remain in your files but unstaged.--hard: index and working tree are overwritten. Uncommitted work is lost; the dropped commits are recoverable only through the reflog.
git revertdoesn't move anything backwards. It creates a new commit that applies the inverse of an existing one, so history is preserved.The rule of thumb: use
reseton local commits nobody else has; userevertfor anything already pushed to a shared branch, because rewriting shared history forces everyone else to recover.git reset --soft HEAD~1 # undo commit, keep changes staged git reset HEAD~1 # --mixed (default): keep changes, unstaged git reset --hard HEAD~1 # drop the commit AND the changes git revert abc1234 # new commit that undoes abc1234What interviewers listen for- Reset moves the branch pointer
- soft keeps staged, mixed keeps unstaged, hard discards
- Revert adds a new inverse commit
- Revert for shared history, reset for local only
Likely follow-up: How would you recover from an accidental
reset --hard? · How do you revert a merge commit?6.How do you resolve a merge conflict? Walk me through it.easy
A conflict happens when both sides changed the same lines (or one side edited a file the other deleted) and Git can't decide automatically.
- Run
git statusto see the files marked "both modified". - Open each file. Between
<<<<<<<and=======is your side (HEAD), and between=======and>>>>>>>is the incoming side. Withmerge.conflictStyleset tozdiff3(ordiff3), Git also shows the common ancestor, which makes the intent of each side much clearer. - Edit the code into the correct combined result and delete the markers. Sometimes you keep one side wholesale with
git checkout --oursor--theirs. git addeach file to mark it resolved, run the tests, thengit commit(orgit rebase --continue).
If it goes wrong,
git merge --abortreturns you to the state before the merge. Prevention is better: small PRs and frequent integration keep conflicts small.<<<<<<< HEAD greeting = "hey" ||||||| be0d1df greeting = "hi" ======= greeting = "hello" >>>>>>> featureWhat interviewers listen forgit statuslists conflicted files- Understand both sides, remove markers
git addmarks resolved, then commit or continuegit merge --abortbails out safely- zdiff3 conflict style shows the common ancestor
Likely follow-up: What do "ours" and "theirs" mean during a rebase? · What is
git rerere?- Run
7.What does
git stashdo, and what are its gotchas?easygit stashsaves your uncommitted changes, both staged and unstaged, onto a stack and resets the working tree toHEAD. It's useful when you need a clean tree to switch branches, pull or check something quickly without making a throwaway commit.git stash popreapplies the latest entry and drops it;git stash applyreapplies but keeps it.git stash listshows entries asstash@{0},stash@{1}and so on;-mgives them a readable message.
Gotchas:
- Untracked files are not stashed by default; use
-u(or-ato include ignored files too). - If
pophits a conflict, the entry is not dropped; resolve it andgit stash dropyourself. - Stashes are local only. They aren't pushed, and old ones pile up and get forgotten, so for anything longer than a few minutes a WIP commit on a branch is safer.
What interviewers listen for- Saves staged and unstaged changes, cleans the tree
- pop drops the entry, apply keeps it
- Untracked files need
-u - On conflict, pop keeps the stash entry
- Stashes are local, never pushed
Likely follow-up: How do you turn a stash into a branch?
8.What actually is a branch in Git, and what is
HEAD?easyA branch is just a named, movable pointer to a commit: a ref under
refs/heads/that stores one commit SHA. That's why creating a branch is instant and cheap; Git doesn't copy any files. When you commit on a branch, Git creates the new commit and moves that branch's pointer to it.HEAD is Git's pointer to what you currently have checked out. Normally it's a symbolic ref to a branch (
ref: refs/heads/main), so new commits advance that branch. If HEAD points directly at a commit instead, you're in detached HEAD state.Useful notation:
HEAD~1(orHEAD~) is the first parent of HEAD,HEAD~3goes three generations back along first parents, andHEAD^2is the second parent of a merge commit.Tags are also refs, but they're meant to stay fixed, while branches move.
What interviewers listen for- Branch = movable pointer to a commit SHA
- Creating branches is cheap, nothing copied
- HEAD usually points to the current branch
- HEAD pointing at a commit = detached HEAD
~walks first parents,^2is a merge's second parent
Likely follow-up: What happens to commits made in detached HEAD state?
9.What is the difference between continuous integration, continuous delivery and continuous deployment?easy
They're three increasing levels of automation.
Continuous integration (CI): developers merge small changes into the main branch frequently, at least daily, and every push or pull request triggers an automated build and test run. The goal is fast feedback and a main branch that is always green.
Continuous delivery: every change that passes the pipeline produces an artifact that is ready to release and has been deployed to production-like environments. Going to production is a push-button business decision, often with an approval step.
Continuous deployment: there's no manual gate. Every change that passes all the automated checks is deployed to production automatically.
Continuous deployment only works with strong automated tests, good monitoring and alerting, feature flags to hide unfinished work, and fast, reliable rollback. Many teams practise continuous delivery and keep a human approval for production.
What interviewers listen for- CI: frequent merges, automated build and tests
- Delivery: always releasable, manual production decision
- Deployment: every green change ships automatically
- Needs tests, monitoring, feature flags, rollback
Likely follow-up: What would stop you from moving a team to continuous deployment?
10.You just committed with a typo in the message and forgot a file. How do you fix it, and what if you had already pushed?easy
git commit --amendreplaces the last commit with a new one built from the current index. Stage the forgotten file, then amend;--no-editkeeps the message, or-mrewrites it. If you want to undo the commit completely but keep the work,git reset --soft HEAD~1.Amending doesn't edit the old commit, it creates a new commit with a new SHA. The old one is still reachable through the reflog for a while.
If it's not pushed, that's the end of it. If it is pushed, your local branch and the remote have now diverged, so publishing the fix needs a force push, ideally
git push --force-with-lease. That's fine on your own feature branch, but on a shared branch likemaindon't rewrite history: make a follow-up commit or agit revertinstead.What interviewers listen for--amendreplaces the last commit- Amended commit gets a new SHA
reset --soft HEAD~1undoes but keeps changes- Pushed? Needs
--force-with-lease, only on your own branch
Likely follow-up: How would you fix a commit three commits back?
11.How do you throw away all local changes, including untracked files, and get back to the last commit?easy
There are two separate kinds of local change:
- Tracked files you modified or staged.
git restore --staged --worktree .resets both the index and the files toHEAD;git reset --harddoes the same. Plaingit restore .only discards unstaged changes. - Untracked files Git has never recorded. Neither command touches them.
git cleandeletes them:-fis required to actually delete,-dincludes directories, and-xalso removes ignored files, which may include your.envor local config.
Always run
git clean -n(dry run) first, because this is one of the few truly destructive operations: discarded uncommitted work isn't in the reflog and can't be recovered.If you might want the changes later,
git stash -uis the safer option: it clears the working tree but keeps everything, including untracked files, in the stash.git status --short # see what you're about to lose git restore --staged --worktree . # tracked files back to HEAD git clean -n -d # dry run: untracked files and dirs git clean -f -d # delete them git clean -f -d -x # also delete ignored files (build output, .env!)What interviewers listen forrestore --staged --worktree .orreset --hardfor tracked filesgit clean -fdremoves untracked files and dirs-xalso deletes ignored files; beware.env- Dry-run with
git clean -nfirst git stash -uif you might need it later
- Tracked files you modified or staged.
12.What does
git cherry-pickdo, and when is it the right tool?midgit cherry-picktakes the changes introduced by an existing commit and applies them as a new commit on your current branch. The new commit has the same diff and message but a different parent, so it gets a different SHA.Typical uses:
- Backporting a bug fix from
mainto a release branch. - Rescuing one useful commit from a branch you're otherwise abandoning.
Useful options:
-xappends "(cherry picked from commit ...)" to the message, which is great for traceability on release branches;-napplies without committing; a rangeA..Bpicks everything afterAup toB;-m 1is needed to pick a merge commit. Conflicts are resolved like a merge, thengit cherry-pick --continue.It's not a replacement for merging: if you need most of a branch, merge it. Heavy cherry-picking duplicates commits across branches and makes it hard to tell what has really been shipped where.
What interviewers listen for- Copies a commit's diff onto the current branch
- New commit, new SHA
- Great for backporting hotfixes
-xrecords the original commit- Overuse duplicates history; merge whole branches instead
Likely follow-up: Why does Git sometimes skip a cherry-picked commit during a later rebase?
- Backporting a bug fix from
13.Compare Git Flow, GitHub Flow and trunk-based development. Which would you pick and why?mid
Git Flow uses two long-lived branches,
main(released code) anddevelop(integration), plusfeature/*,release/*andhotfix/*branches. It suits software with versioned, scheduled releases or several supported versions, like installed apps or libraries. For a web service it's heavy: long-lived branches mean big merges and slow feedback.GitHub Flow has one long-lived branch,
main, which is always deployable. You create a short-lived branch, open a pull request, CI runs, it gets reviewed, merged and deployed. Simple and a good default for most web teams.Trunk-based development goes further: everyone integrates into trunk at least daily, via very short-lived branches or direct commits, and unfinished work is hidden behind feature flags. Release branches, if any, are cut from trunk. It's what high-performing CI/CD teams tend to use, but it demands good automated tests and discipline.
I'd pick GitHub Flow or trunk-based for a continuously deployed service, and Git Flow only when release management genuinely requires it.
What interviewers listen for- Git Flow: develop, release and hotfix branches
- Git Flow suits versioned, scheduled releases
- GitHub Flow: main always deployable, short PR branches
- Trunk-based: integrate daily, feature flags
- Choice depends on release cadence and test maturity
Likely follow-up: How do you handle an urgent hotfix in trunk-based development? · What goes wrong with long-lived feature branches?
14.You added
.envto.gitignore, but Git still tracks it. Why, and how do you fix it?easy.gitignoreonly applies to untracked files. It stops new files from being picked up bygit addandgit status, but once a file is tracked, Git keeps tracking changes to it regardless of ignore rules.To fix it, add the pattern and remove the file from the index only with
git rm --cached .env(-rfor a directory), then commit. The file stays on your disk and is now ignored.Two gotchas:
- For everyone else, that commit is a deletion, so when they pull, Git removes the file from their working tree. Warn the team or have them back it up.
- The file is still in history. If it contained secrets, treat them as leaked: rotate them, and only then consider rewriting history.
git check-ignore -v pathtells you which rule ignores a file. For personal ignores that shouldn't be committed, use.git/info/excludeor a global excludes file.echo ".env" >> .gitignore git rm --cached .env # stop tracking, keep the file on disk git commit -m "Stop tracking .env" git check-ignore -v .env # .gitignore:1:.env .envWhat interviewers listen for- .gitignore only affects untracked files
git rm --cacheduntracks but keeps the file- Pulling that commit deletes it for teammates
- Still in history: rotate leaked secrets
git check-ignore -vexplains which rule matched
Likely follow-up: How would you remove a leaked secret from the whole history?
15.What is a squash merge? What are its pros and cons compared with a merge commit or rebase-and-merge?mid
A squash merge combines all of a branch's changes into one new commit on the target. Locally,
git merge --squash featurestages the combined result without committing; you then commit it. It isn't a real merge: the new commit has only one parent, so Git doesn't record thatfeaturewas merged. On GitHub or GitLab it's the "Squash and merge" button.Pros: one tidy commit per pull request, a linear and readable
main, and reverting a whole feature is a singlegit revert. Messy "fix typo" commits disappear.Cons: the individual commits are lost from
main(less useful forbisectandblameon large PRs), and because Git can't see the branch as merged,git branch -drefuses to delete it. If you keep committing on that branch and merge again, you'll get duplicate changes or conflicts.A merge commit keeps every commit plus the branch structure; rebase-and-merge keeps every commit but linearly.
What interviewers listen for- All branch changes become one commit
- Only one parent: branch not seen as merged
- Clean linear history, easy one-commit revert
- Loses granular commits for bisect and blame
- Don't keep working on a squash-merged branch
Likely follow-up: Which merge method would you enforce on a team, and why?
16.You ran
git reset --hardand lost commits. How do you get them back?midUse the reflog. Every time
HEADor a branch tip moves (commit, reset, rebase, checkout, amend), Git records the old and new position in a local log. Commits that no branch points to still exist in the object database, and the reflog tells you their SHAs.Run
git reflog, find the entry just before the mistake, and either create a branch there (git branch rescue <sha>) or move your branch back withgit reset --hard HEAD@{1}.ORIG_HEADalso holds the previous tip after a reset, merge or rebase.Limits worth mentioning:
- The reflog is local only; it isn't pushed or shared, and a fresh clone has none.
- Entries expire (by default after 90 days, or 30 for unreachable commits), after which
git gccan prune the objects. - Uncommitted changes discarded by
reset --hardwere never commits, so the reflog can't help. Previously staged content may survive as dangling blobs (git fsck --lost-found), but that's a long shot.
git reflog # 14424d6 HEAD@{0}: reset: moving to HEAD~1 # 88bc598 HEAD@{1}: commit: Add payment retries git branch rescue 88bc598 # safest: put a branch on it git reset --hard HEAD@{1} # or move the current branch backWhat interviewers listen for- Reflog records every move of HEAD and branches
- Find the old SHA, branch or reset to it
- Local only, never pushed
- Entries expire; gc eventually prunes
- Uncommitted work is not in the reflog
Likely follow-up: How would you recover a branch you deleted with
git branch -D?17.What is "detached HEAD" state, how do you get into it, and what happens to commits you make there?mid
Normally
HEADpoints to a branch, and the branch points to a commit. In detached HEAD state,HEADpoints directly at a commit. You get there by checking out a SHA, a tag or a remote-tracking branch (git checkout origin/main), and Git itself detaches HEAD during rebase and bisect. Many CI systems also check out a specific commit rather than a branch.It's perfectly fine for looking around, building an old version or testing. You can even commit, but no branch moves to include those commits. When you switch away, they're left unreachable; Git warns "you are leaving 1 commit behind" and prints the SHA. Eventually garbage collection deletes them.
To keep the work, create a branch while you're still there,
git switch -c name. If you already left, find the SHA in the warning or ingit reflogand rungit branch name <sha>.What interviewers listen for- HEAD points at a commit, not a branch
- Caused by checking out a SHA, tag or remote branch
- New commits belong to no branch
- Create a branch to keep them:
git switch -c - Rescue later via reflog before gc
18.What is a commit internally? Explain blobs, trees, commits, refs and the SHA hash.mid
Git is a content-addressed object store. Every object is identified by a hash of its type, size and content: SHA-1 by default, with SHA-256 repositories available via
git init --object-format=sha256.- Blob: the raw content of a file. No name, no permissions, so identical content is stored once.
- Tree: a directory listing mapping names and modes to blobs and sub-trees.
- Commit: a pointer to one root tree (a full snapshot of the project), zero or more parent commits, author, committer, timestamps and the message.
- Annotated tag: an object pointing at a commit, with a tagger and message.
Refs such as branches and tags are just human-friendly names that store a SHA.
So a commit is a snapshot, not a diff; unchanged files reuse the same blob. Because a commit's hash covers its tree and its parents' hashes, changing anything in history changes every descendant's SHA. That's why rewriting history produces new commits and why a SHA identifies an exact state.
git cat-file -p HEAD # tree b4ed918248039b78f24383523fa4e51f80994fac # parent 14424d6... # author Ana <ana@example.com> 1790561526 +0530 git cat-file -p HEAD^{tree} # 100644 blob ce013625030ba8dba906f756967f9e9ca394464a f.txt echo hello | git hash-object --stdin # ce0136250...What interviewers listen for- Objects are named by the hash of their content
- Blob = file content, tree = directory listing
- Commit = root tree + parents + metadata
- Commits are snapshots, not diffs
- Parent hashes chain history; rewrites change SHAs
Likely follow-up: If commits are snapshots, why is a Git repo so small? · Why does amending a commit change its SHA?
19.Walk me through the stages of a typical CI/CD pipeline for a web service.mid
A typical pipeline, triggered by a push or pull request:
- Build: check out the exact commit, install dependencies from the lockfile, compile.
- Fast checks: lint, formatting, type checks and unit tests. Cheapest first, so failures surface in minutes.
- Security scans: SAST on the code, dependency (SCA) scanning for vulnerable packages, secret scanning, and later container image and IaC scans.
- Package the artifact: produce one immutable, versioned artifact, such as a container image tagged with the commit SHA, and push it to a registry.
- Deploy to staging and run integration, end-to-end and smoke tests against it.
- Approval (continuous delivery) or automatic promotion (continuous deployment).
- Deploy to production progressively, canary or rolling, with health checks and automatic rollback.
Principles: fail fast, parallelise independent jobs, and build once, promote the same artifact through every environment instead of rebuilding, so what you tested is exactly what you ship.
What interviewers listen for- Build, fast checks, security scans, artifact, deploy
- Cheapest, fastest checks first
- One immutable artifact tagged with the SHA
- Build once, promote the same artifact
- Progressive production deploy with rollback
Likely follow-up: Where would you run end-to-end tests, and why not on every commit?
20.Explain the building blocks of a GitHub Actions workflow: triggers, jobs, steps and runners.mid
A workflow is a YAML file in
.github/workflows/.- Triggers (
on): events such aspush,pull_request,workflow_dispatch(manual button),schedule(cron, UTC by default),releaseorworkflow_call(reusable workflows). You can filter by branch, tag or path. - Jobs: units of work that run in parallel by default, each on its own runner.
needscreates dependencies and ordering. Jobs don't share a filesystem, so you pass files between them with artifacts. - Runners: the machines that execute jobs, chosen by
runs-on. GitHub-hosted runners (ubuntu-latest,windows-latest,macos-latest) give each job a fresh VM; self-hosted runners are your own machines. - Steps: run in order inside a job and share its workspace. A step either
usesan action (reusable code likeactions/checkout) with inputs inwith, orruns shell commands.
Other common keys:
env,permissionsfor theGITHUB_TOKEN,strategy.matrix,environmentandconcurrency.name: CI on: push: branches: [main] pull_request: jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v7 with: { node-version: 22, cache: npm } - run: npm ci - run: npm testWhat interviewers listen for- Workflows live in
.github/workflows/ ondefines triggers: push, pull_request, schedule...- Jobs run in parallel;
needsorders them - Each job gets a runner; hosted runners are fresh VMs
- Steps
usesactions orruncommands
Likely follow-up: How do two jobs share a build output? · How would you stop two deployments running at once?
- Triggers (
21.You rebased a branch that is already pushed. How do you publish it safely, and why prefer
--force-with-leaseover--force?midAfter a rebase or amend, your local branch no longer contains the remote tip, so a normal push is rejected as non-fast-forward. You have to force it.
--forceoverwrites the remote branch with whatever you have, no questions asked. If a teammate pushed a commit since you last fetched, it silently disappears.--force-with-leaseonly overwrites the remote ref if it still points where your remote-tracking branch says it does, the state you last saw. If someone pushed in the meantime, the push fails and you can fetch and incorporate their work. The caveat: if your IDE fetches in the background, your tracking ref is already updated and the lease protects nothing.--force-if-includesadds a check that the remote tip is actually in your local branch's history.Team rules: only force-push your own feature branches, protect
mainso force pushes are blocked, and tell anyone else on the branch so they can reset onto the new history.What interviewers listen for- Rewritten history needs a force push
--forcecan silently delete others' commits- Lease: only overwrite if remote is what you last fetched
- Background fetch defeats the lease;
--force-if-includeshelps - Never force-push shared or protected branches
Likely follow-up: What should a teammate do after you force-pushed a branch they have checked out?
22.How do you clean up a messy feature branch before opening a pull request?mid
With an interactive rebase:
git rebase -i main(orHEAD~5) opens a todo list with one line per commit, oldest first. You edit the list and Git replays it:pickkeeps a commit,rewordchanges its message,editstops so you can amend or split it.squashmelds a commit into the previous one and combines messages;fixupdoes the same but discards its message.drop(or deleting the line) removes a commit; reordering lines reorders commits.execruns a command, for example the tests, after a commit.
A nice workflow is to make corrections as
git commit --fixup=<sha>while you work, thengit rebase -i --autosquash mainmoves each fixup under its target automatically.If it goes wrong,
git rebase --abortrestores the branch, and the reflog has the old tip. Because it rewrites commits, do it only on your own branch, then publish with--force-with-lease.git commit --fixup=5dc6f3f # "fixup! add parser" commit git rebase -i --autosquash main # editor opens the todo list: # pick 5dc6f3f add parser # fixup 31af573 fixup! add parser # reword cd4a1d2 add tests # drop 9e1b77a debug loggingWhat interviewers listen forgit rebase -iedits a todo list of commits- pick, reword, edit, squash, fixup, drop, exec
--fixupplus--autosquashautomates squashing--abortor reflog to undo- Only on unshared branches
Likely follow-up: How would you split one commit into two?
24.What are remotes, remote-tracking branches and upstream branches? How do
originandupstreamdiffer?easyA remote is a named URL of another copy of the repository.
git clonecreates one calledoriginfor the place you cloned from. In a fork workflow, people conventionally addupstreamfor the original project, so they pull fromupstreamand push to their fork,origin. The names are only conventions.A remote-tracking branch like
origin/mainis your local, read-only snapshot of where that branch was on the remote at your last fetch. You don't commit to it;git fetchupdates it.A local branch can have an upstream (tracking) branch configured. That's what makes plain
git pullandgit pushknow where to go and letsgit statusreport "ahead 2, behind 1". Set it withgit push -u origin featureorgit branch --set-upstream-to=origin/feature.git fetch --pruneremoves tracking refs for branches deleted on the remote.What interviewers listen for- Remote = named URL;
origincreated by clone upstreamconventionally the original repo of a forkorigin/mainis a cached snapshot from last fetch- Upstream config powers pull, push and ahead/behind
push -usets the upstream
- Remote = named URL;
25.What is the difference between forking and cloning a repository? Describe the fork-based contribution workflow.easy
A clone is a Git operation:
git clonecopies a repository, with its full history, onto your machine.A fork is a hosting-platform feature: GitHub or GitLab creates a server-side copy of someone else's repository under your account, remembering which project it came from. Forks exist so you can contribute to a project you don't have write access to.
The usual open-source workflow:
- Fork the project, then clone your fork; it becomes
origin. - Add the original project as a second remote, conventionally
upstream. - Create a branch, commit, and push the branch to your fork.
- Open a pull request from your fork's branch to the upstream repository.
- Keep up to date by fetching
upstreamand rebasing or merging itsmain, or with the platform's "sync fork" button.
Inside a company, where developers usually have write access, people normally skip forks and push branches to the shared repository.
What interviewers listen for- Clone: local copy with full history (Git)
- Fork: server-side copy under your account (platform)
- Forks let you contribute without write access
originis your fork,upstreamthe original- PR goes from fork branch to upstream
- Fork the project, then clone your fork; it becomes
26.Compare rolling, blue-green and canary deployments. When would you use each?mid
Rolling: replace instances of the old version with the new one a few at a time, keeping the service up. It needs little extra capacity and is the default for a Kubernetes Deployment. The downsides: old and new versions serve traffic side by side for a while, and rolling back means rolling through all instances again.
Blue-green: run two identical environments. Blue serves production while you deploy and test green; then you switch all traffic at the load balancer or DNS. Rollback is nearly instant: switch back. It costs double capacity during the switch, and the database must work with both versions.
Canary: send a small slice of real traffic, say 1–5%, to the new version, compare error rates and latency with the baseline, then increase step by step, ideally with automated analysis that aborts on regression. It has the smallest blast radius, but needs traffic splitting and good observability.
I'd default to rolling for low-risk services, canary for high-traffic, business-critical ones, and blue-green where instant cutover and rollback matter most.
What interviewers listen for- Rolling: gradual replacement, minimal extra capacity
- Blue-green: two environments, instant switch back
- Canary: small traffic slice, watch metrics, ramp up
- All need backward-compatible database changes
- Canary needs traffic splitting and observability
Likely follow-up: How do feature flags differ from a canary release? · How do database migrations fit into blue-green?
27.What makes a good pull request, and what are your code review best practices?easy
As the author:
- Keep PRs small and focused: one logical change that can be reviewed in one sitting. Big PRs get rubber-stamped.
- Write a clear description: why the change is needed, what it does, how you tested it, screenshots for UI, and a link to the ticket.
- Review your own diff first, make sure CI is green, and mark it draft until it's ready.
As the reviewer:
- Respond promptly; waiting on review is a major source of lead time.
- Focus on correctness, design, tests, security and readability. Leave style to linters and formatters.
- Ask questions rather than issue orders, explain the reasoning, and label optional comments as nits so the author knows what blocks the merge.
- Approve when the change improves the codebase, not only when it's perfect.
Enforce the basics with branch protection: required reviews, required status checks, and CODEOWNERS for sensitive areas.
What interviewers listen for- Small, focused PRs with a clear "why"
- Self-review and green CI before requesting review
- Review promptly; focus on correctness and design
- Automate style; label nits as non-blocking
- Branch protection, required checks, CODEOWNERS
Likely follow-up: How do you handle a disagreement in code review?
28.What makes a good commit, and a good commit message?easy
A good commit is atomic: one logical change, complete on its own, with the tests passing. That makes history easy to review, revert, cherry-pick and
bisect. Mixing a bug fix, a refactor and a formatting pass in one commit makes all of those harder.A good message:
- A short summary line, conventionally around 50 characters, in the imperative mood: "Fix token refresh race", not "fixed stuff". It's what
git log --oneline, PR lists and tools show. - A blank line, then a body, wrapped at about 72 characters, explaining why the change was needed and any trade-offs. The diff already shows what changed.
- References to the issue or ticket, and a
BREAKING CHANGEnote if relevant.
Many teams add a Conventional Commits prefix such as
fix(auth):so changelogs and version bumps can be automated. Avoid "WIP" and "misc" commits on shared branches; clean them up with an interactive rebase or a squash merge.What interviewers listen for- Atomic: one logical change, tests pass
- Short imperative summary line
- Blank line, then a body explaining why
- Reference the issue; flag breaking changes
- Clean up WIP commits before merging
- A short summary line, conventionally around 50 characters, in the imperative mood: "Fix token refresh race", not "fixed stuff". It's what
29.What do
git diff,git diff --staged,git diff HEADandgit diff main...featureeach show?easyThey compare different pairs of snapshots:
git diff: working tree against the index, so only changes you haven't staged.git diff --staged(same as--cached): the index againstHEAD, exactly what the next commit will contain.git diff HEAD: working tree against the last commit, staged and unstaged together.git diff main feature(ormain..feature): the two tips directly. Ifmainmoved on, its new commits show up as reversed changes.git diff main...feature:featureagainst the merge base of the two, which is only what the feature branch changed. That's what a pull request shows.
Handy flags:
--stator--name-onlyfor a summary,--word-difffor prose,-wto ignore whitespace.Careful: in
git log, the dots mean something different.main..featurelists commits onfeaturenot onmain, and...is the symmetric difference.git diff # working tree vs index (unstaged changes) git diff --staged # index vs HEAD (what you're about to commit) git diff HEAD # working tree vs HEAD (all local changes) git diff main feature # snapshot of main vs snapshot of feature git diff main...feature # feature's changes since it branched off main git diff --stat HEAD~3 # summary: files and line countsWhat interviewers listen for- Plain diff: unstaged changes only
--staged: what will be committeddiff HEAD: all local changes- Three dots: changes since the merge base, like a PR
- Dots mean different things in
logvsdiff
30.Why were
git switchandgit restoreadded whengit checkoutalready existed?easygit checkoutdoes two unrelated jobs: switching branches and overwriting files from the index or a commit. The same command name for both is confusing, and it's dangerous:git checkout -- filesilently throws away your uncommitted changes, andgit checkout namebehaves differently depending on whethernameis a branch or a file.Git 2.23 split it into two focused commands:
git switchonly changes branches:-ccreates one,-returns to the previous branch,--detachchecks out a commit.git restoreonly restores files: by default the working tree from the index,--stagedto unstage,--source=<commit>to take a file from another commit.
checkoutstill works and you'll see it everywhere in older docs and scripts, butswitchandrestoremake intent explicit and are harder to misuse.git switch main # was: git checkout main git switch -c feature # was: git checkout -b feature git switch - # back to the previous branch git restore app.js # discard unstaged changes to app.js git restore --staged app.js # unstage, keep the change git restore --source=HEAD~2 app.js # bring back an older versionWhat interviewers listen forcheckoutmixed branch switching and file restoringswitchhandles branches onlyrestorehandles files: worktree or--stagedrestorediscards changes, so use it deliberatelycheckoutstill works; new commands are clearer
31.How do you rename or delete a branch, locally and on the remote?easy
Renaming:
git branch -m new-namerenames the current branch, orgit branch -m old newrenames another one. Git has no "rename" on a remote, so you push the branch under the new name withgit push -u origin new-name(which also updates the upstream) and delete the old remote branch. Open pull requests tied to the old name may need attention, and teammates must update their tracking branches.Deleting locally:
git branch -d featureonly succeeds if the branch is fully merged: into its upstream, or into the current branch if it has none. That safety check fails after a squash merge, because Git can't see the commits inmain.git branch -Dforces it; the commits are then reachable only through the reflog.Deleting on the remote:
git push origin --delete feature. Other clones keep a staleorigin/featureuntil they rungit fetch --prune. Most platforms can delete the branch automatically after a PR is merged.git branch -m new-name # rename the current branch git branch -m old-name new-name # rename another branch git push -u origin new-name # publish under the new name git push origin --delete old-name # remove the old remote branch git branch -d feature # delete if fully merged git branch -D feature # force delete (unmerged work!)What interviewers listen forgit branch -mrenames- Remote rename = push new name, delete old
-drequires merged;-Dforcesgit push origin --deleteremoves a remote branchgit fetch --pruneclears stale tracking refs
32.A bug appeared somewhere in the last 300 commits. How would you find the commit that introduced it?mid
I'd use
git bisect, which does a binary search through history. Mark a known bad commit and a known good one, and Git checks out the commit halfway between. You test it and mark itgoodorbad, and each answer halves the range, so 300 commits take about 9 steps.Better still, automate it with
git bisect run <script>. The script's exit code drives the search: 0 means good, 1 to 127 (except 125) means bad, and 125 means "can't test this one, skip it", for example when the build is broken. Anything else aborts the run. A failing test written for the bug is the perfect script.git bisect skiphandles untestable commits manually, andgit bisect resetreturns you to your original branch. You can also rename the terms, for example--term-old=fast --term-new=slow, when hunting a performance regression rather than a bug.Bisect works best when every commit builds and is small, which is one more argument for clean history.
git bisect start git bisect bad # current commit is broken git bisect good v2.3.0 # this release was fine # Git checks out the midpoint; test it, then mark good/bad... git bisect run ./check.sh # or automate: exit 0 good, 1-127 bad, 125 skip git bisect reset # return to where you startedWhat interviewers listen for- Binary search between a good and a bad commit
- About log2(n) steps
bisect run: exit 0 good, 1-127 bad, 125 skipbisect resetwhen finished- Works best with small, buildable commits
Likely follow-up: How does bisect handle merge commits and branches?
33.How do you find out who last changed a line and why? How do you stop a mass reformat from polluting
git blame?easygit blame fileannotates every line with the commit, author and date that last changed it.-Lnarrows it to a line range,-wignores whitespace-only changes, and-M/-Cdetect lines moved within a file or copied from other files.Blame tells you which commit; the useful part is then reading that commit with
git show, its message and its pull request, to understand why. It's for context, not for blaming people.Large formatting commits make every line point at "run prettier". List such commits in a file, conventionally
.git-blame-ignore-revs, and pass it with--ignore-revs-fileor setblame.ignoreRevsFileso blame looks past them. GitHub's blame view honours a.git-blame-ignore-revsfile in the repository root.When a line has been rewritten many times,
git log -Lshows the history of a line range, andgit log -Sfinds commits that added or removed a string.What interviewers listen for- Blame shows last commit, author, date per line
-Lrange,-wwhitespace,-M/-Cmoves- Read the commit and PR to learn why
- Ignore formatting commits via
.git-blame-ignore-revs
34.A deployment broke production. How do you roll back, and how do you design a pipeline so rollbacks are easy?mid
First restore service, then fix properly. The fastest options, roughly in order:
- Turn off the feature flag if the change was behind one: no deploy at all.
- Redeploy the previous artifact. If every build produces an immutable, versioned artifact (an image tagged with the commit SHA), rollback is just deploying the last known-good version: switching blue-green back, aborting the canary, or
kubectl rollout undo. - Then
git revertthe bad commit onmainso the code matches what's running and the next deploy doesn't reintroduce the bug.
Design for it:
- Build once and keep previous artifacts in the registry.
- Make database migrations backward compatible (expand and contract), so the old version can still run against the new schema. Irreversible migrations are what usually make rollback impossible.
- Automate health checks so a failing canary or rollout stops and rolls back on its own.
- Practise rollbacks; an untested rollback path fails when you need it.
Sometimes rolling forward with a small fix is faster, but only if you're confident.
What interviewers listen for- Restore service first: flag off or redeploy
- Immutable versioned artifacts make rollback trivial
git revertso main matches production- Backward-compatible (expand/contract) migrations
- Automated health checks trigger rollback
Likely follow-up: When would you roll forward instead of rolling back?
35.How do you handle database schema changes in a continuous deployment pipeline without downtime?hard
The core problem is that during a rolling, blue-green or canary deployment, old and new application versions run at the same time against the same database, and you may need to roll the application back. So every migration must be compatible with both the current and the previous version.
The standard technique is expand and contract (parallel change). To rename a column:
- Expand: add the new column, nullable. Deploy code that writes to both columns and reads the old one. Backfill existing rows in batches.
- Migrate: deploy code that reads the new column.
- Contract: once no running version uses the old column, drop it in a later release.
Other rules: never drop or rename in the same deploy as the code change; run migrations as a separate pipeline step with a versioned tool like Flyway, Liquibase or Alembic; avoid long table locks (for example, create indexes concurrently in PostgreSQL); and test against production-sized data. Rolling back usually means rolling the app back, not the schema, which is exactly why the schema must stay backward compatible.
What interviewers listen for- Old and new versions run against one database
- Migrations must support current and previous version
- Expand, migrate, contract over several releases
- Backfill in batches; avoid long locks
- Versioned migrations as a separate pipeline step
Likely follow-up: How would you roll back a deployment that included a migration?
36.How do you handle secrets safely in a CI/CD pipeline?mid
- Never commit secrets. Keep them in the CI platform's secret store (repository, organisation or environment secrets in GitHub) or an external vault, and inject them at runtime through environment variables.
- Prefer short-lived credentials. Use OIDC so the job exchanges a signed identity token for temporary cloud credentials instead of storing long-lived access keys.
- Least privilege. Scope each secret to the job and environment that needs it, set the
GITHUB_TOKENpermissionsto read-only by default, and put production secrets behind an environment with required approval. - Don't leak them. Log masking is best-effort: a secret that's transformed (base64, JSON) may not be redacted, so never echo secrets or pass them as command-line arguments.
- Untrusted code. Secrets aren't passed to workflows triggered from forks, and
pull_request_targetshould never run a PR's untrusted code with secrets. Pin third-party actions to a full commit SHA. - Detect and rotate. Enable secret scanning and push protection, rotate regularly, and rotate immediately on any leak; deleting the commit isn't enough.
What interviewers listen for- Secret store or vault, never the repo
- OIDC short-lived credentials over static keys
- Least-privilege tokens and scoped, gated secrets
- Masking is best-effort; never print secrets
- Scan, rotate, and rotate immediately on leaks
Likely follow-up: How does OIDC authentication from GitHub Actions to a cloud provider work?
37.How do matrix builds and dependency caching work in GitHub Actions?mid
A matrix runs the same job once per combination of variables. With two operating systems and two Node versions you get four parallel jobs, each reading its values from
matrix.osandmatrix.node.includeadds or extends combinations,excluderemoves some, andmax-parallellimits concurrency.fail-fastdefaults to true, which cancels the remaining jobs when one fails; set it to false when you want the full picture. A matrix can generate at most 256 jobs per workflow run.Caching saves directories like
~/.npmbetween runs. Thekeyusually includes the OS and a hash of the lockfile, so a dependency change produces a new cache. On an exact key miss,restore-keysare tried as prefixes to restore the most recent close match, and the cache is saved under the new key at the end. Setup actions such asactions/setup-nodeoffer this built in viacache: npm.Caches are scoped by branch (a branch can use its own and the default branch's), unused entries are evicted after 7 days, and you should never cache secrets.
strategy: matrix: os: [ubuntu-latest, windows-latest] node: [22, 24] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v7 with: { node-version: "${{ matrix.node }}" } - uses: actions/cache@v4 with: path: ~/.npm key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} restore-keys: npm-${{ runner.os }}-What interviewers listen for- Matrix = one job per variable combination
include,exclude,max-parallel,fail-fast- Cache key includes OS and lockfile hash
restore-keysgive prefix fallbacks- Branch-scoped; unused caches evicted after 7 days
Likely follow-up: What is the difference between a cache and an artifact?
38.What are feature flags, and how do they enable trunk-based development and continuous deployment?mid
A feature flag is a runtime switch that turns a code path on or off without a new deployment, usually read from a flag service or config.
Their key effect is to decouple deploying from releasing. Unfinished work can be merged into trunk every day behind a flag that's off, so there's no long-lived feature branch, and it deploys harmlessly with everything else. Releasing becomes a flag flip: to internal users first, then a percentage of customers, then everyone. If something goes wrong, turning the flag off is the fastest rollback there is.
Common kinds: short-lived release toggles, ops toggles and kill switches, experiment toggles for A/B tests, and long-lived permission toggles.
The costs are real: every flag doubles the code paths to test, and stale flags become technical debt, so give each one an owner and remove it once the rollout finishes. Define a safe default in case the flag service is unreachable.
What interviewers listen for- Runtime switch, no redeploy needed
- Decouples deployment from release
- Lets unfinished work merge to trunk daily
- Progressive rollout and instant kill switch
- Flag debt: owners, cleanup, safe defaults
Likely follow-up: How would you test code with several flags?
39.What are Conventional Commits, and how do they relate to semantic versioning?easy
Semantic versioning uses
MAJOR.MINOR.PATCH: bump MAJOR for incompatible API changes, MINOR for backward-compatible features, and PATCH for backward-compatible bug fixes. Versions below1.0.0mean initial development, where anything may change. Pre-releases look like2.0.0-rc.1and sort before2.0.0; build metadata after a+is ignored for precedence.Conventional Commits is a commit message convention:
type(scope): description.featmaps to a MINOR bump,fixto a PATCH, and a breaking change, marked by!before the colon or aBREAKING CHANGE:footer, maps to MAJOR. Other types such asdocs,refactor,test,ciandchoreare allowed and don't trigger a release on their own.Because the messages are machine-readable, tools can generate changelogs and compute the next version automatically, and they're easy to enforce with a
commit-msghook or a CI check.feat(cart): add coupon codes # -> MINOR fix(auth): refresh expired tokens # -> PATCH docs: explain rate limits # no release by itself feat(api)!: remove v1 endpoints # ! -> MAJOR BREAKING CHANGE: clients must call /v2 # footer also -> MAJORWhat interviewers listen for- SemVer: MAJOR breaking, MINOR feature, PATCH fix
type(scope): descriptionmessage formatfeatminor,fixpatch,!or footer major- Enables automated changelogs and version bumps
Likely follow-up: What does a
0.xversion tell consumers?40.What are Git hooks, and how do you share pre-commit checks across a team?mid
Hooks are scripts Git runs at certain points. Client-side ones include
pre-commit(lint and test before the commit is created),commit-msg(validate the message),pre-push,post-checkoutandpost-merge. Server-side hooks such aspre-receiverun on the server during a push. For thepre-hooks, a non-zero exit aborts the operation.By default they live in
.git/hooks, which is not versioned or cloned, so every developer would have to install them manually. To share them:- commit a directory like
.githooksand pointcore.hooksPathat it; - or use a hook manager such as the pre-commit framework, Husky or lefthook, which installs hooks from a committed config.
Keep hooks fast, for example by checking only staged files. And remember they're a convenience, not enforcement:
git commit --no-verifyskipspre-commitandcommit-msg. Anything that really matters must also run in CI.#!/bin/sh # .githooks/pre-commit (enable: git config core.hooksPath .githooks) files=$(git diff --cached --name-only --diff-filter=ACM -- '*.js') [ -z "$files" ] && exit 0 npx eslint $files || exit 1 # non-zero exit aborts the commitWhat interviewers listen for- Scripts run at points like pre-commit, commit-msg, pre-push
- Non-zero exit aborts the operation
.git/hooksis not versioned or cloned- Share via
core.hooksPathor a hook manager --no-verifybypasses them; CI must enforce
- commit a directory like
41.Your CI has flaky tests that fail randomly. How do you deal with them?mid
A flaky test passes and fails on the same code. It's dangerous beyond the wasted time: people learn to hit "re-run" and stop trusting red builds, so real failures slip through.
Detect: track test results over time, rerun failures to confirm flakiness, and measure a flake rate per test.
Contain: quarantine known-flaky tests out of the blocking suite, with a ticket, an owner and a deadline, so they don't block everyone but aren't forgotten.
Fix the root cause. The usual suspects:
- timing: fixed sleeps instead of waiting for a condition
- shared state or test order dependence
- real network calls or external services
- dates, time zones and unseeded randomness
- concurrency and resource limits on the runner
Automatic retries are fine as a short-term safety net, but they can hide real race conditions in the product. Treat flakiness as a bug with priority, because a pipeline nobody trusts is worse than none.
What interviewers listen for- Same code, different results erodes trust in CI
- Detect with history and reruns, track flake rate
- Quarantine with owner and deadline
- Fix causes: timing, shared state, network, time
- Retries can hide real race conditions
42.What are the DORA metrics, and how would you use them?mid
DORA's research measures software delivery performance with a small set of metrics. The classic "four keys" are:
- Deployment frequency: how often you deploy to production.
- Lead time for changes: time from commit to running in production.
- Change failure rate: share of deployments that need immediate intervention, such as a rollback or hotfix.
- Time to restore: how quickly you recover, now framed as failed deployment recovery time.
Recent DORA guidance adds a fifth, deployment rework rate: unplanned deployments made because of a production incident. DORA groups them into throughput (frequency, lead time, recovery time) and instability (change failure rate, rework rate), and the key finding is that they aren't a trade-off: top performers are both fast and stable, because small, frequent changes are easier to test and roll back.
Use them at the team or service level to spot bottlenecks and track improvement, measured from version control, the pipeline and incident data. Don't use them to rank individuals or set targets to game.
What interviewers listen for- Throughput: deploy frequency, lead time, recovery time
- Instability: change failure rate, rework rate
- Newer fifth metric: deployment rework rate
- Speed and stability improve together
- Team-level improvement, not individual ranking
Likely follow-up: How would you actually measure lead time for changes?
43.How would you add a manual approval gate before production deployments in GitHub Actions?hard
Use an environment. In the repository settings, create
productionand add deployment protection rules, then reference it from the job withenvironment: production.- Required reviewers: the job pauses until one of the listed people or teams approves. You can also prevent people from approving their own deployments.
- Wait timer: delay the job for a set number of minutes after it's triggered.
- Deployment branches and tags: only allow, say,
mainorv*tags to deploy there. - Custom protection rules can call external systems, such as a change-management check.
Secrets stored on the environment are only available to jobs that reference it, and only after approval, so the production credentials are gated too. Add
concurrencyso two production deploys never overlap, andneedsso production only runs after staging succeeds. Availability of some protection rules for private repositories depends on your GitHub plan.GitLab CI's equivalent is a job with
environment,when: manualand protected environments.deploy-prod: needs: deploy-staging runs-on: ubuntu-latest environment: name: production url: https://shop.example.com concurrency: production # one prod deploy at a time steps: - run: ./deploy.sh env: TOKEN: ${{ secrets.PROD_DEPLOY_TOKEN }} # environment secretWhat interviewers listen for- Job references
environment: production - Protection rules: reviewers, wait timer, branch rules
- Environment secrets released only after approval
concurrencyprevents overlapping deploysneedschains staging before production
44.What is a build artifact, and how should artifacts and container images be stored and promoted?mid
An artifact is the output of a build that you deploy or distribute: a JAR, a binary, a zipped static site, an npm package or a container image. The core principle is build once, deploy many: the same artifact that passed the tests is promoted through staging and production, never rebuilt per environment. Environment differences belong in configuration.
Artifacts live in a repository manager (Nexus, Artifactory, GitHub Packages, npm) and images in a container registry (GHCR, Docker Hub, Amazon ECR, Google Artifact Registry, GitLab's registry).
Good practice:
- Tag images immutably with the commit SHA and a version; don't deploy
latest. For exactness, deploy by digest (@sha256:...). - Scan images for vulnerabilities, and consider signing them and publishing an SBOM.
- Set retention policies, but keep enough old versions to roll back.
Don't confuse this with CI workflow artifacts (
actions/upload-artifact), which pass files between jobs and expire after a retention period; they're not a release registry.What interviewers listen for- Artifact = deployable build output
- Build once, promote the same artifact
- Immutable tags (SHA, version), avoid
latest - Scan, sign, keep versions for rollback
- CI job artifacts are temporary, not a registry
- Tag images immutably with the commit SHA and a version; don't deploy
45.How do you revert a merge commit, and what happens when you later try to merge that branch again?hard
A merge commit has two parents, so
git revertneeds to know which side is the mainline:git revert -m 1 <merge>keeps the first parent, the branch you merged into, and undoes everything the merged branch brought in. Without-m, Git refuses.The gotcha is that a revert undoes the changes, not the history. The feature's commits are still ancestors of
main, so Git considers them merged. If the team fixes the feature and merges the branch again, Git only brings in the commits made after the first merge. If there are none, it says "Already up to date", and the original work stays reverted.To re-land it, revert the revert first, which restores the original changes, then merge the new fixes. Alternatively rebuild the feature as fresh commits, for example by rebasing it onto current
mainso it gets new SHAs.This is why many teams prefer squash merges or feature flags: undoing a feature is simpler.
git revert -m 1 3c06b58 # keep parent 1 (main), undo the branch's changes git merge feature # Already up to date. (Git thinks it's merged) git revert 62bff45 # revert the revert to bring the feature backWhat interviewers listen for-m 1picks the mainline parent- Revert undoes changes, not history
- Re-merging brings only newer commits
- Revert the revert to re-land the feature
46.How would you protect the
mainbranch on GitHub or GitLab, and why not rely on local hooks?midLocal hooks can be skipped with
--no-verifyor simply never installed, so rules that matter have to be enforced on the server. On GitHub that's branch protection rules or rulesets; on GitLab, protected branches and merge request approval rules.Typical settings for
main:- Require a pull request before merging, with at least one or two approvals, and dismiss stale approvals when new commits are pushed.
- Require review from CODEOWNERS for sensitive paths.
- Require status checks to pass, such as build, tests and security scans, and optionally require the branch to be up to date with
main, or use a merge queue so each change is tested against the latestmain. - Block force pushes and deletion, optionally require linear history or signed commits, and require conversations to be resolved.
- Apply the rules to administrators too, with a documented break-glass path.
Together these make "main is always green and reviewed" a guarantee instead of a hope.
What interviewers listen for- Client hooks are bypassable; enforce server-side
- Required PRs, approvals and CODEOWNERS review
- Required status checks or a merge queue
- Block force pushes and branch deletion
- Apply to admins too
Likely follow-up: What problem does a merge queue solve?
47.Why are large binary files a problem in Git, and how does Git LFS help?mid
Git keeps every version of every file forever, and every clone downloads the full history. Binaries such as images, videos, datasets or compiled builds don't diff or compress well, so each change adds roughly the full file size. The repository gets permanently slower to clone and fetch, and GitHub warns above 50 MiB and blocks files over 100 MiB.
Git LFS (Large File Storage) replaces large files in the repository with small pointer files containing the object's SHA-256 and size. The real content lives on an LFS server and is downloaded only for the commits you check out. You choose files by pattern with
git lfs track, which writes rules to.gitattributes; commit that file.Things to know: files already committed stay in history until you rewrite it with
git lfs migrate import; hosts have LFS storage and bandwidth quotas; CI checkouts must fetch LFS content (for examplelfs: trueonactions/checkout); andgit lfs lockhelps with files that can't be merged. Build outputs belong in an artifact store, not in Git at all.What interviewers listen for- Every version of every file stays in history
- LFS commits small pointer files instead
- Content fetched from the LFS server on checkout
git lfs trackwrites.gitattributes- Existing history needs
git lfs migrate import
48.Someone pushed an API key to a public repository. What do you do?hard
First, revoke or rotate the key immediately. Assume it's compromised: automated scanners pick up secrets on public repositories within minutes, and removing the commit doesn't un-leak it. Check the provider's logs for misuse.
Only then clean up history, if it's worth it:
- A new commit deleting the file isn't enough; the key is still in history.
- Rewrite history with
git filter-repo, which Git's own documentation recommends overgit filter-branch, or a tool like BFG: remove the file with--invert-paths --path, or replace the string with--replace-text. - Force-push every rewritten branch and tag, and have collaborators re-clone. An old clone that pushes again reintroduces the secret.
- Forks and hosting-side caches, such as old pull request refs and views, can keep the data; on GitHub you contact Support to purge them.
Then prevent a repeat:
.gitignorethe file, load secrets from environment or a vault, and enable secret scanning with push protection or a pre-commit scanner.# 1. Revoke/rotate the key FIRST. Then, in a fresh clone: git filter-repo --sensitive-data-removal --invert-paths --path config/keys.json # or replace the value everywhere: git filter-repo --sensitive-data-removal --replace-text ../expressions.txt git push --force --mirror originWhat interviewers listen for- Rotate the secret first; assume it's compromised
- A deleting commit leaves it in history
- Rewrite with
git filter-repo, force push all refs - Collaborators must re-clone; forks keep copies
- Prevent: secret scanning, push protection, vaults
49.Monorepo or polyrepo? What are the trade-offs, and how do you keep a large monorepo fast?hard
Monorepo: many projects in one repository.
- Pros: atomic changes across services and shared libraries in one PR, one version of each dependency, shared tooling and CI, easy large-scale refactoring and code discovery.
- Cons: Git and CI must scale. Clones and
git statusget slow, every PR could trigger every build, and access control is coarse.
Polyrepo: one repository per service or library.
- Pros: clear ownership, independent versioning and release cadence, fine-grained permissions, small fast repos.
- Cons: cross-cutting changes need coordinated PRs and version bumps across repos, dependencies drift, and tooling gets duplicated.
Keeping a monorepo fast: partial clone (
--filter=blob:none) and sparse checkout so developers only materialise what they work on; build tools that understand the dependency graph and run only affected builds and tests (Bazel, Nx, Turborepo), or CI path filters; and CODEOWNERS for ownership.The deciding factor is coupling: tightly coupled code that changes together favours a monorepo.
What interviewers listen for- Monorepo: atomic cross-project changes, shared tooling
- Polyrepo: independent releases, clear ownership
- Monorepo costs: scale, CI load, coarse access
- Partial clone and sparse checkout for speed
- Build only affected projects in CI
Likely follow-up: How would you version shared libraries in a polyrepo setup?
50.How do Git submodules differ from subtrees? When would you use each?hard
A submodule embeds another repository as a pointer: the parent records a special tree entry (a gitlink) with the exact commit SHA of the child, plus its URL in
.gitmodules. The child's files aren't stored in the parent. That gives exact pinning and independent history, but it's easy to get wrong: a plain clone leaves the directory empty until you rungit submodule update --init --recursive(or clone with--recurse-submodules), the submodule sits in detached HEAD, and forgetting to push the child's commit breaks everyone's checkout.A subtree copies the other project's files, optionally with its history squashed, into a subdirectory as ordinary commits. Clones just work and contributors need no extra commands;
git subtree pullbrings in upstream updates andgit subtree pushcan send changes back. The cost is a bigger repo and manual syncing.git subtreeships in Git's contrib area, so check it's installed.I'd use submodules for a large external dependency with its own release cycle, and subtrees when vendoring something small that consumers shouldn't have to think about. Often a package manager beats both.
What interviewers listen for- Submodule = pinned commit pointer plus
.gitmodules - Submodules need
--recurse-submodulesorupdate --init - Subtree = files copied in as normal commits
- Subtree: simple for consumers, manual syncing
- A package manager is often better than both
- Submodule = pinned commit pointer plus
51.What is infrastructure as code, and what is GitOps? How do they differ from a push-based CI deployment?hard
Infrastructure as code (IaC) means defining infrastructure, such as networks, clusters, databases and permissions, in declarative files (Terraform or OpenTofu, CloudFormation, Pulumi) kept in Git. Changes go through pull requests like application code: CI runs a
planshowing exactly what will change, a reviewer approves, andapplymakes it so. You get reproducible environments, an audit trail and drift detection, but you must manage the tool's state carefully, with remote storage and locking.GitOps applies the same idea to deployments. Git holds the desired state, for example Kubernetes manifests or Helm values, and an agent running in the cluster, such as Argo CD or Flux, continuously pulls that state and reconciles the live system to match it. Deploying is merging a PR that changes an image tag; rolling back is a
git revert; manual changes in the cluster are detected as drift and reverted.Compared with push-based CI, where the pipeline runs
kubectl applywith cluster credentials, the pull model keeps credentials inside the cluster and makes Git the single source of truth.What interviewers listen for- IaC: infrastructure as declarative, versioned code
- Plan in CI, review, then apply
- GitOps: Git holds desired state
- Agent (Argo CD, Flux) pulls and reconciles
- Rollback is a git revert; drift auto-corrected
52.Why would you sign commits, and how does it work?hard
Commit author details are just text: anyone can set
user.nameanduser.emailto your name and push a commit that looks like yours. A signature proves the commit was created by someone holding a specific private key and that it hasn't been altered since.Git supports GPG, SSH keys (in recent versions, the easiest option since most developers already have one) and X.509 certificates. You set
gpg.format, pointuser.signingKeyat the key, and sign withgit commit -Sor always viacommit.gpgSign;git tag -ssigns tags. The signature is stored inside the commit object, so it travels with it.To verify,
git log --show-signatureorgit verify-commit; for SSH, Git needs an allowed-signers file mapping emails to keys. Hosts like GitHub and GitLab show a Verified badge when the key is registered to the account, and branch rules can require signed commits.Signing proves authorship, not that the code is good. Rewriting history, such as a rebase, re-creates commits, so they must be signed again.
git config gpg.format ssh git config user.signingKey ~/.ssh/id_ed25519.pub git config commit.gpgSign true # sign every commit git commit -m "Harden auth" # or: git commit -S git log --show-signature -1 git verify-commit HEAD # SSH needs gpg.ssh.allowedSignersFileWhat interviewers listen for- Author metadata is trivially spoofable
- Signature ties a commit to a private key
- GPG, SSH or X.509 via
gpg.format commit.gpgSign,git commit -S,git tag -s- Host shows Verified; can require signed commits
53.How does Git actually perform a merge? Explain the merge base, three-way merge, and
-X theirsversus-s ours.hardGit finds the merge base, the best common ancestor of the two tips (
git merge-base), and does a three-way merge: it compares base with ours and base with theirs. For each region:- changed on only one side: take that change;
- changed identically on both: take it once;
- changed differently on both: conflict.
That's why merges are smarter than comparing two files directly: the base shows who changed what. The default strategy, ort, also detects renames. When there are several merge bases (criss-cross history), it first merges them into a virtual base. The older
recursivestrategy is now just an alias forort.Options are easy to confuse:
-X ours/-X theirsis a strategy option: a normal merge, but conflicting hunks are resolved automatically in favour of one side. Non-conflicting changes from both sides still come in.-s oursis a whole strategy: it records a merge commit but ignores the other branch's content entirely, which is useful for marking an old branch as merged.
What interviewers listen for- Merge base = best common ancestor
- Three-way: compare each side against the base
- Only both-sides-changed regions conflict
ortis the default; detects renames-X theirsresolves conflicts;-s oursdiscards their changes
54.What does
git rebase --ontodo? Give a scenario where you need it.hardA normal
git rebase mainreplays every commit on your branch that isn't inmain.git rebase --onto <newbase> <upstream> <branch>is more precise: take the commits inupstream..branchand replay only those ontonewbase.The classic scenario: you branched
featBofffeatAbecause you needed its code. ThenfeatAwas squash-merged intomain, so its original commits never appear inmain. A plaingit rebase mainfromfeatBwould try to replayfeatA's old commits too, causing duplicate changes and conflicts.git rebase --onto main featA featBreplays onlyfeatB's own commits ontomain.It's also a way to drop a range:
git rebase --onto HEAD~3 HEAD~1removes the two commits in between.For stacks of dependent branches, recent Git offers
--update-refs, which moves every branch in the stack to its rewritten commit in one rebase. As with any rebase, it rewrites SHAs, so only do it on unshared branches.# featB was branched from featA; featA got squash-merged into main git rebase --onto main featA featB # replay only commits in featA..featB onto main # stacked branches: move part1 and part2 together git switch part2 git rebase --update-refs main # part1 is updated in the same runWhat interviewers listen for- Replays only
upstream..branchonto a new base - Fixes branches built on a squash-merged branch
- Can drop a range of commits
--update-refsmoves stacked branches together
- Replays only
55.What are the main security risks in GitHub Actions workflows, and how do you mitigate them?hard
- Mutable action references. A tag like
@v4can be moved to malicious code, and there have been real supply-chain incidents where that happened. Pin third-party actions to a full commit SHA and let Dependabot or Renovate update them. - Script injection.
${{ }}expressions are substituted into the script before it runs, so an attacker-controlled PR title or branch name can inject shell commands. Pass untrusted values throughenvand quote them. - Privileged triggers.
pull_request_targetruns in the context of the base repository, with access to its secrets and potentially a write-capable token. Never check out and execute the PR's code in it. - Over-privileged tokens. Set
permissionsto read-only by default and grant more per job. - Long-lived secrets. Prefer OIDC for cloud access; gate production secrets behind environments with reviewers.
- Self-hosted runners can keep state between jobs, so don't use them for public repositories where anyone can open a PR.
- uses: actions/checkout@<full-40-char-commit-sha> # pin, don't trust tags - name: Check PR title env: TITLE: ${{ github.event.pull_request.title }} # data, not code run: echo "$TITLE" | grep -qE '^(feat|fix)' # unsafe: run: echo "${{ github.event.pull_request.title }}"What interviewers listen for- Pin actions to full commit SHAs
- Pass untrusted input via env to avoid injection
- Never run PR code under
pull_request_target - Read-only
permissionsby default - OIDC over long-lived secrets; careful with self-hosted runners
- Mutable action references. A tag like
56.Your CI pipeline takes 45 minutes. How would you make it faster?hard
First measure: find which jobs and steps take the time, and whether it's queueing, setup or the tests themselves. Then, typically:
- Parallelise: split independent jobs, and shard the test suite across several runners.
- Cache: dependencies, Docker layers and build outputs; a remote build cache helps a lot in monorepos.
- Do less: run only what's affected by the change (path filters, affected-project detection, test impact analysis), and build once, reusing the artifact in later jobs instead of rebuilding.
- Order for fast feedback: lint and unit tests first, so most failures surface in minutes. Move the slowest end-to-end suites after merge or to a schedule, keeping a smoke test on PRs.
- Cancel superseded runs with
concurrencyandcancel-in-progress. - Optimise Docker builds: multi-stage builds, dependency layers before source, a
.dockerignore. - Fix flaky tests; reruns quietly double your pipeline time.
- Use bigger or self-hosted runners when you're CPU-bound.
A common target is PR feedback in about ten minutes, because slow pipelines push people towards bigger, riskier batches.
on: pull_request: paths: ['services/api/**', 'libs/**'] # skip unrelated changes concurrency: group: ci-${{ github.ref }} cancel-in-progress: true # drop superseded runsWhat interviewers listen for- Measure first: where does the time go?
- Parallelise and shard tests
- Cache dependencies and build outputs
- Run only affected work; build once
- Fast checks first; cancel superseded runs
57.What is
git worktree, and when is it better than stashing or cloning the repository twice?hardgit worktreeattaches additional working directories to the same repository.git worktree add ../app-hotfix -b hotfix origin/maincreates a new folder with its own checked-out branch, its ownHEADand its own index, while sharing the object database, refs and configuration with the main working tree.It shines when you need two branches at once:
- an urgent hotfix while you're mid-feature, without stashing half-finished work or losing your build state;
- running a long test suite or build on one branch while you keep coding on another;
- checking out a colleague's PR to review it side by side.
Compared with a second clone, it's cheaper: no duplicate objects, one
fetchupdates both, and branches and stashes are visible everywhere. The main constraint is that the same branch can't be checked out in two worktrees at the same time; Git refuses with "already used by worktree". Clean up withgit worktree remove, andgit worktree pruneremoves records of folders you deleted manually.What interviewers listen for- Extra working directories for one repository
- Each has its own HEAD and index; objects shared
- Great for hotfixes and parallel builds
- Same branch cannot be in two worktrees
git worktree list,remove,prune
58.If every commit is a full snapshot, why doesn't a Git repository grow enormous? Explain loose objects and packfiles.hard
Conceptually each commit is a snapshot, but storage is much smarter:
- Deduplication: objects are named by their content hash, so an unchanged file in a new commit points to the same blob as before. A commit that changes one file adds one blob, the trees on the path to it, and the commit object.
- Loose objects: newly created objects are written individually, zlib-compressed, under
.git/objects. - Packfiles:
git gc, which Git also runs automatically from time to time, bundles objects into packfiles with delta compression, and fetches and pushes transfer packfiles too. Similar objects are stored as a base plus small deltas, typically keeping the newest version whole and older versions as deltas, since recent versions are read most.
So the "snapshots, not diffs" model is about Git's data model and speed of checkout; physically it's heavily compressed. You can inspect this with
git count-objects -vandgit verify-pack -v.The exception is binaries: they rarely delta well, which is why large binary files bloat repositories and belong in Git LFS.
What interviewers listen for- Unchanged files reuse the same blob
- Loose objects are zlib-compressed files
- gc and transfers create delta-compressed packfiles
- Snapshot data model, compressed storage
- Binaries delta poorly: use LFS
No questions match that filter.
Prefer multiple choice? All 23 Git & CI/CD MCQs with answers →