Ch. 16

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.

58 interview questions23 quiz questions0 notes
your progress0%

Top 58 Git & CI/CD interview questions most asked first

  1. 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. 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 add copies the current content of a file into it.
    • Repository: the .git directory, holding every commit, tree and blob plus the refs that point at them. git commit turns 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 diff shows working tree vs index, and git diff --staged shows index vs the last commit.

    What interviewers listen for
    • Working tree is what you edit
    • Index is the snapshot of the next commit
    • .git stores commits, trees, blobs and refs
    • Staging lets you craft focused commits (git add -p)

    Likely follow-up: What does git add -p let you do? · How do you unstage a file without losing the change?

  3. 3.What is the difference between git merge and git rebase, and when would you use each?mid

    Both 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 main to 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 possible
    What 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-ff do and why would a team want it? · What happens to conflicts during a rebase?

  4. 4.What is the difference between git fetch and git pull? What does git pull --rebase do?easy

    git fetch downloads new commits from a remote and updates your remote-tracking branches such as origin/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 pull is 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 set pull.rebase or pull.ff.

    git pull --rebase replays 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 status saying "up to date" only reflects your last fetch.

    What interviewers listen for
    • Fetch updates remote-tracking branches only
    • Pull = fetch + merge (or rebase)
    • --rebase replays local commits, no merge commit
    • Divergent pull without config errors in modern Git

    Likely follow-up: What does pull.ff only do? · Why might git status say you are up to date when you are not?

  5. 5.Explain git reset --soft, --mixed and --hard. How is that different from git revert?mid

    git reset moves 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 revert doesn'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 reset on local commits nobody else has; use revert for 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 abc1234
    What 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. 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 status to see the files marked "both modified".
    • Open each file. Between <<<<<<< and ======= is your side (HEAD), and between ======= and >>>>>>> is the incoming side. With merge.conflictStyle set to zdiff3 (or diff3), 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 --ours or --theirs.
    • git add each file to mark it resolved, run the tests, then git commit (or git rebase --continue).

    If it goes wrong, git merge --abort returns 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"
    >>>>>>> feature
    What interviewers listen for
    • git status lists conflicted files
    • Understand both sides, remove markers
    • git add marks resolved, then commit or continue
    • git merge --abort bails 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?

  7. 7.What does git stash do, and what are its gotchas?easy

    git stash saves your uncommitted changes, both staged and unstaged, onto a stack and resets the working tree to HEAD. It's useful when you need a clean tree to switch branches, pull or check something quickly without making a throwaway commit.

    • git stash pop reapplies the latest entry and drops it; git stash apply reapplies but keeps it.
    • git stash list shows entries as stash@{0}, stash@{1} and so on; -m gives them a readable message.

    Gotchas:

    • Untracked files are not stashed by default; use -u (or -a to include ignored files too).
    • If pop hits a conflict, the entry is not dropped; resolve it and git stash drop yourself.
    • 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. 8.What actually is a branch in Git, and what is HEAD?easy

    A 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 (or HEAD~) is the first parent of HEAD, HEAD~3 goes three generations back along first parents, and HEAD^2 is 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, ^2 is a merge's second parent

    Likely follow-up: What happens to commits made in detached HEAD state?

  9. 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. 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 --amend replaces the last commit with a new one built from the current index. Stage the forgotten file, then amend; --no-edit keeps the message, or -m rewrites 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 like main don't rewrite history: make a follow-up commit or a git revert instead.

    What interviewers listen for
    • --amend replaces the last commit
    • Amended commit gets a new SHA
    • reset --soft HEAD~1 undoes 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. 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 to HEAD; git reset --hard does the same. Plain git restore . only discards unstaged changes.
    • Untracked files Git has never recorded. Neither command touches them. git clean deletes them: -f is required to actually delete, -d includes directories, and -x also removes ignored files, which may include your .env or 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 -u is 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 for
    • restore --staged --worktree . or reset --hard for tracked files
    • git clean -fd removes untracked files and dirs
    • -x also deletes ignored files; beware .env
    • Dry-run with git clean -n first
    • git stash -u if you might need it later
  12. 12.What does git cherry-pick do, and when is it the right tool?mid

    git cherry-pick takes 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 main to a release branch.
    • Rescuing one useful commit from a branch you're otherwise abandoning.

    Useful options: -x appends "(cherry picked from commit ...)" to the message, which is great for traceability on release branches; -n applies without committing; a range A..B picks everything after A up to B; -m 1 is needed to pick a merge commit. Conflicts are resolved like a merge, then git 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
    • -x records 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?

  13. 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) and develop (integration), plus feature/*, release/* and hotfix/* 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. 14.You added .env to .gitignore, but Git still tracks it. Why, and how do you fix it?easy

    .gitignore only applies to untracked files. It stops new files from being picked up by git add and git 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 (-r for 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 path tells you which rule ignores a file. For personal ignores that shouldn't be committed, use .git/info/exclude or 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  .env
    What interviewers listen for
    • .gitignore only affects untracked files
    • git rm --cached untracks but keeps the file
    • Pulling that commit deletes it for teammates
    • Still in history: rotate leaked secrets
    • git check-ignore -v explains which rule matched

    Likely follow-up: How would you remove a leaked secret from the whole history?

  15. 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 feature stages 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 that feature was 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 single git revert. Messy "fix typo" commits disappear.

    Cons: the individual commits are lost from main (less useful for bisect and blame on large PRs), and because Git can't see the branch as merged, git branch -d refuses 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. 16.You ran git reset --hard and lost commits. How do you get them back?mid

    Use the reflog. Every time HEAD or 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 with git reset --hard HEAD@{1}. ORIG_HEAD also 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 gc can prune the objects.
    • Uncommitted changes discarded by reset --hard were 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 back
    What 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. 17.What is "detached HEAD" state, how do you get into it, and what happens to commits you make there?mid

    Normally HEAD points to a branch, and the branch points to a commit. In detached HEAD state, HEAD points 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 in git reflog and run git 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. 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. 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. 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 as push, pull_request, workflow_dispatch (manual button), schedule (cron, UTC by default), release or workflow_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. needs creates 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 uses an action (reusable code like actions/checkout) with inputs in with, or runs shell commands.

    Other common keys: env, permissions for the GITHUB_TOKEN, strategy.matrix, environment and concurrency.

    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 test
    What interviewers listen for
    • Workflows live in .github/workflows/
    • on defines triggers: push, pull_request, schedule...
    • Jobs run in parallel; needs orders them
    • Each job gets a runner; hosted runners are fresh VMs
    • Steps uses actions or run commands

    Likely follow-up: How do two jobs share a build output? · How would you stop two deployments running at once?

  21. 21.You rebased a branch that is already pushed. How do you publish it safely, and why prefer --force-with-lease over --force?mid

    After 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.

    --force overwrites 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-lease only 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-includes adds a check that the remote tip is actually in your local branch's history.

    Team rules: only force-push your own feature branches, protect main so 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
    • --force can silently delete others' commits
    • Lease: only overwrite if remote is what you last fetched
    • Background fetch defeats the lease; --force-if-includes helps
    • 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. 22.How do you clean up a messy feature branch before opening a pull request?mid

    With an interactive rebase: git rebase -i main (or HEAD~5) opens a todo list with one line per commit, oldest first. You edit the list and Git replays it:

    • pick keeps a commit, reword changes its message, edit stops so you can amend or split it.
    • squash melds a commit into the previous one and combines messages; fixup does the same but discards its message.
    • drop (or deleting the line) removes a commit; reordering lines reorders commits.
    • exec runs a command, for example the tests, after a commit.

    A nice workflow is to make corrections as git commit --fixup=<sha> while you work, then git rebase -i --autosquash main moves each fixup under its target automatically.

    If it goes wrong, git rebase --abort restores 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 logging
    What interviewers listen for
    • git rebase -i edits a todo list of commits
    • pick, reword, edit, squash, fixup, drop, exec
    • --fixup plus --autosquash automates squashing
    • --abort or reflog to undo
    • Only on unshared branches

    Likely follow-up: How would you split one commit into two?

  23. 23.What is the difference between a lightweight and an annotated tag, and how do tags relate to releases?easy

    A tag is a named ref to a specific commit that, unlike a branch, isn't supposed to move.

    • A lightweight tag is just a ref: a name that stores a commit SHA, nothing more.
    • An annotated tag (git tag -a) is a real object in the database with the tagger's name, a date and a message, and it can be signed (git tag -s). Use annotated tags for releases; tools treat them as the official ones. For example git describe uses only annotated tags unless you pass --tags, and git push --follow-tags pushes only annotated tags.

    Tags aren't pushed with git push by default: push one explicitly or use --follow-tags.

    A release on GitHub or GitLab is a platform feature built on top of a tag: release notes, a changelog and downloadable build assets. Treat published tags as immutable; moving a tag others have fetched causes confusion.

    What interviewers listen for
    • Lightweight tag = plain ref to a commit
    • Annotated tag = object with tagger, date, message
    • Use annotated (or signed) tags for releases
    • Tags must be pushed explicitly
    • Platform releases add notes and assets to a tag

    Likely follow-up: How do you delete a tag locally and on the remote?

  24. 24.What are remotes, remote-tracking branches and upstream branches? How do origin and upstream differ?easy

    A remote is a named URL of another copy of the repository. git clone creates one called origin for the place you cloned from. In a fork workflow, people conventionally add upstream for the original project, so they pull from upstream and push to their fork, origin. The names are only conventions.

    A remote-tracking branch like origin/main is your local, read-only snapshot of where that branch was on the remote at your last fetch. You don't commit to it; git fetch updates it.

    A local branch can have an upstream (tracking) branch configured. That's what makes plain git pull and git push know where to go and lets git status report "ahead 2, behind 1". Set it with git push -u origin feature or git branch --set-upstream-to=origin/feature.

    git fetch --prune removes tracking refs for branches deleted on the remote.

    What interviewers listen for
    • Remote = named URL; origin created by clone
    • upstream conventionally the original repo of a fork
    • origin/main is a cached snapshot from last fetch
    • Upstream config powers pull, push and ahead/behind
    • push -u sets the upstream
  25. 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 clone copies 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 upstream and rebasing or merging its main, 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
    • origin is your fork, upstream the original
    • PR goes from fork branch to upstream
  26. 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. 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. 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 CHANGE note 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
  29. 29.What do git diff, git diff --staged, git diff HEAD and git diff main...feature each show?easy

    They 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 against HEAD, exactly what the next commit will contain.
    • git diff HEAD: working tree against the last commit, staged and unstaged together.
    • git diff main feature (or main..feature): the two tips directly. If main moved on, its new commits show up as reversed changes.
    • git diff main...feature: feature against the merge base of the two, which is only what the feature branch changed. That's what a pull request shows.

    Handy flags: --stat or --name-only for a summary, --word-diff for prose, -w to ignore whitespace.

    Careful: in git log, the dots mean something different. main..feature lists commits on feature not on main, 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 counts
    What interviewers listen for
    • Plain diff: unstaged changes only
    • --staged: what will be committed
    • diff HEAD: all local changes
    • Three dots: changes since the merge base, like a PR
    • Dots mean different things in log vs diff
  30. 30.Why were git switch and git restore added when git checkout already existed?easy

    git checkout does 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 -- file silently throws away your uncommitted changes, and git checkout name behaves differently depending on whether name is a branch or a file.

    Git 2.23 split it into two focused commands:

    • git switch only changes branches: -c creates one, - returns to the previous branch, --detach checks out a commit.
    • git restore only restores files: by default the working tree from the index, --staged to unstage, --source=<commit> to take a file from another commit.

    checkout still works and you'll see it everywhere in older docs and scripts, but switch and restore make 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 version
    What interviewers listen for
    • checkout mixed branch switching and file restoring
    • switch handles branches only
    • restore handles files: worktree or --staged
    • restore discards changes, so use it deliberately
    • checkout still works; new commands are clearer
  31. 31.How do you rename or delete a branch, locally and on the remote?easy

    Renaming: git branch -m new-name renames the current branch, or git branch -m old new renames another one. Git has no "rename" on a remote, so you push the branch under the new name with git 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 feature only 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 in main. git branch -D forces it; the commits are then reachable only through the reflog.

    Deleting on the remote: git push origin --delete feature. Other clones keep a stale origin/feature until they run git 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 for
    • git branch -m renames
    • Remote rename = push new name, delete old
    • -d requires merged; -D forces
    • git push origin --delete removes a remote branch
    • git fetch --prune clears stale tracking refs
  32. 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 it good or bad, 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 skip handles untestable commits manually, and git bisect reset returns 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 started
    What 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 skip
    • bisect reset when finished
    • Works best with small, buildable commits

    Likely follow-up: How does bisect handle merge commits and branches?

  33. 33.How do you find out who last changed a line and why? How do you stop a mass reformat from polluting git blame?easy

    git blame file annotates every line with the commit, author and date that last changed it. -L narrows it to a line range, -w ignores whitespace-only changes, and -M / -C detect 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-file or set blame.ignoreRevsFile so blame looks past them. GitHub's blame view honours a .git-blame-ignore-revs file in the repository root.

    When a line has been rewritten many times, git log -L shows the history of a line range, and git log -S finds commits that added or removed a string.

    What interviewers listen for
    • Blame shows last commit, author, date per line
    • -L range, -w whitespace, -M/-C moves
    • Read the commit and PR to learn why
    • Ignore formatting commits via .git-blame-ignore-revs
  34. 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 revert the bad commit on main so 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 revert so 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. 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. 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_TOKEN permissions to 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_target should 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. 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.os and matrix.node. include adds or extends combinations, exclude removes some, and max-parallel limits concurrency. fail-fast defaults 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 ~/.npm between runs. The key usually includes the OS and a hash of the lockfile, so a dependency change produces a new cache. On an exact key miss, restore-keys are 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 as actions/setup-node offer this built in via cache: 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-keys give prefix fallbacks
    • Branch-scoped; unused caches evicted after 7 days

    Likely follow-up: What is the difference between a cache and an artifact?

  38. 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. 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 below 1.0.0 mean initial development, where anything may change. Pre-releases look like 2.0.0-rc.1 and sort before 2.0.0; build metadata after a + is ignored for precedence.

    Conventional Commits is a commit message convention: type(scope): description. feat maps to a MINOR bump, fix to a PATCH, and a breaking change, marked by ! before the colon or a BREAKING CHANGE: footer, maps to MAJOR. Other types such as docs, refactor, test, ci and chore are 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-msg hook 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 -> MAJOR
    What interviewers listen for
    • SemVer: MAJOR breaking, MINOR feature, PATCH fix
    • type(scope): description message format
    • feat minor, fix patch, ! or footer major
    • Enables automated changelogs and version bumps

    Likely follow-up: What does a 0.x version tell consumers?

  40. 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-checkout and post-merge. Server-side hooks such as pre-receive run on the server during a push. For the pre- 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 .githooks and point core.hooksPath at 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-verify skips pre-commit and commit-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 commit
    What interviewers listen for
    • Scripts run at points like pre-commit, commit-msg, pre-push
    • Non-zero exit aborts the operation
    • .git/hooks is not versioned or cloned
    • Share via core.hooksPath or a hook manager
    • --no-verify bypasses them; CI must enforce
  41. 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. 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. 43.How would you add a manual approval gate before production deployments in GitHub Actions?hard

    Use an environment. In the repository settings, create production and add deployment protection rules, then reference it from the job with environment: 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, main or v* 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 concurrency so two production deploys never overlap, and needs so 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: manual and 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 secret
    What interviewers listen for
    • Job references environment: production
    • Protection rules: reviewers, wait timer, branch rules
    • Environment secrets released only after approval
    • concurrency prevents overlapping deploys
    • needs chains staging before production
  44. 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
  45. 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 revert needs 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 main so 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 back
    What interviewers listen for
    • -m 1 picks the mainline parent
    • Revert undoes changes, not history
    • Re-merging brings only newer commits
    • Revert the revert to re-land the feature
  46. 46.How would you protect the main branch on GitHub or GitLab, and why not rely on local hooks?mid

    Local hooks can be skipped with --no-verify or 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 latest main.
    • 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. 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 example lfs: true on actions/checkout); and git lfs lock helps 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 track writes .gitattributes
    • Existing history needs git lfs migrate import
  48. 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 over git 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: .gitignore the 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 origin
    What 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. 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 status get 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. 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 run git 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 pull brings in upstream updates and git subtree push can send changes back. The cost is a bigger repo and manual syncing. git subtree ships 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-submodules or update --init
    • Subtree = files copied in as normal commits
    • Subtree: simple for consumers, manual syncing
    • A package manager is often better than both
  51. 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 plan showing exactly what will change, a reviewer approves, and apply makes 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 apply with 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. 52.Why would you sign commits, and how does it work?hard

    Commit author details are just text: anyone can set user.name and user.email to 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, point user.signingKey at the key, and sign with git commit -S or always via commit.gpgSign; git tag -s signs tags. The signature is stored inside the commit object, so it travels with it.

    To verify, git log --show-signature or git 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.allowedSignersFile
    What 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. 53.How does Git actually perform a merge? Explain the merge base, three-way merge, and -X theirs versus -s ours.hard

    Git 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 recursive strategy is now just an alias for ort.

    Options are easy to confuse:

    • -X ours / -X theirs is 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 ours is 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
    • ort is the default; detects renames
    • -X theirs resolves conflicts; -s ours discards their changes
  54. 54.What does git rebase --onto do? Give a scenario where you need it.hard

    A normal git rebase main replays every commit on your branch that isn't in main. git rebase --onto <newbase> <upstream> <branch> is more precise: take the commits in upstream..branch and replay only those onto newbase.

    The classic scenario: you branched featB off featA because you needed its code. Then featA was squash-merged into main, so its original commits never appear in main. A plain git rebase main from featB would try to replay featA's old commits too, causing duplicate changes and conflicts. git rebase --onto main featA featB replays only featB's own commits onto main.

    It's also a way to drop a range: git rebase --onto HEAD~3 HEAD~1 removes 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 run
    What interviewers listen for
    • Replays only upstream..branch onto a new base
    • Fixes branches built on a squash-merged branch
    • Can drop a range of commits
    • --update-refs moves stacked branches together
  55. 55.What are the main security risks in GitHub Actions workflows, and how do you mitigate them?hard
    • Mutable action references. A tag like @v4 can 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 through env and quote them.
    • Privileged triggers. pull_request_target runs 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 permissions to 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 permissions by default
    • OIDC over long-lived secrets; careful with self-hosted runners
  56. 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 concurrency and cancel-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 runs
    What 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. 57.What is git worktree, and when is it better than stashing or cloning the repository twice?hard

    git worktree attaches additional working directories to the same repository. git worktree add ../app-hotfix -b hotfix origin/main creates a new folder with its own checked-out branch, its own HEAD and 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 fetch updates 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 with git worktree remove, and git worktree prune removes 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. 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 -v and git 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

Prefer multiple choice? All 23 Git & CI/CD MCQs with answers →

esc