4. Git Fundamentals
Git Fundamentals
TL;DR
- Git tracks content across three trees: working directory, staging area (index), and commit history —
git addexists specifically to let a commit include only some of the changes present in the working directory. - A merge conflict is Git refusing to guess between two edits to the same lines; it inserts conflict markers into the file and requires manual resolution before staging.
- Rebase rewrites history — safe on a branch nobody else has pulled, destructive on any branch others have already built work on top of.
- Cherry-pick moves a single commit’s changes to another branch without merging everything else from the source branch — the standard tool for hotfix backports.
- Tags are permanent, human-readable pointers to a commit, used to answer “what code shipped as v2.3.1” without hash archaeology.
- Biggest gotcha: rebasing a shared branch forces every collaborator into a force-push-and-reconcile cycle — it is a local-branch-only operation in team settings.
Core Concepts
The Three-Tree Model
Git tracks state in three distinct areas:
- Working directory — the actual files on disk, including any uncommitted edits.
- Staging area (index) — a snapshot of what will go into the next commit.
- Commit history (HEAD / repository) — the sequence of committed snapshots.
git add copies changes from the working directory into the index; git commit takes what’s in the index and writes it as a new commit. This separation is deliberate: it allows staging exactly the intended changes for a commit even when the working directory contains other, unrelated, unstaged edits (git add -p exploits this directly for partial-file staging).
Branching and Merging
A branch is a movable pointer to a commit; HEAD tracks the currently checked-out branch. Merging combines the histories of two branches.
A merge conflict occurs when both branches changed the same lines (or one branch deleted a file the other modified) since their common ancestor. Git cannot automatically decide which version is correct, so it:
- Inserts conflict markers directly into the affected file:
<<<<<<<,=======,>>>>>>>. - Leaves the file in a “both modified” state in
git status. - Requires the file be manually edited to the intended final content, then staged with
git add, before the merge can be completed withgit commit.
Fast-forward vs. --no-ff: if the target branch has no new commits since the feature branch diverged, git merge can fast-forward — it simply moves the branch pointer forward, producing no merge commit and flattening the feature branch’s history into a straight line. git merge --no-ff forces a merge commit even when a fast-forward is possible, preserving an explicit record that a set of commits was developed and merged as a unit. This trades a slightly noisier history for feature-level traceability.
Rebase
git rebase <branch> takes the commits unique to the current branch, replays them one at a time on top of the tip of <branch>, and produces new commits with new hashes — the original commits still exist but become unreferenced (and eventually garbage-collected).
Because rebase rewrites commit identity:
- Rebasing a purely local branch (nothing else has fetched or based work on it) is safe — no one else holds references to the old commits.
- Rebasing a branch others have already pulled is dangerous — their local history now diverges from the rewritten upstream history, and reconciling requires a force-push on one side and manual history surgery on the other for every collaborator.
Practical rule: rebase local feature branches onto main before opening a PR to keep history linear and avoid unnecessary merge commits; never rebase main or any branch other people are actively based on.
git rebase -i (interactive rebase) additionally allows reordering, squashing, splitting, editing, or dropping commits — used to clean up a feature branch’s history before merging.
Cherry-pick
git cherry-pick <commit-hash> applies the diff introduced by a single specific commit onto the current branch, creating a new commit with the same changes (and a new hash). It does not pull in any other commits from the source branch.
Primary use case: a fix lands on a release/hotfix branch and needs to reach main (or another release branch) without merging the rest of that branch’s unrelated, possibly untested commits. Cherry-pick can produce conflicts exactly like a merge if the target branch has diverged around the same lines, requiring the same manual resolution process.
Tags
A tag is a named, (usually) immutable pointer to a specific commit. Annotated tags (git tag -a v2.3.1 -m "...") store the tagger, date, and message as a full Git object; lightweight tags are just a pointer with no metadata.
Tags exist to make “what code is running in version X” answerable long after the fact — release tooling, changelogs, rollback scripts, and incident postmortems reference tags rather than raw commit hashes, because hashes are not memorable or discoverable without already knowing them.
Merge vs. Rebase
| Aspect | Merge | Rebase |
|---|---|---|
| History shape | Preserves actual branch/commit topology (with --no-ff) |
Produces a linear history; original commits are replaced |
| Commit hashes | Unchanged | Rewritten for every replayed commit |
| Safe on shared branches | Yes | No — rewrites history others may depend on |
| Conflict resolution | Resolved once, at the merge point | May need to be resolved once per replayed commit |
| Typical use | Combining a completed feature branch into main |
Cleaning up / updating a local feature branch before merging |
| Resulting commit | Merge commit with two parents (or none, if fast-forward) | Sequence of new single-parent commits |
Command / Configuration Reference
git add -p file.txt # interactively stage only some hunks of a file
git commit -m "message" # write staged changes as a new commit
git merge feature-branch # merge feature-branch into current branch (fast-forward if possible)
git merge --no-ff feature-branch # force a merge commit even if fast-forward is possible
git rebase main # replay current branch's commits on top of main
git rebase -i HEAD~5 # interactively edit/squash/reorder the last 5 commits
git cherry-pick abc1234 # apply the changes from commit abc1234 onto the current branch
git cherry-pick abc1234^..def5678 # cherry-pick a range of commits
git tag v2.3.1 # create a lightweight tag at HEAD
git tag -a v2.3.1 -m "Release 2.3.1" # create an annotated tag with metadata
git push origin v2.3.1 # push a single tag to the remote
git log --graph --oneline --all # visualize branch/merge topology
git merge-base branchA branchB # find the common ancestor commit of two branches
Common Pitfalls
- Pitfall: Rebasing a branch other people have already pulled. Why: rebase rewrites commit hashes, so downstream copies no longer share history with the rewritten branch. Fix: only rebase branches that are exclusively local, or coordinate a synchronized force-push if the branch must be rebased.
- Pitfall: Merging an entire release branch into
mainjust to bring over one hotfix. Why: it drags in every other unrelated, possibly untested commit on that branch. Fix: usegit cherry-pickto move only the specific fix commit. - Pitfall: No tags on production releases. Why: without a tag, identifying “what code was running during an incident” requires reconstructing it from commit timestamps and deploy logs. Fix: tag every release with a consistent scheme (e.g., semantic versioning) at deploy time.
- Pitfall: Force-pushing through a large, unexpected batch of merge conflicts. Why: conflicts across unrelated files usually indicate the branch is based on the wrong parent or has diverged further than assumed, not a normal merge situation. Fix: inspect
git log --graphandgit merge-basebefore resolving conflicts, to confirm the merge itself is sound. - Pitfall: Committing a file with unresolved conflict markers still in it. Why: the file was staged without actually replacing the
<<<<<<</=======/>>>>>>>markers with resolved content. Fix: always diff or review staged changes before committing a conflict resolution; some teams add a pre-commit hook that greps for conflict markers.
Interview Questions
- “Explain exactly when rebase is safe and when it’s dangerous.” — Tests whether the shared-history/hash-rewriting mechanism is understood, not just the rule of thumb.
- “A critical fix is on a release branch and needs to reach
mainwithout merging unrelated commits. What do you do?” — Tests whether cherry-pick is recognized as the correct, minimal tool. - “A merge between two branches produces conflicts in files neither branch should have touched. What’s your first move?” — Tests whether checking merge-base/branch ancestry precedes blind conflict resolution.
- “What is the difference between a lightweight tag and an annotated tag?” — Tests familiarity with Git’s tag object model.
- “Why does
git addexist as a separate step fromgit commit?” — Tests understanding of the three-tree model. - “What actually happens, mechanically, during
git rebase?” — Tests whether the commit-replay-and-rehash mechanism is understood versus treated as a black box.