LearnDevOps
DevOps02. Version Control·EXPERT·7 min read

4. Git Fundamentals

Git Fundamentals

TL;DR

  • Git tracks content across three trees: working directory, staging area (index), and commit history — git add exists 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:

  1. Inserts conflict markers directly into the affected file: <<<<<<<, =======, >>>>>>>.
  2. Leaves the file in a “both modified” state in git status.
  3. Requires the file be manually edited to the intended final content, then staged with git add, before the merge can be completed with git 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 main just to bring over one hotfix. Why: it drags in every other unrelated, possibly untested commit on that branch. Fix: use git cherry-pick to 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 --graph and git merge-base before 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 main without 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 add exist as a separate step from git 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.
🔒 Locked
Sign in and unlock the full syllabus to keep reading.
Sign in to continue
Knowledge check
6 scenario questions on this topic
Take the quiz →
Related in 02. Version Control
5. Git Workflows
EXPERT