Git
How Git stores history and syncs it with the server, how merge differs from rebase, which branching model a team should pick and how to rescue “lost” commits. Git comes up in every interview: the answers show whether a candidate has worked in a team with pull requests or only clicked buttons in the IDE.
How does Git work and which commands do you need every day?
Section titled “How does Git work and which commands do you need every day?”What it is
A commit is a snapshot of all files plus the author, a message and a link to the previous commit. A change moves through three areas:
- Working tree — the files on disk.
- Index — what goes into the next commit.
- Repository — the
.gitfolder with the history.
How to use
| Command | What it does |
|---|---|
git clone <url> |
copies a repository |
git status |
shows what changed and what is staged |
git add <file> |
stages changes |
git commit -m "..." |
creates a commit from the index |
git switch -c <branch> |
creates a branch and switches to it |
git fetch |
downloads other people’s commits |
git pull |
fetch plus a merge into the current branch |
git push |
sends commits to the server |
Common mistake
Assuming, as in SVN, that your colleagues already have the commit. Until git push only you have it.
Takeaway: a commit saves a snapshot locally, and push and fetch sync it with the server.
How it works
| Aspect | SVN | Git |
|---|---|---|
| History | on the central server | full copy in every clone |
| Commit | goes to the server at once | local; reaches the server via push |
| Branch | a copy of a directory | a pointer to a commit |
| Offline | almost nothing | everything except talking to the server |
fetch versus pull
git fetchupdates only the remote-tracking branchesorigin/*. Your branches and files stay untouched.git pullrunsfetchand then integratesorigin/<branch>into the current branch with a merge or a rebase.
Pitfalls
- Unexpected merge commit. If you and a colleague both committed to the same branch,
git pullwith merge creates a merge commit. History fills up with “Merge branch ‘main’ of …”. - Junk in the commit.
git add .stages everything, includingbin/,obj/and local settings. Exclude them in.gitignore. - pull refuses. Without a configured strategy Git stops with “Need to specify how to reconcile divergent branches”. The Git for Windows installer usually sets
pull.rebase false, so on Windows you may never see this error.
Rule of thumb
First git fetch and git log main..origin/main, then a deliberate merge or rebase. Configure the pull strategy explicitly.
Takeaway: fetch safely shows what changed on the server, while pull immediately pulls it into your branch.
Under the hood
git clonecopies all objects and creates remote-tracking branchesorigin/*. The localmaingets an upstream — the server branch it is compared against.git status -sbshows the divergence from upstream:[ahead 2, behind 1]. It compares against the lastfetch, not against the server right now.- You can have several remotes:
originfor your fork andupstreamfor the original project.
Cost
- A clone holds the entire history. CI is fine with
git clone --depth 1(shallow clone); large repositories use--filter=blob:none(partial clone), where old file contents are downloaded on demand. - Binary files do not merge, and Git itself has no locks like
svn lock: a conflict in a binary is resolved only by picking one version.
Trade-offs
pull.rebase truekeeps local history linear, but conflicts are resolved commit by commit.pull.rebase falseis simpler but litters history with merge commits.pull.ff onlyis the safest: on divergence Git stops and waits for a human decision.
How to check
git remote -v— where the remotes point.git log --oneline --graph --all— graph of local and remote branches.git config --show-origin --get-regexp pull— where thepullstrategy comes from.
Takeaway: Git has two histories, yours and the server’s, and nothing happens between them automatically beyond what you configured.
git clone https://github.com/acme/shop.git # copy of the repository with full historycd shopgit switch -c feature/discount # new branch from the current commitgit status # what changed and what is already stagedgit add src/Discount.cs # stage the filegit commit -m "Add discount calculation" # local commit: the server knows nothing yetgit fetch origin # update origin/*, leave your branches alonegit log --oneline main..origin/main # which commits arrived in main on the servergit push -u origin feature/discount # publish the branch and remember the upstream- How is
git fetchdifferent fromgit pull? —fetchonly downloads and updatesorigin/*;pullalso integrates into the current branch. - What is the index for? — It is a staging area that lets you commit only part of your changes, for example with
git add -p. - What does
[ahead 1, behind 2]ingit status -sbmean? — You have one commit the server lacks, and the server has two you lack. - How do you undo
git add? —git restore --staged <file>: the file leaves the index, the edits stay in the working tree. - How is
git switchdifferent fromgit checkout? —switchandrestoresplit the two jobs of the overloadedcheckout: switching branches and restoring files.
How is merge different from rebase, and when do you use each?
Section titled “How is merge different from rebase, and when do you use each?”What it is
Two ways to bring the changes of one branch into another.
- Merge joins branches. If both have moved on, you get a merge commit with two parents.
- Rebase replays your commits on top of another branch. You get new commits with new hashes and a straight history.
How to use
- Update your branch:
git fetch, thengit rebase origin/mainorgit merge origin/main. - On a conflict Git stops and marks the disputed spots:
<<<<<<<,=======,>>>>>>>. - Fix the file,
git add <file>, thengit rebase --continue(after a merge —git commit). - To back out —
git rebase --abortorgit merge --abort.
Common mistake
Rebasing a branch your colleagues have already pulled and running git push --force: their history diverges from the server.
Takeaway: merge keeps history as it is, rebase rewrites your commits on top of another branch.
How it works
- Fast-forward. If the target branch has no new commits, merge just moves its pointer forward. No new commit is created.
- Three-way merge. Otherwise Git compares the common ancestor and the two tips and creates a merge commit.
--no-ffcreates one even when a fast-forward is possible. - Rebase. Git finds the common ancestor, applies your commits one by one on the new base and moves the branch pointer to the result.
When to choose what
| Situation | What to use |
|---|---|
Update your branch from main |
rebase |
| Tidy your commits before a PR | rebase -i |
| Branch shared by several people | merge |
Merge a PR into main |
the method set in the repository |
Pitfalls
- Rebasing published work. The golden rule from Pro Git: do not rewrite commits that exist outside your repository and that people may have based work on.
- Force push.
--forceoverwrites other people’s commits.--force-with-leaserefuses if the server branch changed since your lastfetch. Afetchwithout integrating defeats that protection — that is what--force-if-includes(Git 2.30+) is for. - Squash merge. Afterwards the branch’s commits are not in
main. You cannot keep working on the same branch — delete it and start a new one.
Rule of thumb
Your own branch — rebase and --force-with-lease; a shared one — merge only.
Takeaway: rebase is for tidying your own commits, merge is for joining shared history.
Under the hood
- Rebase is a series of cherry-picks: each commit is reapplied as a separate patch. That is why a conflict can surface on every commit, not just once.
- The old commits are not deleted right away:
ORIG_HEADand the reflog point to them until garbage collection removes them. - Since Git 2.34 the default merge strategy is
ort, a much faster replacement forrecursive.
Trade-offs
| PR merge method | Pro | Con |
|---|---|---|
| Merge commit | full history, the merge is undone with one revert -m 1 |
graph with parallel lines |
| Squash | one commit per PR, readable main |
intermediate commits and their authors are lost |
| Rebase merge | linear history with individual commits | new hashes, commit signatures are lost |
Tidying your commits
git commit --fixup <hash>andgit rebase -i --autosquash mainfold a fix into the right commit.git config rerere.enabled truerecords conflict resolutions and replays them on the next rebase.
How to check in production
git log --first-parent mainshowsmainhistory one commit per PR, if PRs were merged with merge commits.git range-diffcompares a branch before and after a rebase and shows what changed while resolving conflicts.
Takeaway: merge keeps the truth about how the work went, rebase tells a convenient story about it; rewrite only what nobody has pulled yet.
git switch feature/discountgit fetch origingit rebase origin/main # move our commits on top of a fresh main# CONFLICT (content): Merge conflict in price.txtgit status --short # UU price.txt — the conflicted file# edit price.txt and remove the <<<<<<< ======= >>>>>>> markersgit add price.txtgit rebase --continue # apply the remaining commitsgit push --force-with-lease # refuses if someone changed the branch on the server# Before: the branches diverged$ git log --oneline --graph --all* d331889 Add tests* 19aa729 Apply discount| * 2767ee0 Update notes|/* 2ccb685 Add price
# Option 1: git merge feature/discount into main$ git log --oneline --graph* 6371798 Merge branch 'feature/discount'|\| * d331889 Add tests| * 19aa729 Apply discount* | 2767ee0 Update notes|/* 2ccb685 Add price
# Option 2: git rebase main on the branch, then git merge feature/discount into main$ git log --oneline --graph* 37980f0 Add tests* e677de9 Apply discount* 2767ee0 Update notes* 2ccb685 Add priceAfter the rebase the branch commits have new hashes (19aa729 → e677de9), and the merge into main was a fast-forward.
- What is a fast-forward and how do you prevent it? — Moving the pointer without a new commit;
git merge --no-ffalways creates a merge commit. - Why is
--force-with-leasebetter than--force? — It overwrites the branch only if the server still has what you saw at your lastfetch. - How do you undo a merged merge commit? —
git revert -m 1 <hash>:-m 1tells Git which parent is the mainline. - Why delete the branch after a squash merge? — Its commits are not in
main, so the next PR from it shows the old changes again or conflicts. - How do you split one big commit into several? —
git rebase -i, mark the commitedit, thengit reset HEAD~1and commit in parts.
What branching models exist and how do you choose one?
Section titled “What branching models exist and how do you choose one?”What it is
A branching model answers three questions: which branch you branch from, where you merge back and what goes to production.
How to use: GitHub Flow
- Create a branch from a fresh
main:git switch -c feature/discount origin/main. - Make small commits and push the branch to the server.
- Open a pull request. CI checks the build and tests, colleagues review.
- Once approved, the PR is merged into
mainand the branch is deleted.
Common mistake
Keeping a branch for weeks. The PR grows to hundreds of files, review becomes a formality and conflicts pile up at merge time.
Takeaway: a short branch from main, a PR with review and checks, merge, delete the branch.
How it works
| Model | Branches | Fits |
|---|---|---|
| Git Flow | main, develop, feature/*, release/*, hotfix/* |
versioned product, infrequent releases |
| GitHub Flow | main plus short task branches |
web service with frequent deployments |
| Trunk-based | trunk (main), one- or two-day branches, release branches when needed |
frequent integration and CI/CD |
When to choose what
- You deploy several times a week and support one version — GitHub Flow or trunk-based.
- You ship boxed versions and patch old ones — release branches: trunk-based with them, or Git Flow.
- Unfinished features do not live in a branch: they are merged switched off behind a feature flag.
Pitfalls
- Hotfix in Git Flow. The fix is merged into both
mainanddevelop. Forget the second and the bug returns in the next release. - Flags forever. Every flag means two code paths and two sets of tests. Once it is on for everyone, remove it.
- Protection without checks. A “PR only” rule without required checks and reviews protects nothing. Which checks to make required is covered in CI/CD and code quality.
Rule of thumb
The more often you deploy, the shorter your branches should live.
Takeaway: pick the model with the fewest long-lived branches that still lets you support the versions you need.
Cost of long branches
- Conflicts grow with divergence, and integration is postponed until the merge. Integration bugs surface at the end, when they are most expensive to fix.
- A big PR gets a shallow review: the reviewer’s attention is limited, the line count is not.
Release branches
release/2.4is cut frommainwhen the version is ready and tagged.- No new features go in — fixes only.
- A fix is made in
mainand carried over withcherry-pick -x, which keeps a reference to the original commit in the message.
The reverse order — release first — risks losing the fix in main, and the bug comes back in the next version.
Trade-offs
- Feature flags decouple deployment from release but add branching in code and combinations to test.
- A large refactoring without a long branch uses branch by abstraction: the new implementation grows next to the old one behind a shared abstraction.
- When dozens of PRs a day land in
main, checks run against a stale base miss breakages. GitHub’s merge queue tests each PR on top of the ones already queued.
How to check in production
Model metrics: branch lifetime, PR size, time from commit to production (lead time for changes from DORA). If they grow, the model is not working as intended.
Takeaway: a branching model serves the delivery cadence; long branches defer integration, flags and small PRs bring it forward.
# Trunk-based: a short branch from a fresh main, into main only via a PRgit switch -c fix/rounding origin/maingit commit -am "Fix rounding"git push -u origin fix/rounding
# The release branch is cut from main and taggedgit switch -c release/2.4 maingit tag -a v2.4.0 -m "Release 2.4.0"
# The fix landed in main first (commit f2096a1); carry it over to the releasegit switch release/2.4git cherry-pick -x f2096a1 # -x appends "(cherry picked from commit ...)"git tag -a v2.4.1 -m "Hotfix 2.4.1"git push origin release/2.4 v2.4.1$ git log --oneline --graph --all --decorate* f2096a1 (main) Fix rounding* b182e6f Feature B (behind flag)| * bf02e76 (HEAD -> release/2.4, tag: v2.4.1) Fix rounding|/* 7f43956 (tag: v2.4.0) Feature A- Why does the author of Git Flow not recommend it for web services? — It was designed for versioned software; for continuous delivery he recommends the simpler GitHub Flow.
- How do you ship an unfinished feature in trunk-based development? — Merge it switched off behind a feature flag and turn it on when ready.
- Where does a fix go first when there is a release branch? — Into
main, then cherry-pick into the release; otherwise the fix is easy to lose. - What does
mainprotection usually include? — PRs with approval, required CI checks, no force pushes and no branch deletion. - What is a merge queue for? — It tests a PR on top of the already accepted ones, so two PRs that are green on their own do not break
maintogether.
How does Git store data and how do you recover from mistakes?
Section titled “How does Git store data and how do you recover from mistakes?”What it is
A commit does not vanish when you reset or delete a branch. Git remembers every move of HEAD and branches in the reflog.
How to use
| Situation | Command |
|---|---|
| Undo a commit that is already pushed | git revert <hash> |
| Drop the last local commit but keep the edits | git reset --soft HEAD~1 |
Get commits back after reset --hard |
git reflog, then git reset --hard HEAD@{1} |
| Put unfinished work aside | git stash, later git stash pop |
| Copy one commit to another branch | git cherry-pick <hash> |
Common mistake
git reset --hard with uncommitted edits. The reflog only tracks commits, so unsaved changes are gone for good.
Takeaway: anything committed can almost always be recovered through the reflog; anything uncommitted cannot.
How it works
- A branch is a file in
.git/refs/heads/holding a commit hash. HEAD normally points to a branch:ref: refs/heads/main. - If HEAD points directly at a commit, that is a detached HEAD. New commits made in that state are lost unless you create a branch.
reset versus revert
| Command | What it changes |
|---|---|
reset --soft <commit> |
only the branch; changes stay staged |
reset --mixed <commit> |
branch and index; edits stay in the files |
reset --hard <commit> |
branch, index and working tree |
revert <commit> |
rewrites nothing, adds an inverse commit |
Other tools
- Accidentally deleted branch.
git branch -Dprints the hash:(was f45de9d). It is also ingit reflog. Restore withgit branch <name> <hash>. - Stash.
git stash -ualso stashes untracked files. - Bisect.
git bisect start <bad> <good>, thengoodorbadat each step;git bisect run <command>checks automatically.
Pitfalls
- Reverting a merge commit needs
-m 1. If you later merge the same branch again, the reverted changes do not come back — revert the revert first. - A dropped stash is not in the reflog. Find it with
git fsck --unreachable.
Rule of thumb
Shared history — revert; your own local history — reset.
Takeaway: reset moves a pointer, revert adds a commit, and the reflog remembers where the pointer used to be.
Under the hood
- An object is addressed by the hash of its contents (SHA-1; SHA-256 is also supported). A commit cannot be changed — only replaced by a new one with a different hash.
- New objects are stored as separate compressed files;
git gcpacks them into pack files with delta compression. - The reflog is local and expires: by default entries live 90 days, unreachable ones 30. After that
git gcremoves commits nothing references.
A secret got into history
- Revoke and rotate the secret. This is the main step: everyone who ran
fetchalready has a copy of the history. - Rewrite history:
git filter-repo --sensitive-data-removalwith--invert-paths --path <file>or--replace-text. - Push the result:
git push --force --mirror origin. - On GitHub, PR refs cannot be overwritten this way — Support removes them and the cached views.
- Colleagues re-clone. Prevent a repeat with
.gitignore, a secret store and push protection.
Large files
Git LFS keeps a text pointer in the repository and the content on a separate server. git lfs track writes a rule to .gitattributes; git lfs migrate import moves already committed files, rewriting history. GitHub blocks files larger than 100 MiB.
Trade-offs
Rewriting history changes every hash after the first modified commit: open PRs and other people’s clones break, so it is done once and by agreement.
Takeaway: commits are immutable and recoverable locally for a long time, but a leaked secret is saved by rotating it, not by cleaning history.
git reset --hard HEAD~3 # "lost" three commitsgit reflog -2# 7bf4219 HEAD@{0}: reset: moving to HEAD~3# e87e8fa HEAD@{1}: commit: Step 8git reset --hard HEAD@{1} # the branch is back on e87e8fa
git branch -D feature/x # Deleted branch feature/x (was f45de9d).git branch feature/x f45de9d # the branch is restored
# Find the commit that broke a test: v2.3.0 is good, HEAD is badgit bisect start HEAD v2.3.0git bisect run dotnet test --filter "FullyQualifiedName~DiscountTests"# f45de9dde16e396ecacc40236ccfa2d41ffdb7e6 is the first bad commitgit bisect reset # go back to where you were before the search# 0. The secret is already revoked and rotated — without that the cleanup is pointlessgit clone --mirror https://github.com/acme/shop.gitcd shop.git# passwords.txt has lines like: P@ssw0rd==>***REMOVED***git filter-repo --sensitive-data-removal --replace-text ../passwords.txtgit log -p --all | grep -c "P@ssw0rd" # 0 — the secret is gone from historygit push --force --mirror origin- What is the difference between
reset --soft,--mixedand--hard? — What gets reset along with the branch: nothing, the index, or the index and the working tree. - How do you restore a branch deleted with
git branch -D? — Take the hash from the command’s output or fromgit reflogand rungit branch <name> <hash>. - How long do reflog entries live? — 90 days by default, 30 for unreachable commits; the reflog exists only in the local repository.
- Is deleting the file with the password in a new commit enough? — No, it stays in history; rotate the secret and clean history with
git filter-repo. - What does
git bisect rundo if the command exits with 125? — It skips the commit as untestable, for example because it does not build.