Git internals and commands you’ll be asked to explain, then how a change gets from a commit to production safely.
Three areas & the object model
Working tree (your files) → git add → index (staging area, the next commit) → git commit → repository (.git: objects and refs).
Four object types, each addressed by the hash of its content (SHA-1 by default; SHA-256 repositories exist):
Object
Holds
blob
a file’s contents (no name, no mode)
tree
a directory: names and modes → blobs and subtrees
commit
one root tree, parent commit(s), author, committer, message
annotated tag
a target object, tagger, date, message (optionally signed)
A commit is a snapshot, not a diff; packfiles delta-compress for storage. Identical content is stored once.
A branch is a movable pointer to a commit (a ref under refs/heads/). HEAD normally points to the current branch; detached HEAD points straight at a commit.
Inspect it yourself: git cat-file -t HEAD prints commit; git cat-file -p HEAD shows its tree, parents and author.
Everyday commands
Task
Command
Start
git init, git clone <url>
What changed
git status -sb; git diff (tree vs index); git diff --staged (index vs HEAD)
git clean -nd (dry run), then -fd; -X removes only ignored files
Two checkouts at once
git worktree add ../hotfix -b hotfix
git switch (branches) and git restore (files) split the two jobs of the old git checkout.
Revisions: HEAD~2 = two first-parent steps back; HEAD^2 = a merge’s second parent; A..B = commits in B not in A; git diff A...B = changes on B since the merge base.
Merge vs rebase vs squash
Merge
Rebase
Squash merge
Result
merge commit with two parents (or a fast-forward)
your commits replayed on top: new SHAs, linear
one new commit on the target
History
true, but noisy
clean and linear, rewritten
one commit per PR; branch commits aren’t ancestors
Conflicts
resolved once
possibly once per replayed commit
once
Shared branches
safe
never on commits others have
safe on the target
Command
git merge feat (--no-ff forces a merge commit)
git rebase main, git rebase -i HEAD~3
git merge --squash feat, then git commit
Golden rule of rebasing: never rebase commits that exist outside your repository and that others may have built on. Rebase your own branch onto the latest main; merge (or squash) into shared branches.
Fast-forward: if the target is an ancestor, the pointer just moves. --ff-only refuses otherwise (“Not possible to fast-forward”).
Interactive rebase verbs: pick, reword, edit, squash (meld, keep message), fixup (meld, drop message), drop. git commit --fixup=<sha> plus git rebase -i --autosquash places fixups automatically.
git rebase --onto main old-base feat moves only the commits after old-base onto main.
git merge --squash stages the changes but doesn’t commit, and records no second parent.
Undoing things
Situation
Command
Notes
Discard unstaged edits
git restore file
restores from the index; edits are gone
Unstage
git restore --staged file
keeps the working-tree changes
Old version of a file
git restore --source=HEAD~2 file
only the working tree
Fix the last commit
git add x && git commit --amend --no-edit
new SHA: don’t amend pushed commits
Undo commit, keep changes staged
git reset --soft HEAD~1
moves the branch only
Undo commit, keep changes unstaged
git reset HEAD~1
--mixed is the default
Throw commits and changes away
git reset --hard HEAD~1
tracked changes lost; untracked files stay
Undo a pushed commit
git revert <sha>
new inverse commit; safe on shared branches
Undo a merge
git revert -m 1 <merge-sha>
-m 1 keeps the first parent (mainline)
Recover “lost” commits
git reflog, then git reset --hard HEAD@{1} or git branch rescue <sha>
reflog is local only
reset, merge and rebase save the previous tip in ORIG_HEAD: git reset --hard ORIG_HEAD undoes the last one.
Reflog entries expire (defaults: 90 days, 30 for unreachable commits); then gc can delete the objects. Uncommitted changes were never in the reflog.
Stash, cherry-pick & conflicts
git stash saves modified tracked files and the index; untracked files stay put unless you add -u (-a adds ignored files too).
git stash push -m "wip" -- path, git stash list, git stash show -p stash@{1}. apply keeps the entry, pop applies and drops it (kept if it conflicts). git stash branch fix creates a branch at the stash’s base and pops it.
git cherry-pick <sha> re-applies one commit’s change as a new commit: backporting a fix to a release branch. -x appends “(cherry picked from commit …)”; -n stages without committing; A..B picks a range (excluding A).
Resolving a conflict:
git status lists unmerged paths (UU); git diff --name-only --diff-filter=U lists just the files.
Edit the markers, or take one side: git checkout --ours file / --theirs file.
git add file marks it resolved.
git merge --continue (or git commit), git rebase --continue, git cherry-pick --continue. Any of them takes --abort to start over.
<<<<<<< HEADconst timeout = 30; // your current branch (ours)=======const timeout = 60; // the branch being merged in (theirs)>>>>>>> feature
JavaScript
During a rebase the sides flip: --ours is the branch you’re rebasing onto, --theirs is your commit being replayed.
git config rerere.enabled true records resolutions and reuses them when the same conflict comes back.
Remotes
git fetch downloads objects and updates remote-tracking refs (origin/main); it never touches your branches or files. git pull = fetch + merge (or rebase) into the current branch.
With diverged branches and no pull.rebase/pull.ff setting, recent Git refuses to pull (“Need to specify how to reconcile divergent branches”). Pick one: git pull --rebase, --no-rebase or --ff-only.
git pull --rebase replays your local commits on top of the fetched ones: no “Merge branch ‘main’ of …” commits.
git push -u origin feat sets the upstream (git branch -vv shows it). git push origin --delete feat deletes a remote branch; git fetch --prune drops stale remote-tracking refs.
--force overwrites whatever is on the remote. --force-with-lease only overwrites if the remote ref still equals your remote-tracking ref (what you last fetched), so it won’t clobber a teammate’s push. A background fetch (IDE) defeats it; add --force-if-includes.
Forks: git remote add upstream <url>, git fetch upstream, git rebase upstream/main. origin is just the default name.
Tags, releases & commits
Lightweight tag: a plain ref to a commit. Annotated (git tag -a v1.2.0 -m "Release 1.2.0"): a tag object with tagger, date and message; use these for releases (-s signs).
Tags aren’t pushed with branches: git push origin v1.2.0, git push --tags (all tags) or git push --follow-tags (annotated tags reachable from what you push).
git describe prints the nearest annotated tag, commits since and the short SHA: v1.0.0-2-ga7e00c9.
SemVerMAJOR.MINOR.PATCH: breaking change / backward-compatible feature / backward-compatible fix. 1.4.0-rc.1 sorts before 1.4.0; build metadata (+build.7) is ignored for precedence; in 0.y.z anything may change.
Conventional Commits: type(scope)!: summary. fix → patch, feat → minor, ! or a BREAKING CHANGE: footer → major. docs, refactor, test, ci, chore, perf, build are common conventions. Tools generate changelogs and version bumps from them.
git commit -m "fix(cart): keep coupon after login" # patchgit commit -m "feat(cart): add gift cards" # minorgit commit -m "feat(api)!: remove v1 order endpoints" \ -m "BREAKING CHANGE: clients must call /v2/orders" # major
Terminal
Bisect, blame & .gitignore
git bisect start <bad> <good>, then mark each checkout git bisect good/bad/skip; a binary search needs about log2(n) steps. git bisect reset returns you to where you started.
git bisect run ./test.sh automates it: exit 0 = good, 125 = skip, other codes from 1 to 127 = bad.
git blame -L 10,20 file (line range), -w (ignore whitespace), -C (follow lines moved from other files); --ignore-revs-file .git-blame-ignore-revs skips mass-reformat commits.
Pickaxe: git log -S 'retryCount' finds commits that add or remove that string; -G <regex> matches changed lines; git log --follow -- file crosses renames.
.gitignore patterns: *.log, build/ (directories), /config.local (root only), **/tmp, !keep.log (negation). A file can’t be re-included if its parent directory is ignored.
Ignore rules apply only to untracked files. Stop tracking with git rm --cached file (the file stays on disk), then commit. git check-ignore -v path names the matching rule.
Personal ignores: .git/info/exclude (this repo) or core.excludesFile (all repos).
Branching strategies
Git Flow
GitHub Flow
Trunk-based
Branches
main, develop, feature/*, release/*, hotfix/*
main + short-lived feature branches
trunk + branches that live hours to a day or two
Release
release branch off develop, merged to main and develop, tagged
merge the PR, deploy main
trunk is always releasable; incomplete work behind feature flags
Fits
versioned, installed software; scheduled releases
web apps deployed continuously
high-frequency CI/CD at any team size
Cost
long-lived branches, big merges, slow feedback
needs solid CI and a deployable main
needs fast CI, strong tests, flag discipline
Protect main: required reviews and status checks, no force pushes, optionally linear history or signed commits.
CI/CD & pipeline stages
Term
Means
Continuous integration
everyone merges small changes to main often; every change is built and tested automatically
Continuous delivery
every change that passes is releasable; the production deploy is a manual decision
Continuous deployment
every change that passes goes to production automatically, no human gate
Typical pipeline, cheapest and fastest checks first:
Lint, format check, type-check; unit tests with coverage.
Build the artifact or image once, tagged with the commit SHA or version.
Security: SAST, dependency (SCA) and secret scanning, image scan.
Integration and end-to-end tests against the built artifact.
Publish to a registry; deploy to staging; smoke tests.
Approval gate (delivery) or automatic promotion (deployment); progressive rollout to production; monitor and roll back automatically on bad health signals.
Build once, promote the same artifact through environments; only configuration and secrets differ.
Keep pipelines fast (minutes, not hours) and deterministic: pinned versions, no shared mutable state, flaky tests quarantined and fixed.
deploy: # a second job under jobs: needs: test if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest environment: production # reviewers, wait timer, branch rules concurrency: { group: production, cancel-in-progress: false } permissions: { contents: read, id-token: write } # OIDC instead of stored cloud keys steps: - uses: actions/checkout@v6 - run: ./deploy.sh env: { API_TOKEN: "${{ secrets.API_TOKEN }}" }
yaml
Triggers: push, pull_request, workflow_dispatch (manual, with inputs), schedule (cron, in UTC), release, workflow_call (reusable workflows); filter with branches, tags, paths.
Jobs run in parallel on fresh runners unless linked with needs. Steps in a job share a filesystem; jobs share data through artifacts (actions/upload-artifact) or outputs.
A matrix expands to one job per combination; include/exclude adjust it, and fail-fast (on by default) cancels the rest when one fails.
Cache (cache: on setup-* actions, or actions/cache keyed on hashFiles('**/package-lock.json')) speeds up dependencies. Artifacts are build outputs you keep or pass on.
Secrets: ${{ secrets.NAME }}, masked in logs; not passed to workflows triggered from forks (except GITHUB_TOKEN); can’t be used directly in if:. A job can’t read environment secrets until its required reviewers approve.
GITHUB_TOKEN is generated per job: scope it down with permissions:. pull_request_target runs with the base repo’s secrets, so never check out and execute a fork’s code in it.
Pin third-party actions to a full commit SHA; set timeout-minutes on jobs.
Deployments & rollbacks
Strategy
How
Rollback
Trade-off
Recreate
stop old, start new
redeploy the old version
downtime; simplest
Rolling
replace instances in batches
roll back batch by batch
no extra capacity; two versions live at once
Blue-green
deploy to an idle copy, switch traffic at the load balancer
switch back instantly
double the infrastructure during the switch
Canary
send 1% → 10% → 50% → 100% to the new version, watch metrics
shift traffic back
needs traffic splitting and good observability
Feature flags
ship code dark, turn it on per user or cohort
turn the flag off
decouples deploy from release; flag debt
Roll back (redeploy the previous immutable artifact) when the fix isn’t obvious; roll forward (ship a fix) when it’s small and the pipeline is fast.
Rollback-safe database changes use expand/contract: add the new column, write both, backfill, switch reads, and drop the old column in a later release. Old and new code must both work against the schema.
In Git, git revert the bad change on main so the next build is known-good; don’t rewrite shared history.
Automate it: health checks and SLO-based canary analysis that halt and revert without a human.
DORA metrics & secrets hygiene
Metric
Measures
Type
Deployment frequency
how often you deploy to production
throughput
Change lead time
commit → running in production
throughput
Failed deployment recovery time
time to recover from a failed deployment (formerly MTTR)
throughput
Change fail rate
share of deployments needing immediate intervention
instability
Deployment rework rate
share of deployments that are unplanned, caused by a production incident
instability
The classic “four keys” are the first four; DORA’s research finds speed and stability go together rather than trading off.
Secrets: never commit them; keep .env in .gitignore; scan in pre-commit hooks and on the server (secret scanning, push protection).
A committed secret is compromised: rotate it first, then purge history (git filter-repo) and force-push; clones and forks keep copies.
Store secrets in the CI secret store or a vault, scope them per environment with least privilege, and inject them at run time, never into images or artifacts.
Prefer OIDC federation (short-lived cloud credentials) over long-lived access keys. Masking misses transformed values (base64, JSON), so don’t print secrets.
Quick answers
fetch vs pull?fetch only updates remote-tracking refs; pull also merges or rebases into your branch.
merge vs rebase? Merge preserves history with a merge commit; rebase rewrites your commits onto a new base for a linear history.
reset vs revert?reset moves the branch pointer (rewrites history); revert adds a new commit that undoes one.
What is HEAD? A pointer to the current branch (or a commit, when detached).
Detached HEAD? You checked out a commit or tag; new commits there need git switch -c name or they end up unreferenced.
Undo the last commit but keep the work?git reset --soft HEAD~1.
Recover a deleted branch? Find its tip in git reflog, then git branch name <sha>.
Fast-forward merge? The target is an ancestor, so the branch pointer just moves; no merge commit.
Why is .gitignore ignored? The file is already tracked: git rm --cached it.
--force vs --force-with-lease? The lease refuses if the remote moved since your last fetch.
Find the commit that broke it?git bisect, ideally git bisect run with a test script.
Delivery vs deployment? Both keep main releasable; deployment also pushes to production automatically.
Blue-green vs canary? Blue-green switches all traffic at once with an instant switch back; canary shifts traffic gradually while watching metrics.
Why feature flags? Merge unfinished work to trunk safely and release it independently of deploys.
Gotchas & traps
git commit -am skips untracked files; new files still need git add.
reset --hard and restore discard uncommitted changes: the reflog only knows commits (content you had staged may survive as a dangling blob, found with git fsck --lost-found).
Amending or rebasing pushed commits forces everyone else to reconcile; you’ll need a force push.
A squash-merged branch looks “not fully merged” to git branch -d; delete it with -D.
After reverting a merge, merging the same branch again brings nothing back: revert the revert first.
git stash leaves untracked files behind unless you pass -u.
git push --tags also pushes stray local tags; --follow-tags only pushes annotated ones.
Renaming only the case of a file (File.js → file.js) on macOS or Windows needs git mv.
Mixed line endings produce whole-file diffs: set * text=auto in .gitattributes.
PRs from forks get no secrets, so deploy or integration steps that need them fail there by design.
Interview tip
“I messed up my repo” answers: check git status, then git reflog. Almost any committed state can be recovered from the reflog before it expires.