DeepFrontend
Practice ArenaBlogSign in
Go Pro
DeepFrontend

Interview-grade learning paths and hands-on practice for mid-to-senior engineers. Master internals, patterns, and system architecture with runnable code.

All systems operational

Core Courses

  • -JavaScript Internals
  • -React Reconciliation
  • -TypeScript rigor
  • -Next.js Caching
  • -Node.js Event Loop
  • -System Design
  • -Web Security
  • -CSS & Page Layouts

Explore

  • Learning Paths
  • Practice Arena
  • Pricing Options
  • HTML Sitemap

Resources

  • Privacy Policy
  • Terms of Service
  • Email Support

© 2026 DeepFrontend. All rights reserved.

Expert learning environments for web engineering teams.

Home/Foundations & Tooling/Git & Version Control
intermediate30 min read·Updated Dec 2024Expert reviewed

Git & Version Control

Git is not just 'save and upload' — it's a content-addressable object store with a directed acyclic graph of commits. Understanding the object model, branching strategies, rebase vs merge, and conflict resolution separates engineers who use Git from engineers who are afraid of it.

gitversion-controlbranchingrebasemergeconflict-resolutionworkflow

Interview Questions

1 questions

Cheatsheet

Download-ready reference
Cheatsheet
Git object model:
  blob    file content (no filename)
  tree    directory (filenames + refs)
  commit  snapshot: tree + parent + metadata
  tag     named pointer to a commit

