Git Branching and Merging: Commands, Strategies and Examples
- Branches are cheap pointers to commits — create them freely. They cost nothing in Git.
- Merge preserves commit SHAs and creates a DAG history. Rebase creates linear history but rewrites all commit SHAs.
- Never rebase commits that have been pushed to a shared branch. It breaks every teammate's local history.
git checkout -b feature/name— create and switch to a new branchgit merge feature/x— integrate branch changes into current branchgit rebase main— replay your commits on top of main's latest state--no-ff— force a merge commit even when fast-forward is possible
'Your branch and origin/<branch> have diverged' after teammate's force-push
git fetch origingit reset --hard origin/<branch>Merge conflict — too many files to resolve manually
git merge --abort (or git rebase --abort)git log --oneline --graph HEAD <branch> to understand the divergenceFeature branch parent points to a SHA that no longer exists on develop
git fetch origin && git log --oneline origin/develop -5git rebase --onto origin/develop <old-parent-sha> <feature-branch-tip>git merge says 'Already up to date' but changes exist on the source branch
git branch --show-current && git log --oneline -3git log --oneline --graph --all to see the full branch topologyAfter rebase, commits appear duplicated or in wrong order
git reflog | head -20git reset --hard HEAD@{1} (or the pre-rebase entry)Production Incident
git rebase main on the develop branch to linearize 8 merge commits.
2. This rewrote all 8 commit SHAs. The new commits had the same content but different hashes.
3. git push origin develop was rejected (non-fast-forward). The engineer used git push --force.
4. The remote develop now pointed to the rebased commits. The old 8 commits were orphaned on the remote.
5. Twelve engineers had the old commits in their local develop. Their next git pull showed divergence.
6. Three engineers had feature branches with parents pointing to old develop SHAs. Those parent references were now dangling — their branches were effectively orphaned from develop's history.
7. CI webhook payloads referenced the old SHA of the last pre-rebase commit, which no longer existed on the remote.git reset --hard <pre-rebase-sha> && git push origin develop --force-with-lease.
3. For the 12 engineers with diverged local develop: git fetch origin && git reset --hard origin/develop.
4. For the 3 engineers with orphaned feature branches: used git rebase --onto origin/develop <old-parent> <branch-tip> to replay their feature commits on top of the restored develop.
5. Re-triggered CI manually.
6. Added branch protection rules requiring PR reviews for develop — preventing direct pushes entirely.Production Debug GuideSystematic recovery paths for history corruption, merge conflicts, and divergent branches.
Branching enables parallel development by isolating changes into independent lines of history. Each branch is a pointer to a commit — creating one costs almost nothing in Git because no files are copied.
The merge vs rebase decision affects how your repository's history reads during code review and debugging. Merge preserves the exact timeline of how work happened. Rebase creates a clean linear narrative. Both are used in production teams — the choice depends on your team's workflow and tooling.
Common misconceptions: that rebase is always better because it produces cleaner history, that fast-forward merges are always preferable, and that branches are expensive to create. Understanding the trade-offs prevents the force-push incidents and history corruption that plague teams adopting Git without training.
Creating and Managing Branches
A Git branch is a pointer to a commit. Creating a branch does not copy files — it creates a 41-byte reference file in .git/refs/heads/. This is why branches are cheap: you can create hundreds without meaningful storage or performance cost.
The pointer moves forward automatically when you make new commits on the branch. When you switch branches (git checkout or git switch), Git updates your working directory to match the commit the branch points to.
Branch naming conventions matter for tooling. Most CI systems, PR platforms, and branch protection rules use pattern matching (feature/, release/, hotfix/*). Consistent naming enables automated workflows — branch protection rules, auto-deletion after merge, and CI trigger filters all depend on naming patterns.
# io.thecodeforge — Branch Management # ───────────────────────────────────────────────────────────── # CREATE AND SWITCH # ───────────────────────────────────────────────────────────── # Modern syntax (Git 2.23+) git switch -c feature/payment-retry # Creates branch and switches to it. # Legacy syntax (still widely used) git checkout -b feature/payment-retry # Create branch from a specific commit git switch -c hotfix/critical-bug abc1234 # Branch starts from commit abc1234, not HEAD. # ───────────────────────────────────────────────────────────── # LIST AND INSPECT # ───────────────────────────────────────────────────────────── git branch # local branches (current branch marked with *) git branch -r # remote-tracking branches git branch -a # all branches (local + remote) git branch -v # branches with last commit info git branch --merged main # branches already merged into main git branch --no-merged main # branches not yet merged into main # Find branches containing a specific commit git branch --contains abc1234 # Useful when you need to know which branch introduced a bug. # ───────────────────────────────────────────────────────────── # PUSH AND TRACK # ───────────────────────────────────────────────────────────── # First push: set upstream tracking git push -u origin feature/payment-retry # -u sets upstream so future 'git push' and 'git pull' work without arguments. # Subsequent pushes (upstream already set) git push # Automatically pushes to origin/feature/payment-retry. # Push all local branches git push origin --all # ───────────────────────────────────────────────────────────── # DELETE AND CLEAN UP # ───────────────────────────────────────────────────────────── # Delete local branch (safe — refuses if unmerged changes exist) git branch -d feature/payment-retry # Force delete local branch (discards unmerged changes) git branch -D feature/payment-retry # Delete remote branch git push origin --delete feature/payment-retry # Prune stale remote-tracking references git remote prune origin # Removes references to remote branches that have been deleted. # Auto-delete after merge (GitHub/GitLab setting) # Most platforms delete the branch automatically after PR merge. # This keeps the remote clean without manual intervention. # ───────────────────────────────────────────────────────────── # RENAME # ───────────────────────────────────────────────────────────── # Rename current branch git branch -m new-name # Rename a different branch git branch -m old-name new-name
# * feature/payment-retry a1b2c3d Add PaymentRetryService
# main 8d1e3f5 Release v2.4.0
# feature/user-auth e4f5g6h Add OAuth2 integration
# git branch --no-merged main
# * feature/payment-retry
# feature/user-auth
# git branch --contains e4f5g6h
# feature/user-auth
- Creating a branch: one 41-byte file in .git/refs/heads/
- Switching branches: Git updates the working directory to match the target commit
- Deleting a branch: removes the pointer file. The commits remain until garbage-collected.
- Branch naming patterns enable CI/CD automation: feature/, release/, hotfix/*
git branch --no-merged main to find branches that haven't been integrated yet. Auto-delete merged branches to keep the remote clean.Merge vs Rebase
Merge and rebase both integrate changes from one branch into another. They produce different histories, and the choice affects code review, debugging, and collaboration.
Merge creates a merge commit — a special commit with two parents. It preserves the exact history of how work happened: when the branch was created, what commits were made on it, and when it was integrated. The history is a DAG (directed acyclic graph), not a straight line.
Rebase replays your commits on top of another branch. Each commit is cherry-picked onto the new base, creating new commits with new SHAs. The result is a linear history — no merge commits, no branching visible in git log. But the original commit SHAs are gone, which breaks any reference to them.
The golden rule: never rebase commits that have been pushed to a shared branch. Rebasing rewrites commit SHAs. Every teammate who pulled the old SHAs now has references to commits that no longer exist on the remote.
# io.thecodeforge — Merge vs Rebase # ───────────────────────────────────────────────────────────── # MERGE: Preserves exact history # ───────────────────────────────────────────────────────────── # Standard merge (fast-forward if possible, merge commit if diverged) git checkout main git merge feature/payment-retry # Force merge commit even when fast-forward is possible git merge --no-ff feature/payment-retry # Creates a merge commit that groups all feature branch commits. # Useful for traceability: git log --first-parent main shows # only merge commits, giving a high-level integration timeline. # Squash merge: collapse all feature commits into one git merge --squash feature/payment-retry git commit -m "feat(payment): Add PaymentRetryService" # All changes from the feature branch are staged. # You create a single commit on main with the combined changes. # The feature branch's individual commit history is lost on main. # ───────────────────────────────────────────────────────────── # REBASE: Linearises history # ───────────────────────────────────────────────────────────── # Rebase feature branch onto latest main git checkout feature/payment-retry git rebase main # Your commits are replayed on top of main's latest commit. # Each commit gets a new SHA. # Then fast-forward merge: git checkout main git merge feature/payment-retry # fast-forward, linear history # Interactive rebase: squash, reorder, edit commits before merging git rebase -i main # Editor opens with: # pick a1b2c3d Add PaymentRetryService # pick e4f5g6h Add retry config validation # pick i7j8k9l Fix typo in RetryConfig # # Change to: # pick a1b2c3d Add PaymentRetryService # squash e4f5g6h Add retry config validation # fixup i7j8k9l Fix typo in RetryConfig # # Result: one clean commit combining all three. # ───────────────────────────────────────────────────────────── # THE GOLDEN RULE # ───────────────────────────────────────────────────────────── # SAFE: rebase local-only branches (never pushed) git rebase main # on a branch only you use # DANGEROUS: rebase pushed shared branches git rebase main # on develop, which 12 engineers pull from # Then: git push --force ← THIS BREAKS EVERYONE # IF YOU MUST rewrite shared history: # 1. Coordinate with the team first # 2. Use --force-with-lease (never --force) # 3. Have everyone fetch and reset after your push
# Merge made by the 'ort' strategy.
# src/main/java/io/thecodeforge/payment/PaymentRetryService.java | 87 +++
# 1 file changed, 87 insertions(+)
# git rebase main (on feature branch)
# Successfully rebased and updated refs/heads/feature/payment-retry.
# All commits now have new SHAs.
- Merge: original commits untouched, merge commit added. DAG history.
- Rebase: all commits replayed with new SHAs. Linear history.
- Squash merge: collapses feature commits into one. Simplest history, loses granularity.
- The golden rule: never rebase commits that others have pulled.
git log --first-parent main shows a clean integration timeline — each merge commit represents a feature. With rebase, all commits appear linearly, mixing feature work with bug fixes. This makes git bisect more powerful (finer granularity) but git log harder to read. Teams using rebase should enforce squash-before-rebase to keep the linear history readable. Teams using merge should use --no-ff to preserve branch grouping in the history.Resolving Merge Conflicts
Merge conflicts occur when both branches modify the same lines of a file, or when one branch deletes a file the other modifies. Git cannot automatically decide which version to keep — it marks the file with conflict markers and pauses the merge.
Conflict markers show three versions: the current branch's version (<<<<<<< HEAD), a separator (=======), and the incoming branch's version (>>>>>>> feature/branch). Your job is to edit the file to produce the correct result, removing the markers.
Resolving conflicts is not just about picking one version over the other. Often the correct resolution combines both changes — for example, if both branches added different imports to the same file, you need both imports in the resolved version.
After resolving all conflicts, stage the files and commit. Git creates the merge commit automatically if all conflicts are resolved.
# io.thecodeforge — Resolving Merge Conflicts # ───────────────────────────────────────────────────────────── # CONFLICT DURING MERGE # ───────────────────────────────────────────────────────────── git merge feature/payment-retry # CONFLICT (content): Merge conflict in src/main/java/io/thecodeforge/payment/PaymentService.java # Automatic merge failed; fix conflicts and then commit the result. # Check which files have conflicts git status # Both modified: src/main/java/io/thecodeforge/payment/PaymentService.java git diff --name-only --diff-filter=U # Shows only files with unresolved conflicts. # ───────────────────────────────────────────────────────────── # CONFLICT MARKERS IN THE FILE # ───────────────────────────────────────────────────────────── # <<<<<<< HEAD (your current branch) # public PaymentResult processPayment(Order order) { # return paymentClient.charge(order.getAmount(), order.getCurrency()); # ======= # public PaymentResult processPayment(Order order, boolean retryEnabled) { # if (retryEnabled) { # return retryService.chargeWithRetry(order); # } # return paymentClient.charge(order.getAmount(), order.getCurrency()); # } # >>>>>>> feature/payment-retry # Edit the file to produce the correct combined result: # public PaymentResult processPayment(Order order, boolean retryEnabled) { # if (retryEnabled) { # return retryService.chargeWithRetry(order); # } # return paymentClient.charge(order.getAmount(), order.getCurrency()); # } # ───────────────────────────────────────────────────────────── # RESOLVE AND COMPLETE # ───────────────────────────────────────────────────────────── # Stage the resolved file git add src/main/java/io/thecodeforge/payment/PaymentService.java # Complete the merge git commit # Git auto-generates a merge commit message. # Or provide your own: git commit -m "Merge featuren # ─────────────────────────/payment-retry: resolve PaymentService conflict"\──────────────────────────────────── # ABORT: If conflicts are too complex # ───────────────────────────────────────────────────────────── git merge --abort # Cancels the merge. Working directory restored to pre-merge state. # Safe to use at any point during conflict resolution. # ───────────────────────────────────────────────────────────── # CONFLICT DURING REBASE # ───────────────────────────────────────────────────────────── # If conflicts occur during rebase: git rebase main # CONFLICT (content): Merge conflict in PaymentService.java # Resolve the conflict, then: git add src/main/java/io/thecodeforge/payment/PaymentService.java git rebase --continue # Git moves to the next commit in the rebase sequence. # More conflicts may occur in subsequent commits. # Skip a commit during rebase (if it's not important): git rebase --skip # Discards the current commit's changes entirely. # Abort the entire rebase: git rebase --abort # Returns to the state before the rebase started. # ───────────────────────────────────────────────────────────── # MERGE TOOLS # ───────────────────────────────────────────────────────────── # Configure a merge tool git config --global merge.tool vimdiff # Or: vscode, meld, p4merge, kdiff3, beyondcompare # Launch merge tool for all conflicted files git mergetool # Opens the configured tool with three panes: # LOCAL (your version), REMOTE (incoming version), BASE (common ancestor)
# On branch main
# You have unmerged paths.
# (fix conflicts and run "git commit")
#
# Unmerged paths:
# (use "git add <file>..." to mark resolution)
#
# both modified: src/main/java/io/thecodeforge/payment/PaymentService.java
#
# no changes added to commit
# (use "git add" and/or "git commit -a")
- Conflict markers show three versions: yours, theirs, and the common ancestor
- Resolving often requires combining both changes, not picking one
- After resolving: always compile and test before committing the merge
- Frequent conflicts in the same files indicate the code needs refactoring into separate modules
git merge --abort to cancel. After resolving, always compile and test — conflict resolution is a common source of regression bugs.Fast-Forward vs 3-Way Merge
A fast-forward merge happens when the target branch's HEAD is a direct ancestor of the source branch. Git simply moves the target pointer forward — no merge commit is created. The history remains linear.
A 3-way merge happens when both branches have diverged — each has commits the other doesn't. Git creates a merge commit with two parents, combining the changes from both branches. This requires finding the common ancestor (merge base) and computing the diff from that point.
The --no-ff flag forces a merge commit even when fast-forward is possible. This is useful for traceability: git log --first-parent main shows only merge commits, giving a clean integration timeline. Without --no-ff, feature branch commits appear directly in main's history with no grouping.
# io.thecodeforge — Fast-Forward vs 3-Way Merge # ───────────────────────────────────────────────────────────── # FAST-FORWARD: No divergence, pointer just moves forward # ───────────────────────────────────────────────────────────── # Scenario: main is at commit A. You branched feature from A. # You made commits B and C on feature. main is still at A. # No commits were added to main since you branched. git checkout main git merge feature/payment-retry # Fast-forward # Updating 8d1e3f5..a1b2c3d # Fast-forward # src/main/java/io/thecodeforge/payment/PaymentRetryService.java | 87 +++ # Result: main now points to commit C. No merge commit created. # git log shows: C → B → A (linear) # Force a merge commit even with fast-forward git merge --no-ff feature/payment-retry # Result: Merge commit M with parents C and A. # git log shows: M → C → B → A (with branch visible) # ───────────────────────────────────────────────────────────── # 3-WAY MERGE: Both branches diverged # ───────────────────────────────────────────────────────────── # Scenario: main is at commit A. You branched feature from A. # You made B and C on feature. Meanwhile, someone added D on main. # Both branches diverged from A. git checkout main git merge feature/payment-retry # Merge made by the 'ort' strategy. # src/main/java/io/thecodeforge/payment/PaymentRetryService.java | 87 +++ # 1 file changed, 87 insertions(+) # Result: Merge commit M with parents C (feature) and D (main). # The merge base is A. Git computes: diff(A→C) + diff(A→D) and combines them. # ───────────────────────────────────────────────────────────── # VIEWING THE MERGE BASE # ───────────────────────────────────────────────────────────── git merge-base main feature/payment-retry # Output: <sha-of-commit-A> # This is the common ancestor used for the 3-way merge. # ───────────────────────────────────────────────────────────── # FIRST-PARENT LOG: See only merge commits on main # ───────────────────────────────────────────────────────────── git log --first-parent main --oneline # Shows only commits on main's direct lineage. # Merge commits appear, but individual feature commits don't. # This gives a high-level integration timeline. # Only works well if you use --no-ff merges.
# 3-way merge: merge commit M created with parents C and D.
# --no-ff: forces merge commit even when fast-forward is possible.
- Fast-forward: no merge commit, linear history, pointer just moves forward
- 3-way merge: merge commit with two parents, DAG history, content combined
- --no-ff forces a merge commit for traceability even when fast-forward is possible
- git log --first-parent main shows only the integration timeline (merge commits)
git log --first-parent main that shows exactly when features were integrated. Teams that allow fast-forward get a linear history that's easier to read with git log but harder to trace which commits belong to which feature. For release management and changelog generation, --no-ff is superior because each merge commit represents a complete feature. For simple projects with few contributors, fast-forward is simpler and sufficient.--no-ff to force merge commits for traceability. git log --first-parent main shows only the integration timeline when using --no-ff merges.Branch Strategies: GitFlow, GitHub Flow, and Trunk-Based Development
Branch strategy defines how your team uses branches for development, releases, and hotfixes. The three dominant strategies are GitFlow, GitHub Flow, and trunk-based development. Each has different trade-offs for release cadence, code stability, and operational complexity.
GitFlow uses long-lived branches (main, develop) and short-lived branches (feature, release, hotfix). It's designed for scheduled releases with strict version management. The overhead is significant — multiple branch types, merge ordering rules, and release branch management.
GitHub Flow is simpler: one long-lived branch (main), short-lived feature branches, and deploy-from-main. Every merge strong CI/CD and feature flags for incomplete work.
Trunk-based development uses very short-lived branches (hours, not days) or direct commits to main. Feature flags control incomplete work. It maximizes integration frequency and minimizes merge conflicts but requires mature CI/CD and feature flag infrastructure.
# io.thecodeforge — Branch Strategies # ───────────────────────────────────────────────────────────── # GITFLOW # ───────────────────────────────────────────────────────────── # Branches: main, develop, feature/*, release/*, hotfix/* # Start a feature git checkout develop git checkout -b feature/payment-retry # Finish a feature: merge into develop git checkout develop git merge --no-ff feature/payment-retry git branch -d feature/payment-retry # Start a release git checkout develop git checkout -b release/v2.5.0 # Bug fixes only on release branch # Finish a release: merge into main works well for continuous to main triggers a deploy. It deployment but requires AND develop git checkout main git merge --no-ff release/v2.5.0 git tag -a v2.5.0 -m "Release v2.5.0" git checkout develop git merge --no-ff release/v2.5.0 git branch -d release/v2.5.0 # Hotfix: branch from main, merge back to main AND develop git checkout main git checkout -b hotfix/critical-bug # Fix the bug git checkout main git merge --no-ff hotfix/critical-bug git tag -a v2.5.1 -m "Hotfix v2.5.1" git checkout develop git merge --no-ff hotfix/critical-bug # ───────────────────────────────────────────────────────────── # GITHUB FLOW # ───────────────────────────────────────────────────────────── # Branches: main, feature/* (short-lived) # Create feature branch from main git checkout main git checkout -b feature/payment-retry # Push and open PR git push -u origin feature/payment-retry # Open PR on GitHub. CI runs automatically. # After PR approval: merge into main (or squash merge) # GitHub merges the PR. Main is deployed automatically. # Delete feature branch after merge git branch -d feature/payment-retry # ───────────────────────────────────────────────────────────── # TRUNK-BASED DEVELOPMENT # ───────────────────────────────────────────────────────────── # Branches: main (trunk), very short-lived branches (hours) # Create short-lived branch git checkout -b feature/payment-retry # Commit frequently (multiple times per day) git add src/main/java/io/thecodeforge/payment/PaymentRetryService.java git commit -m "WIP: PaymentRetryService skeleton" # Rebase onto main before merging (to stay current) git fetch origin git rebase origin/main # Merge into main via PR (or direct push with CI gates) git checkout main git merge --ff-only feature/payment-retry # --ff-only ensures no merge commit. Linear history. # Feature flags for incomplete work # In application code: # if (featureFlags.isEnabled("payment-retry")) { # return retryService.chargeWithRetry(order); # } # The feature is merged to main but hidden behind a flag.
# GitHub Flow: simple, deploy-from-main, requires strong CI
# Trunk-based: fast integration, requires feature flags and mature CI
- GitFlow: main + develop + feature/release/hotfix branches. Scheduled releases. High overhead.
- GitHub Flow: main + short-lived feature branches. Deploy on merge. Simple.
- Trunk-based: main + very short-lived branches (hours). Feature flags. Maximum integration frequency.
- Most teams at scale use trunk-based development with feature flags.
| Aspect | git merge | git rebase | git merge --squash |
|---|---|---|---|
| History shape | DAG (merge commits show branching) | Linear (no merge commits) | Linear (single commit per feature) |
| Commit SHAs preserved | Yes — original commits keep their SHAs | No — all replayed commits get new SHAs | N/A — only one new commit created |
| Safe on shared branches | Yes — does not rewrite history | No — rewrites history, breaks references | Yes — creates new commit, no rewrite |
| Traceability | High — merge commits group feature work | Medium — linear but commits are interleaved | Low — individual commits lost on main |
| git bisect effectiveness | Lower — merge commits are opaque | Higher — each commit is a bisect point | Lowest — only one commit per feature |
| Conflict resolution | Once at merge time | Once per commit during rebase | Once at merge time |
| Best for | Team integration, release branches | Local branch cleanup before PR | Simple integration, clean main history |
| Used by | GitFlow, release-managed teams | Individual developers before PR | Many open-source projects, simple teams |
🎯 Key Takeaways
- Branches are cheap pointers to commits — create them freely. They cost nothing in Git.
- Merge preserves commit SHAs and creates a DAG history. Rebase creates linear history but rewrites all commit SHAs.
- Never rebase commits that have been pushed to a shared branch. It breaks every teammate's local history.
- --no-ff forces a merge commit even when fast-forward is possible — useful for traceability and git log --first-parent.
- git merge --abort cancels an in-progress merge and restores the pre-merge state. Use it when conflicts are too complex.
- After resolving merge conflicts, always compile and test before committing. Conflict resolution is a common source of regression bugs.
- Branch strategy should match release cadence: GitFlow for scheduled releases, GitHub Flow for continuous deployment, trunk-based for high-frequency integration.
- Short-lived branches (hours, not days) minimize merge conflicts. Long-lived branches accumulate divergence and create integration pain.
- Feature flags enable merging incomplete work to main safely — the feature is deployed but hidden until enabled.
- git log --first-parent main shows only the integration timeline when using --no-ff merges — useful for release management and changelog generation.
⚠ Common Mistakes to Avoid
Interview Questions on This Topic
- QWhat is the difference between git merge and git rebase? When would you use each?
- QWhat is a fast-forward merge? When does it happen and how does it differ from a 3-way merge?
- QWhy should you never rebase a public branch? What exactly breaks when you do?
- QExplain the --no-ff flag. Why would a team choose to always use it?
- QWhat is a squash merge? What are its trade-offs vs a regular merge?
- QHow would you resolve a complex merge conflict in a file modified by both branches?
- QCompare GitFlow, GitHub Flow, and trunk-based development. When is each appropriate?
- QWhat is git merge-base and how does Git use it during a 3-way merge?
- QA teammate rebased develop and force-pushed. Your local develop has diverged. How do you recover?
- QHow do feature flags relate to trunk-based development? Why are they necessary?
Frequently Asked Questions
When should I use merge vs rebase?
Use rebase to clean up your local feature branch before opening a PR — it keeps the PR diff clean and up-to-date with main. Use merge to integrate the PR into main — it preserves the fact that work happened in a branch. Many teams use squash merge to collapse a feature branch into one commit on main.
What is a fast-forward merge?
A fast-forward merge happens when the branch being merged has no diverging commits from the target — target's HEAD is an ancestor of the branch. Git just moves the target pointer forward rather than creating a merge commit. Use --no-ff to force a merge commit if you want to preserve the branch structure in history.
What is a squash merge and when should I use it?
Squash merge (git merge --squash) takes all commits from the source branch and stages them as a single change. You then create one commit on the target branch. This produces the cleanest main history — one commit per feature — but loses the individual commit history of the feature branch. Use it for simple features where the intermediate commits aren't valuable for debugging.
How do I prevent merge conflicts?
Merge conflicts are minimized by keeping branches short-lived and integrating frequently. Rebase your feature branch onto main daily (git rebase main). If a branch must live for weeks, merge main into it regularly (git merge main) to resolve conflicts incrementally. Good modular architecture also helps — if two branches rarely touch the same files, conflicts are rare.
What is trunk-based development?
Trunk-based development is a branching strategy where developers integrate to main (the trunk) very frequently — multiple times per day. Branches live for hours, not days. Incomplete work is hidden behind feature flags. This maximizes integration frequency, minimizes merge conflicts, and enables continuous deployment. It requires strong CI/CD pipelines and feature flag infrastructure.
What happens if I rebase a shared branch?
Rebasing rewrites all commit SHAs from the rebase point onward. If teammates have pulled the old SHAs, their local branches now reference commits that don't exist on the remote. Their next pull shows divergence. Feature branches based on old SHAs become orphaned. The recovery requires every teammate to fetch, inspect, and reset their local branches. This is why the golden rule exists: never rebase public commits.
Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.