Git Workflow 🔀: Best Practices for Your Development Process

Scenario:

  • You have a main branch (main).
  • You create feature branches (e.g., feature-x) for development.
  • You want to keep your history clean and structured.

1️⃣ git merge → Use When You Want to Keep Full History (Default Approach)

What It Does?

  • Combines feature branch commits into main, keeping all commit history.
  • Creates a new merge commit (Fast-forward if no diverging commits).
  • Easy to understand but can clutter history with many small commits.

When to Use?

  • If you want a full commit history, including every small commit.
  • When working in teams where visibility of each commit matters.

Example (Merging feature-x into main):

git checkout main
git pull origin main   # Ensure main is up to date
git merge feature-x
git push origin main

2️⃣ git rebase → Use When You Want a Clean, Linear History

What It Does?

  • Moves (reapplies) all commits from the feature branch on top of the latest main.
  • No merge commit → keeps history linear and clean.
  • Can cause conflicts if multiple people are rebasing.

When to Use?

  • When working solo and want a clean, linear history.
  • Before merging a feature branch to avoid unnecessary merge commits.
  • If you regularly update your feature branch with main.

Example (Rebasing feature-x onto main Before Merging):

# 1. Switch to your feature branch
git switch feature-x
# Commit or stash your changes before rebasing

# Pull and rebase from remote main
# 2. git pull --rebase origin main

# If conflicts occur:
# Resolve conflicts in the affected files

git add <resolved-files>

git rebase --continue
# Repeat if additional conflicts occur.

#to cancel the rebase:
git rebase --abort

# Push your feature branch
git push origin feature-x
Before rebase:

main:       A---B---C
                 \
feature-x:        D---E

After rebase:

main:       A---B---C
                       \
feature-x:              D'---E'

After fast-forward merge:

main:       A---B---C---D'---E'

3️⃣ git squash → Use When You Want a Single Commit for a Feature

What It Does?

  • Combines multiple commits in the feature branch into one commit before merging.
  • Keeps history very clean but loses commit granularity.

When to Use?

  • If a feature branch has many small commits (e.g., fix typo, refactor, debugging).
  • When you want one clean commit per feature.
  • If your team follows a “one commit per feature” policy.

Example (Squashing Commits Before Merging into main):

Step 1: Squash commits interactively

# Step 1
# Already on feature-x

git status

# Fetch latest remote changes
git fetch origin

# Squash feature commits
git rebase -i origin/main

# Step 2: Mark commits to squash

# Git opens an editor showing your feature commits:
pick a123456 Add media source model
pick b234567 Add media source API
pick c345678 Add media source validations
pick d456789 Add request specs

# Change pick to squash (or s) for the commits you want to combine:

pick a123456 Add media source model
squash b234567 Add media source API
squash c345678 Add media source validations
squash d456789 Add request specs

# Save and close the editor

# Set the final commit message
# Git opens another editor where you can write a consolidated commit message:

feat: add media source management

- Add media source model and validations
- Implement media source API endpoints
- Add request specs

# Save and close

# Verify the result
git log --oneline origin/main..HEAD

# Push your feature branch - If you haven't pushed this branch before:
git push -u origin feature-x

# If the branch was already pushed before squashing:
git push --force-with-lease origin feature-x

# Squashing rewrites commit history, so the remote branch may need this update.

Step 4: Merge into main

git checkout main
git merge feature-x
git push origin main

Another git squash example. Suppose you have created a branch for adding a feature and you do 2 more commits one for refactoring and other for commenting on the feature. Now you have 3 commits in the feature-x branch.

Now you think better I would commit these 3 commit together and push it as one feature commit. Now what you do? follow the bel0w steps:

# this tells git you want to do some action upon the last 3 commits
git rebase -i HEAD~3

Now you can see 3 commits in your default editor prefixed: pick. Change the last 2 commit pick to squash and save the file. Now Another file popup in the editor to change the commit message for this one squashed commit. Ignore the messages start with # . Keep/modify the commit message which is not starting with # and save the file. This rewrites the history and save as one commit.

Now check the commit history:

git log --oneline

Checkout git HEAD in our post: https://railsdrop.com/2025/04/06/git-best-practices-git-head-useful-commands-commit-message/

Important: Rebase vs Squash

OperationCommandPurpose
Rebasegit pull --rebase origin mainUpdate feature branch with latest main
Squashgit rebase -i origin/mainCombine multiple feature commits into fewer commits

One important distinction: the command above squashes all commits on feature-x since it diverged from origin/main into one commit. If you want to preserve some commits and squash only selected ones, use the interactive editor accordingly.

