Skip to content

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.

Junior+

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?”
Answer

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:

  1. Working tree — the files on disk.
  2. Index — what goes into the next commit.
  3. Repository — the .git folder 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 fetch updates only the remote-tracking branches origin/*. Your branches and files stay untouched.
  • git pull runs fetch and then integrates origin/<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 pull with merge creates a merge commit. History fills up with “Merge branch ‘main’ of …”.
  • Junk in the commit. git add . stages everything, including bin/, 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 clone copies all objects and creates remote-tracking branches origin/*. The local main gets an upstream — the server branch it is compared against.
  • git status -sb shows the divergence from upstream: [ahead 2, behind 1]. It compares against the last fetch, not against the server right now.
  • You can have several remotes: origin for your fork and upstream for 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 true keeps local history linear, but conflicts are resolved commit by commit.
  • pull.rebase false is simpler but litters history with merge commits.
  • pull.ff only is 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 the pull strategy comes from.

Takeaway: Git has two histories, yours and the server’s, and nothing happens between them automatically beyond what you configured.

Example
Окно терминала
git clone https://github.com/acme/shop.git # copy of the repository with full history
cd shop
git switch -c feature/discount # new branch from the current commit
git status # what changed and what is already staged
git add src/Discount.cs # stage the file
git commit -m "Add discount calculation" # local commit: the server knows nothing yet
git fetch origin # update origin/*, leave your branches alone
git log --oneline main..origin/main # which commits arrived in main on the server
git push -u origin feature/discount # publish the branch and remember the upstream
Middle+

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?”
Answer

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

  1. Update your branch: git fetch, then git rebase origin/main or git merge origin/main.
  2. On a conflict Git stops and marks the disputed spots: <<<<<<<, =======, >>>>>>>.
  3. Fix the file, git add <file>, then git rebase --continue (after a merge — git commit).
  4. To back out — git rebase --abort or git 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

  1. Fast-forward. If the target branch has no new commits, merge just moves its pointer forward. No new commit is created.
  2. Three-way merge. Otherwise Git compares the common ancestor and the two tips and creates a merge commit. --no-ff creates one even when a fast-forward is possible.
  3. 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. --force overwrites other people’s commits. --force-with-lease refuses if the server branch changed since your last fetch. A fetch without 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_HEAD and the reflog point to them until garbage collection removes them.
  • Since Git 2.34 the default merge strategy is ort, a much faster replacement for recursive.

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> and git rebase -i --autosquash main fold a fix into the right commit.
  • git config rerere.enabled true records conflict resolutions and replays them on the next rebase.

How to check in production

  • git log --first-parent main shows main history one commit per PR, if PRs were merged with merge commits.
  • git range-diff compares 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.

Example
Окно терминала
git switch feature/discount
git fetch origin
git rebase origin/main # move our commits on top of a fresh main
# CONFLICT (content): Merge conflict in price.txt
git status --short # UU price.txt — the conflicted file
# edit price.txt and remove the <<<<<<< ======= >>>>>>> markers
git add price.txt
git rebase --continue # apply the remaining commits
git push --force-with-lease # refuses if someone changed the branch on the server
History: before, after merge and after rebase
# 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 price

After the rebase the branch commits have new hashes (19aa729 → e677de9), and the merge into main was a fast-forward.

Middle+

What branching models exist and how do you choose one?

Section titled “What branching models exist and how do you choose one?”
Answer

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

  1. Create a branch from a fresh main: git switch -c feature/discount origin/main.
  2. Make small commits and push the branch to the server.
  3. Open a pull request. CI checks the build and tests, colleagues review.
  4. Once approved, the PR is merged into main and 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 main and develop. 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

  1. release/2.4 is cut from main when the version is ready and tagged.
  2. No new features go in — fixes only.
  3. A fix is made in main and carried over with cherry-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.

Example
Окно терминала
# Trunk-based: a short branch from a fresh main, into main only via a PR
git switch -c fix/rounding origin/main
git commit -am "Fix rounding"
git push -u origin fix/rounding
# The release branch is cut from main and tagged
git switch -c release/2.4 main
git tag -a v2.4.0 -m "Release 2.4.0"
# The fix landed in main first (commit f2096a1); carry it over to the release
git switch release/2.4
git 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
Graph after carrying the fix over
$ 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
Senior+

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?”
Answer

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 -D prints the hash: (was f45de9d). It is also in git reflog. Restore with git branch <name> <hash>.
  • Stash. git stash -u also stashes untracked files.
  • Bisect. git bisect start <bad> <good>, then good or bad at 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 gc packs 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 gc removes commits nothing references.

A secret got into history

  1. Revoke and rotate the secret. This is the main step: everyone who ran fetch already has a copy of the history.
  2. Rewrite history: git filter-repo --sensitive-data-removal with --invert-paths --path <file> or --replace-text.
  3. Push the result: git push --force --mirror origin.
  4. On GitHub, PR refs cannot be overwritten this way — Support removes them and the cached views.
  5. 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.

Example
Окно терминала
git reset --hard HEAD~3 # "lost" three commits
git reflog -2
# 7bf4219 HEAD@{0}: reset: moving to HEAD~3
# e87e8fa HEAD@{1}: commit: Step 8
git 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 bad
git bisect start HEAD v2.3.0
git bisect run dotnet test --filter "FullyQualifiedName~DiscountTests"
# f45de9dde16e396ecacc40236ccfa2d41ffdb7e6 is the first bad commit
git bisect reset # go back to where you were before the search
A secret in history
Окно терминала
# 0. The secret is already revoked and rotated — without that the cleanup is pointless
git clone --mirror https://github.com/acme/shop.git
cd shop.git
# passwords.txt has lines like: P@ssw0rd==>***REMOVED***
git filter-repo --sensitive-data-removal --replace-text ../passwords.txt
git log -p --all | grep -c "P@ssw0rd" # 0 — the secret is gone from history
git push --force --mirror origin