🌿 Rebase and Best Practices in Git

So far you have learned how to create branches with git branch, combine them with git merge, and resolve direct merge conflicts. In professional teams and high-performing environments, how you integrate your changes and document your work directly impacts the long-term maintainability of the codebase.

In this lesson, you will learn:

  1. What git rebase is and how its mental model works internally.
  2. When to choose merge vs rebase (technical tradeoffs and architectural reasoning).
  3. How to resolve conflicts during a rebase step by step.
  4. The Golden Rule of Rebase (and why breaking it harms your team).
  5. Professional best practices: atomic commits, Conventional Commits, and disciplined .gitignore management.

🔄 What is Git Rebase?

The git merge command combines two divergent histories by creating a dedicated merge commit. This preserves exact chronological ordering, but introduces branching curves and crossings in the history graph.

git rebase offers an alternative approach: re-basing your branch. Instead of creating a merge commit, Git takes the commits you created on your feature branch, temporarily “unplugs” them, fast-forwards your branch to the tip of main, and reapplies (replays) your commits one by one onto that new base.

Mental Model (Before and After)

Imagine this scenario: you branched feature off commit B. While you were developing, your teammates pushed commit C to main.

Mental model of git rebase: replaying commits for a linear history

1. BEFORE REBASE (Divergent branches)

main feature A B C D E Replay commits

2. AFTER git rebase main (Linear history)

main A B C D' E' feature (tip) Recalculated SHA-1 hashes · 100% linear history
Mental model of git rebase: feature branch commits are temporarily unplugged and replayed sequentially on top of the latest commit of main, producing a clean, linear history without merge bubbles.

Notice the commits are now labeled D' and E': their commit hashes (SHA-1) have changed, because they now have a different parent commit (C instead of B).

The result is a completely linear history, as if you had started writing your feature right after commit C.


⚖️ Merge vs Rebase: Architectural Tradeoffs

Neither command is inherently superior; they address different needs with different priorities:

Aspectgit mergegit rebase
History shapeBranching graph with explicit join commitsClean, single linear line
Historical contextAccurately records when branches diverged and mergedRewrites history to represent sequential progress
Conflict resolutionHandled once in the final merge commitHandled commit-by-commit during the replay
Pull Request traceabilityIdeal for documenting complete feature mergesIdeal for keeping local feature branches updated

Practical industry rule of thumb:

  • Use git rebase to keep your local branch up to date with main before opening or merging a Pull Request.
  • Use git merge (or the GitHub pull request merge workflow) to formally integrate an approved feature into a shared branch.

🛠️ Running a Rebase Step by Step

Suppose you are working on branch feature/cart and want to catch up with changes merged into main.

1. Update your local main branch

# 1. Switch to main branch
git switch main

# 2. Fast-forward pull latest remote changes
git pull --ff-only

2. Switch back to your working branch

# Switch back to your active feature branch
git switch feature/cart

3. Rebase onto main

# Replay feature/cart commits on top of the current tip of main
git rebase main

If your changes do not conflict with lines changed on main, Git replays each commit automatically:

Successfully rebased and updated refs/heads/feature/cart.

💥 Handling Conflicts During a Rebase

Unlike a merge (where all conflicting changes are resolved at once at the end), a rebase replays commits one by one. If commit D conflicts with changes on main, Git pauses execution at that exact commit.

Step-by-step conflict resolution:

1. Inspect conflicting files

# Check which files have conflicts in the current commit
git status

Git will list conflicting files as both modified.

2. Open the file and resolve the conflict markers

Just like in a traditional merge, you will see standard conflict markers:

<<<<<<< HEAD (incoming changes from main)
const TAX_RATE = 0.21;
=======
const TAX_RATE = 0.18;
>>>>>>> D (your commit currently being replayed)

Edit the file, choose the correct logic, and delete the marker lines (<<<<<<<, =======, >>>>>>>).

3. Stage the resolved file

# Mark the conflict for this step as resolved
git add src/config.js

4. Continue the rebase (Do NOT commit!)

⚠️ Critical detail: During a rebase, do not run git commit. The original commit already exists; you only need to tell Git that the current conflict is resolved so it can continue replaying the remaining commits:

# Instruct Git to continue applying the remaining commits
git rebase --continue

Git will proceed to the next commit. If another commit introduces a conflict, repeat these steps until finished.

Need to abort?

If something went wrong or you want to return to the exact state before starting the rebase, abort cleanly:

# Abort the rebase operation entirely and restore previous branch state
git rebase --abort

🚫 The Golden Rule of Rebase

NEVER rebase public or shared branches.

Apply rebase only to your private local branches that have not been pulled or depended upon by others.

Why?

Rebasing changes commit identifiers. If you rebase a branch that your colleagues have already cloned (such as main or develop) and then force-push with git push --force, you rewrite their history. Their local clones will diverge, resulting in confusing sync errors and potential loss of team work.


🏆 Professional Git Best Practices

Knowing commands is only the foundation. Professional engineering teams demand disciplined workflows:

1. Atomic and Focused Commits

An atomic commit encompasses a single, indivisible logical change.

  • ❌ Bad: A single commit that adds a login page, fixes a shopping cart bug, and re-indents 15 stylesheets ("various fixes and login").
  • ✔️ Good: Three separate commits, each with a single responsibility.

Why atomic commits matter:

  • They make Pull Requests easy and pleasant to review.
  • They allow reverting a broken feature with git revert without losing unrelated fixes.
  • They make tracking regressions effortless using git bisect.

2. Conventional Commits (Industry Standard)

Professional organizations follow the Conventional Commits specification for clear, machine-parseable, and human-readable commit messages:

<type>(<optional scope>): <short imperative description>

Primary types:

  • feat: A new user-facing feature.
    • Example: feat(auth): add google oauth login
  • fix: A bug fix.
    • Example: fix(cart): prevent negative item quantities
  • docs: Documentation-only changes.
    • Example: docs(api): update endpoints table
  • refactor: Code changes that neither fix a bug nor add a feature.
    • Example: refactor(db): extract query builder helper
  • test: Adding or fixing tests.
    • Example: test(auth): add unit test for token validation
  • chore: Maintenance, dependencies, or configuration tasks.
    • Example: chore(deps): bump astro to version 6.0

3. Strict .gitignore Management

The .gitignore file specifies intentionally untracked files that Git should ignore:

# Package dependencies
node_modules/
vendor/

# Build artifacts
dist/
build/
*.class

# Environment variables and secrets (CRITICAL!)
.env
.env.local
*.pem
*.key

# OS and IDE temporary files
.DS_Store
Thumbs.db
.vscode/
.idea/

🔒 Security Rule:

Never commit passwords, tokens, API keys, or .env files. Removing a secret in a subsequent commit does not purge it from Git history; anyone with access to the repo can inspect previous commits. If a secret is committed, rotate or revoke it immediately.


4. Inspect Before You Commit

Before running git add and git commit, develop the habit of inspecting what changed:

# Check a concise summary of modified and untracked files
git status --short

# Inspect line-by-line differences in the working tree against the last commit
git diff

Avoid blindly running git add . without checking that you are not unintentionally staging temporary logs, test files, or unintended modifications.


📌 Lesson Command Summary

ActionCommand
Rebase current branch on top of maingit rebase main
Resume rebase after resolving conflictsgit rebase --continue
Abort rebase and restore previous stategit rebase --abort
View short working tree statusgit status --short
View unstaged modificationsgit diff