Which One to Use in Your Development Process?

ScenarioUse merge?Use rebase?Use squash?
Merging a feature branch✅ Yes (keeps history)✅ Yes (cleaner history)✅ Yes (one commit per feature)
Keeping history linear❌ No✅ Yes✅ Yes
Keeping every commit✅ Yes✅ Yes❌ No (loses small commits)
Team collaboration✅ Yes (safer)⚠️ Be careful (can rewrite history)✅ Yes (if team agrees)
Avoiding merge commits❌ No✅ Yes✅ Yes

Recommendation for Your Workflow 🔀

For daily work, keep your feature branch updated with main:

git checkout feature-x 
git pull origin main 
git rebase main

Before merging into main, If you want full history:

git merge feature-x

If you want a clean history:

git rebase main

If you want a single commit per feature:

git squash

Merge into main and push:

git checkout main 
git merge feature-x 
git push origin main

🤔 What’s the Problem with Merge Commits?

A merge commit happens when you run:

git merge feature-x

and Git creates an extra commit to record the merge.

Example merge commit:

commit abc123 (HEAD -> main)
Merge: def456 ghi789
Author: You <you@example.com>
Date:   Sat Mar 29 12:00:00 2025 +0000

    Merge branch 'feature-x' into 'main'

Why Do Some Developers Avoid Merge Commits?

1️⃣ Cluttered History

  • Merge commits pollute history when feature branches have multiple commits.
  • Running git log shows lots of merge commits, making it harder to track actual code changes.
  • Example of messy history with merge commits:
    * Merge branch 'feature-x' (Merge Commit)
    * Fix typo in error message
    * Add validation for user input
    * Implement user authentication
    * Merge branch 'feature-y' (Merge Commit)
    * Fix UI issue

2️⃣ Harder to Track Changes

  • git log gets filled with merge commits instead of meaningful commits.
  • git blame might point to a merge commit, making debugging harder.

3️⃣ Confusing Graph with Many Branches

  • Running git log --graph --oneline --all shows a complex branching history:
    * abc123 Merge branch ‘feature-x’
    |\
    | * ghi789 Fix authentication bug
    | * def456 Add new login method
    | 123456 Merge branch ‘feature-y’
    |/
  • If you rebase instead, the history stays linear.

🛠️ When Do Devs Avoid Merge Commits?

1️⃣ When They Want a Clean, Linear History

✅ Use git rebase instead of git merge to avoid merge commits.
Example:

git checkout feature-x
git rebase main
git checkout main
git merge feature-x  # No merge commit (fast-forward merge)

🔹 This way, the commits from feature-x appear directly on top of main.

2️⃣ When They Squash Multiple Commits into One

✅ Use git rebase -i main to combine multiple commits into one before merging.
Example:

git checkout feature-x
git rebase -i main  # Squash commits
git checkout main
git merge feature-x  # No unnecessary commits

🔹 This results in one clean commit per feature.

3️⃣ When Working on a Team with Many Developers

  • If multiple people merge their branches without rebasing, Git history gets full of merge commits.
  • Teams using trunk-based development or feature branch workflows often rebase instead of merging.

✅ Solution → Always rebase before merging:

git checkout feature-x
git pull --rebase origin main
git push origin feature-x

When Are Merge Commits Actually Useful?

❌ Merge commits are not always bad! Sometimes, they are necessary:

  1. Merging Long-Lived Feature Branches
    • If a branch exists for weeks or months, it has many commits.
    • A merge commit documents when the feature was merged.
  2. Merging Changes from a Release Branch
    • Example: Merging release-v1.0 into main should have a merge commit.
  3. Handling Complex Conflicts
    • When resolving big merge conflicts, a merge commit shows exactly when conflicts were fixed.

🔑 Final Answer: When to Avoid Merge Commits?

🚫 Avoid merge commits when you want a clean history
✅ Use rebasing (git rebase) before merging to keep history linear.
✅ Use squashing (git rebase -i) to avoid unnecessary commits in a feature branch.

🔹 If you’re working solo or in a small team, rebase before merging to keep history simple.
🔹 If you’re working in a large team with long-lived branches, merge commits may be useful for tracking.


🔥 Finally Best Practices for Your Development Process

Before merging, update your branch without a merge commit:

git checkout feature-x
git pull --rebase origin main

Squash unnecessary commits (optional):

git rebase -i main

Merge the branch (fast-forward, no merge commit):

git checkout main
git merge feature-x  # Fast-forward merge, no merge commit

This keeps your history clean and easy to read!