Key commands:
  git add -p                    interactive staging (hunk by hunk)
  git diff --staged             review what's staged before commit
  git commit --amend            rewrite last commit (don't do on pushed commits)
  git rebase -i HEAD~N          interactive rebase: squash, reword, reorder
  git stash / git stash pop     temporarily shelve changes
  git bisect start/good/bad     binary search for bug-introducing commit
  git reflog                    history of where HEAD has been (lifesaver)
  git worktree add ../path      check out a second branch without stashing
  git push --force-with-lease   safer force-push (fails if remote has new commits)
  git log --oneline --graph     visualize branch history

Merge vs Rebase:
  merge   preserves history, creates merge commit
  rebase  linear history, new SHAs — never on shared branches

Conventional Commits:
  feat(scope): description    new feature (bumps minor)
  fix(scope): description     bug fix (bumps patch)
  chore: description          tooling, no user impact
  BREAKING CHANGE: ...        in footer — bumps major

Conflict markers:
  <<<<<<< HEAD         your changes
  =======
  >>>>>>> other-branch  incoming changes
  Resolve: edit file, git add, git merge/rebase --continue

Related topics

Package Managers (npm / yarn / pnpm)CI/CD for FrontendPro

On this page

30m
Interview QuestionsCheatsheet

Concept#

The Git object model#

Everything in Git is an object identified by a SHA-1 (soon SHA-256) hash:

blob     — file content (just bytes, no filename)
tree     — directory listing (filenames + blob/tree references)
commit   — snapshot pointer: tree + parent commit(s) + author + message
tag      — annotated reference to a commit

A commit doesn't store diffs — it stores a complete snapshot as a tree of blobs. Diffs are computed on the fly by comparing two trees. This is why git log -p on a huge repo is cheap: it compares parent and child tree objects.

commit c3d4e5f
├─ tree a1b2c3
│  ├─ blob d4e5f6  (src/index.ts)
│  ├─ blob e5f6a7  (src/utils.ts)
│  └─ tree f6a7b8  (src/components/)
│     └─ blob a7b8c9 (Button.tsx)
└─ parent: b2c3d4e

Git's efficiency: if src/utils.ts hasn't changed between two commits, both commits reference the same blob e5f6a7. No duplication.

Refs: branches and HEAD#

A branch is just a file containing one SHA — the commit it points to. main = .git/refs/heads/main = c3d4e5f. When you commit, the branch ref updates to the new commit. Branches are cheap (a 41-byte file).

HEAD is a pointer to the currently checked-out branch (or a commit directly — "detached HEAD"). When you commit, HEAD → branch ref → new commit SHA.

HEAD → main → c3d4e5f
                    ↓
             parent: b2c3d4e

Merge vs Rebase#

The most important and most misunderstood Git operation pair.

Merge

Creates a merge commit with two parents. Preserves the true history of when branches diverged and converged.

Before:            After merge:
A - B - C (main)   A - B - C - M (main)
    \                   \     /
     D - E (feat)        D - E

When to merge:

  • Public/shared branches (don't rewrite history others have pulled)
  • When you want to preserve the exact history of when work was done
  • Merging feature branches into main via PRs

Rebase

Re-applies your commits on top of the target branch. The commits get new SHAs (content may be the same, but parent differs). Creates a linear history — easier to read, easier to bisect.

Before:           After `git rebase main` from feat:
A - B - C (main)  A - B - C (main)
    \                       \
     D - E (feat)            D' - E' (feat)

D' and E' are new commits with the same changes but different parents. The original D and E are abandoned (garbage collected).

When to rebase:

  • Updating a local feature branch with upstream changes before a PR (instead of a merge commit)
  • Interactive rebase (git rebase -i) to clean up messy local commits before review
  • Never on shared branches — rewriting history that others have pulled forces force-pushes and breaks their local copies

Golden rule: Never rebase commits that have been pushed to a branch others are working from.

Merge vs rebase — practical decision

SituationUse
Updating feature branch with latest maingit rebase main
Merging PR into mainMerge (or squash merge)
Cleaning up local WIP commitsgit rebase -i HEAD~N
Public hotfix branchMerge
Syncing a forkgit rebase upstream/main

Interactive rebase — the most useful command you're not using#

git rebase -i HEAD~5 opens an editor with the last 5 commits:

pick a1b2c3 Fix typo in header
pick d4e5f6 Add user auth
pick e5f6a7 WIP
pick f6a7b8 More auth stuff
pick a7b8c9 Tests for auth
 
# Commands:
# p, pick   = use commit as-is
# r, reword = use commit, but edit message
# e, edit   = use commit, but pause to amend
# s, squash = meld into previous commit
# f, fixup  = like squash but discard this commit's message
# d, drop   = remove commit entirely

Common pattern: squash WIP commits before a PR, reword confusing messages.

Conflict resolution#

A merge/rebase conflict means two branches changed the same lines differently. Git marks the conflict:

<<<<<<< HEAD (your branch)
function greet(name) {
  return `Hello, ${name}!`;
=======
function greet(user) {
  return `Hi, ${user.name}!`;
>>>>>>> feature/greet-refactor

Resolution: edit the file to the correct state (pick one side, combine, or rewrite), then git add <file> and git merge --continue (or git rebase --continue).

Strategies for avoiding conflicts:

  • Keep feature branches short-lived (< 1 week)
  • Sync with main frequently (daily rebase)
  • Avoid large refactoring PRs that touch many files

Branching strategies#

GitHub Flow (most teams)

main (always deployable)
  └─ feature/add-auth → PR → review → squash merge → delete branch

Simple. Requires CI on every PR. Works well for web services with continuous deployment.

Git Flow (complex release cycles)

main (production tags only)
develop (integration)
  └─ feature/* → develop → release/x.x → main + develop
hotfix/* → main + develop

Overkill for most web apps. Still used in mobile/native release pipelines.

Trunk-Based Development (large teams/monorepos)

Everyone merges to main daily. Feature flags gate incomplete features. Requires extremely fast CI and strong code review culture. Used by Google, Meta.


Common Mistakes#

1. Force-pushing to a shared branch without --force-with-lease#

git push --force will silently overwrite commits others pushed since your last fetch. git push --force-with-lease fails if the remote has changes you haven't fetched — much safer.

2. Giant commits#

A 4,000-line commit is impossible to review and impossible to bisect. Make commits atomic: one logical change per commit. It's easier to squash later than to split.

3. Committing secrets#

A .env file committed to a public repo exposes credentials even if you delete it in the next commit — the blob still exists in history. Fix: git filter-repo (preferred) or BFG Repo Cleaner. And immediately rotate the exposed credentials.

4. Rebasing shared branches#

Rebasing main or any branch others have checked out rewrites SHAs. Their local history diverges. They see "your branch and origin/main have diverged." Painful to untangle.

5. Not using .gitignore properly#

Tracking node_modules, build artifacts, .DS_Store, or .env files pollutes history. Set up global gitignore (~/.gitignore_global) for OS/editor artifacts and per-repo .gitignore for project artifacts.


Best Practices#

  • Commit messages: type(scope): short description (Conventional Commits) — enables changelog generation and semantic versioning automation. First line ≤ 72 chars.
  • One commit = one logical change. Future you will thank you at git bisect.
  • Use git stash + git stash pop for WIP when switching contexts. Or even better: git worktree add for parallel branches without stashing.
  • Review your diff before committing: git diff --staged shows exactly what's going in. Never git add . blindly.
  • Tag releases: . Tags are permanent checkpoints that survive branch deletion.

Performance Tips#

  • git fetch is safe, git pull is not. git pull = git fetch + git merge (or rebase if configured). Prefer git fetch && git rebase origin/main for explicit control.
  • Shallow clones (git clone --depth=1) for CI pipelines where full history isn't needed — saves bandwidth and time.
  • git bisect is a binary search through history for the commit that introduced a bug. Incredibly powerful with good atomic commits.
  • Sparse checkout (git sparse-checkout) checks out only specific directories — essential for monorepos (e.g., only checkout ).

Quiz#

[
  {
    "id": "git-q1",
    "type": "mcq",
    "prompt": "What does `git rebase main` do when run from a feature branch?",
    "options": [
      "Merges main into the feature branch, creating a merge commit",
      "Re-applies the feature branch's commits on top of main's latest commit, creating new commit SHAs",
      "Fast-forwards the feature branch to main's HEAD",
      "Creates a new branch from main with the feature branch's changes"
    ],
    "correct": 1,
    "explanation": "Rebase replays your commits on top of the target, creating new commits with new SHAs (same diffs, different parents). No merge commit. The feature branch history becomes linear. The original commits are abandoned (eventually GC'd)."







































git tag -a v1.0.0 -m "Release 1.0.0"
  • Protect main: require PRs + CI + at least one review before merge. No direct pushes.
  • packages/my-app
    ,
    "difficulty": "medium"
    },
    {
    "id": "git-q2",
    "type": "mcq",
    "prompt": "You accidentally committed a `.env` file with API keys to a public repo. You delete it in the next commit and push. Are the keys still exposed?",
    "options": [
    "No — deleting the file removes it from all history",
    "Yes — the blob still exists in Git's object store and is accessible via the commit hash",
    "No — GitHub automatically redacts secrets in commit history",
    "Yes, but only for 24 hours until GitHub purges it"
    ],
    "correct": 1,
    "explanation": "Git stores content as blobs by content hash. The `.env` blob still exists in the repo's object store and is reachable via the commit that added it. Anyone who cloned or fetched the repo has it. Immediately rotate all exposed credentials, then use `git filter-repo` to purge the file from history.",
    "difficulty": "hard"
    },
    {
    "id": "git-q3",
    "type": "mcq",
    "prompt": "What is a 'detached HEAD' state in Git?",
    "options": [
    "HEAD points to a commit directly instead of a branch ref",
    "The HEAD file is corrupt",
    "You're on a branch with no commits",
    "The remote branch has been deleted"
    ],
    "correct": 0,
    "explanation": "Normally HEAD → branch → commit. In detached HEAD, HEAD → commit directly (no branch in between). Commits you make won't belong to any branch — they'll be garbage collected unless you create a branch: `git checkout -b new-branch`.",
    "difficulty": "medium"
    },
    {
    "id": "git-q4",
    "type": "predict-output",
    "prompt": "You run `git log --oneline` and see:\n```\nc3d4e5f Add payment flow\nb2c3d4e Fix auth bug\na1b2c3d Initial commit\n```\nYou run `git rebase -i HEAD~2`. What commits appear in the editor?",
    "correct": "The two most recent commits: `b2c3d4e Fix auth bug` and `c3d4e5f Add payment flow` (listed oldest first in the editor). HEAD~2 means 2 commits back from HEAD.",
    "explanation": "HEAD~2 refers to the commit 2 steps before HEAD (a1b2c3d). Interactive rebase shows commits from HEAD~N+1 to HEAD, listed in reverse chronological order (oldest first in the editor so you read top-to-bottom).",
    "difficulty": "medium"
    }
    ]