HomeCurriculumGit & GitHubBranching, Merging & Rebase Strategies
Branching, Merging & Rebase Strategies

Branching, Merging & Rebase Strategies

Analyze Git branch references, fast-forward merges, Lowest Common Ancestor (LCA) three-way merge base calculations, merge conflict resolution, and the commit replay mechanics of git rebase.

DT
Authored by DevThesis Systems Editorial Team
Reviewed for Technical Accuracy & Code Determinism

In distributed version control systems, the true power of software collaboration emerges from non-linear development. Teams of dozens or hundreds of engineers operate simultaneously on overlapping features, bug fixes, and architectural refactorings. To support this parallel execution without chaos, Git provides a lightweight branching model, sophisticated three-way graph merge algorithms, and surgical history-rewriting capabilities via rebasing.

However, non-linear histories introduce complex graph topologies. Misunderstanding the difference between a fast-forward merge, a recursive three-way merge, and a rebase replay can result in overwritten production code, destroyed commit provenance, or crippling "merge hell." This guide delves into the mathematical graph theory, lowest common ancestor (LCA) algorithms, index stage mechanics, and industrial branching strategies that govern professional Git workflows.


1. Branching Topology & The Zero-Cost Branch Invariant#

In legacy centralized version control systems (such as CVS or Subversion), creating a branch was an expensive, heavy-weight operation. In Subversion, branching required duplicating the entire project directory tree into a separate /branches/ folder on the centralized server, consuming disk storage and network bandwidth proportional to the repository size.

text
SUBVERSION BRANCHING:
/trunk/src/... (Files duplicated)
/branches/feature-x/src/... (Full directory copy created on disk)

GIT BRANCHING:
Commit A <--- Commit B <--- Commit C (main)
                             ^
                             |
                           (feature-x)
* Branching creates a 41-byte text file containing a 40-character SHA-1 hash.

The Anatomy of a Git Branch#

In Git, a branch is not a container, a folder, or an isolated commit tree. A branch is nothing more than a mutable pointer to a specific commit node in the Directed Acyclic Graph (DAG).

  • When you execute git branch feature-login, Git does not copy any files or create new objects in .git/objects.
  • It creates a single 41-byte plaintext file: .git/refs/heads/feature-login, writing into it the 40-character hexadecimal SHA-1 hash of the current commit, terminated by a newline (0x0A).
  • The computational and storage cost of creating a branch in Git is O(1). Whether your repository has 10 files or 10 million files, branch creation completes in under a single millisecond.

The Role of `HEAD` & Switching Branches#

How does Git know which branch you are actively working on? It inspects the special pointer file .git/HEAD.

  • Under normal conditions, HEAD contains a symbolic reference: ref: refs/heads/main.
  • When you switch branches via git switch feature-login (or legacy git checkout feature-login):

1. Git updates .git/HEAD to point to ref: refs/heads/feature-login.

2. Git inspects the commit pointed to by feature-login, traverses its root tree object, and synchronizes the Index (.git/index) and the physical Working Directory to match that tree snapshot.

3. Any subsequent git commit advances the pointer in .git/refs/heads/feature-login to the newly minted commit hash, while HEAD remains attached to feature-login.

The Detached `HEAD` State#

If you check out a specific commit hash directly (e.g., git checkout 9f8a2c1) or check out a remote branch without creating a local tracking branch:

  • HEAD points directly to the commit SHA-1 rather than to a branch reference in refs/heads/.
  • If you make commits while in a detached HEAD state, those commits are valid DAG nodes with parent pointers. However, no branch reference points to them.
  • If you switch back to main without creating a branch, those detached commits become unreachable DAG nodes (dangling commits). Unless rescued via git branch rescue-branch <SHA> or recovered from the reference log (git reflog), they will eventually be pruned and permanently deleted by git gc during garbage collection.

2. The Mechanics of Merging: Fast-Forwards vs. Three-Way Merges#

When parallel lines of development must be unified, Git provides two fundamentally distinct merge strategies: Fast-Forward merges and Three-Way merges.

text
FAST-FORWARD MERGE:
Before:  A ---> B (main)
                 \
                  ---> C ---> D (feature)
After:   A ---> B ---> C ---> D (main, feature)
* Pointer simply moves forward. No new merge commit is created.

THREE-WAY MERGE (Non-Fast-Forward):
Before:  A ---> B (LCA) ---> C (main)
                 \
                  ---> D ---> E (feature)
After:   A ---> B ----------> C -------> M (main - 2 parents: C, E)
                 \                      /
                  ---> D ---> E -------+ (feature)
* Creates Merge Commit M with parents [C, E] reconciling divergent diffs.

Fast-Forward Merges (`--ff`)#

A fast-forward merge occurs when the commit history of the target branch (main) is a direct linear ancestor of the branch being merged (feature).

  • Because no divergent commits exist on main since feature branched off, Git does not need to perform diff algorithms or synthesize a merge commit.
  • Git simply advances the reference pointer of main to point to the latest commit of feature.
  • The Trade-off: Fast-forward merges keep history clean and linear, but they destroy the contextual encapsulation of the feature. Looking back at git log six months later, it is impossible to determine which commits belonged to a discrete pull request and when that feature was integrated into production.
  • The Industrial Standard (`--no-ff`): Many engineering teams enforce git merge --no-ff. This flag forces Git to create an explicit merge commit even when a fast-forward is mathematically possible, guaranteeing an auditable milestone in git history for every integrated feature.

Three-Way Merges (`recursive` & `ort`)#

When main and feature have diverged (meaning new commits were pushed to main after feature branched off), a fast-forward merge is mathematically impossible. Git must perform a Three-Way Merge.

To merge feature into main, Git requires three distinct tree snapshots:

  1. The Base Tree (`B`): The Lowest Common Ancestor (LCA) of main and feature.
  2. The Ours Tree (`O`): The snapshot of the target branch (HEAD / main).
  3. The Theirs Tree (`T`): The snapshot of the incoming branch (feature).

The Three-Way Merge Decision Matrix

For every file and every line chunk, Git compares O and T against the common ancestor B:

text
+-----------------------------------------------------------------------------------------+
|                              THREE-WAY MERGE DECISION TABLE                             |
+--------------------+--------------------+--------------------+--------------------------+
| Base (B)           | Ours (O)           | Theirs (T)         | Result in Merged Tree    |
+--------------------+--------------------+--------------------+--------------------------+
| Version 1          | Version 1 (no chg) | Version 1 (no chg) | Version 1 (no change)    |
| Version 1          | Version 2 (mod)    | Version 1 (no chg) | Version 2 (take Ours)    |
| Version 1          | Version 1 (no chg) | Version 2 (mod)    | Version 2 (take Theirs)  |
| Version 1          | Version 2 (mod A)  | Version 3 (mod B)  | CONFLICT! (Requires human|
|                    |                    |                    | or semantic resolution)  |
| Version 1          | Deleted            | Version 1 (no chg) | Deleted                  |
| Version 1          | Version 2 (mod)    | Deleted            | CONFLICT! (Modify/Delete)|
+--------------------+--------------------+--------------------+--------------------------+

Notice the power of the three-way comparison: if a file is modified in Theirs but untouched in Ours, Git knows that Theirs represents an intentional modification relative to the ancestor, and automatically adopts the change without human intervention. A naive two-way comparison between O and T could not discern which side made the change.

The Lowest Common Ancestor (LCA) & Criss-Cross Merges#

How does Git find B? It computes the Lowest Common Ancestor (LCA) in the commit DAG using topological sorting:

bash
git merge-base main feature

In clean graphs, a single unique LCA exists. However, in complex multi-branch environments, criss-cross merges can occur where two branches have merged each other's changes at different points in time, resulting in multiple independent LCAs with no single common ancestor.

  • The `recursive` Merge Strategy: Historical Git resolved this by recursively creating a virtual merge base. It took the multiple LCAs, merged them together into a temporary virtual commit tree, and used that synthesized tree as the common base B!
  • The `ort` Merge Strategy ("Ostensibly Recursive's Twin"): Introduced in Git 2.33 and now the default merge engine, ort completely rewrote the merge algorithm from scratch. It provides massive speedups (often 10x to 100x faster in large monorepos), perfect rename detection memoization, and correct handling of complex directory/file collision conflicts.

3. Rebase: Rewriting Graph History via Patch Replay#

While git merge preserves historical reality by recording exact temporal branching points, git rebase alters history to present a pristine, linear narrative.

text
BEFORE REBASE:
A ---> B ---> C (main)
        \
         ---> D ---> E (feature)

DURING REBASE (`git switch feature && git rebase main`):
Step 1: Commits in feature (D, E) are extracted as patches.
Step 2: HEAD moves to commit C on main.
Step 3: Patch D is applied onto C ---> creates brand new commit D' (new SHA-1).
Step 4: Patch E is applied onto D' ---> creates brand new commit E' (new SHA-1).

AFTER REBASE:
A ---> B ---> C (main) ---> D' ---> E' (feature)
* History is completely linear. Commits D and E become unreachable garbage.

The Rebase Replay Mechanics#

When you rebase feature onto main:

  1. Git identifies the common ancestor commit: LCA = git merge-base main feature.
  2. Git computes the list of commits belonging exclusively to feature: LCA..feature (commits D and E).
  3. Git stores the diffs of these commits in a temporary memory queue.
  4. Git resets HEAD to the tip of main (commit C).
  5. Git iterates through the queued patches, applying each diff sequentially onto the new base.
  6. For each applied patch, Git writes a brand new commit object. Even if the code diff is identical, the commit's parent pointer has changed from B to C. By virtue of the Merkle DAG cascading property, changing the parent hash completely changes the commit's SHA-1 cryptographic identifier! Commits D' and E' are physically brand new objects in .git/objects.

Interactive Rebasing (`git rebase -i`)#

Interactive rebasing allows developers to sanitize, polish, and restructure commit sequences before opening a pull request.

Executing git rebase -i HEAD~4 opens a control sheet in your editor:

text
pick a1b2c3d feat(auth): add JWT generation logic
squash e4f5g6h fix typo in token expiration
reword 7h8i9j0 feat(auth): implement refresh token endpoint
drop 1k2l3m4 debug console log statements

Supported Interactive Commands:

  • pick: Keep the commit as-is.
  • reword: Keep the commit diff, but edit the commit message.
  • edit: Pause the rebase after applying this commit, allowing you to modify files, split commits, or amend code.
  • squash: Meld the commit into the previous commit, combining both commit messages.
  • fixup: Meld the commit into the previous commit, discarding this commit's message (ideal for "fix typo" or "lint fix" commits).
  • drop: Eradicate the commit from the DAG entirely.

The Golden Rule of Rebasing#

> NEVER rebase commits that have been pushed to a public, shared repository.

If you rebase commits that colleagues have already pulled onto their local machines, you replace the original commit hashes (D, E) with new hashes (D', E'). When your colleagues pull, their Git engine sees two divergent branches with identical code but different histories, triggering nightmarish duplicate commits, repeated merge conflicts, and corrupted graph lineage.


4. Conflict Resolution & The Plumbing of the Staging Area#

When two branches modify the exact same lines of a file, or if one branch deletes a file that another branch modified, Git halts the merge or rebase and declares a Merge Conflict.

text
<<<<<<< HEAD (Ours - current branch)
const API_BASE_URL = "https://api.production.internal/v2";
=======
const API_BASE_URL = "https://api.staging.internal/v1";
>>>>>>> feature-env (Theirs - incoming branch)

The Three Conflict Stages in `.git/index`#

When a merge conflict occurs, the staging area (.git/index) enters an exceptional multi-stage state. Instead of holding a single Stage 0 entry for the file, the index holds up to three separate entries:

  • Stage 1 (`:1`): The version of the file as it existed in the Common Ancestor (Base).
  • Stage 2 (`:2`): The version of the file from Target Branch (`HEAD` / Ours).
  • Stage 3 (`:3`): The version of the file from Incoming Branch (`Theirs`).

You can inspect each stage directly via Git's plumbing tools:

bash
# View the common ancestor version
git show :1:src/config.ts

# View our local branch version
git show :2:src/config.ts

# View their incoming branch version
git show :3:src/config.ts

Programmatic Conflict Resolution Workflow#

  1. Inspect the conflict markers inside the working tree file.
  2. Edit the file to produce the correct unified logic.
  3. Remove all conflict markers (<<<<<<<, =======, >>>>>>>).
  4. Execute git add src/config.ts:

- Adding the resolved file writes a new blob to .git/objects.

- It clears Stages 1, 2, and 3 from .git/index and writes the resolved blob into Stage 0 (normal staged state).

  1. Execute git commit (or git rebase --continue) to finalize the merge commit.

Git `rerere`: Reuse Recorded Resolution#

In long-running branches and complex multi-step rebases, developers often resolve the exact same merge conflict dozens of times. Git provides a built-in automation engine called `rerere` (Reuse Recorded Resolution):

bash
git config --global rerere.enabled true

When enabled, Git:

  1. Records a fingerprint (pre-image) of any unresolved conflict hunk in .git/rr-cache/.
  2. When the user successfully resolves the conflict, Git records the post-image resolution.
  3. In future merges or rebases, if Git encounters an identical conflict hunk, it automatically applies the saved resolution without interrupting the developer!

5. Industrial Branching Strategies: Git Flow vs. GitHub Flow vs. Trunk-Based#

How software organizations organize branches determines release velocity, cycle time, and architectural stability.

text
+-----------------------------------------------------------------------------------------+
|                           INDUSTRIAL BRANCHING MODEL COMPARISON                         |
+--------------------+-------------------------+--------------------+---------------------+
| Strategy           | Primary Branches        | Release Cadence    | Best Fit            |
+--------------------+-------------------------+--------------------+---------------------+
| Git Flow           | main, develop, feature, | Scheduled releases | Packaged software,  |
|                    | release, hotfix         | (Weeks to Months)  | embedded firmware   |
+--------------------+-------------------------+--------------------+---------------------+
| GitHub Flow        | main, feature branches  | Continuous Deploy  | SaaS web apps,      |
|                    |                         | (Daily to Hourly)  | microservices       |
+--------------------+-------------------------+--------------------+---------------------+
| Trunk-Based Dev    | main (trunk) exclusively| Continuous Deploy  | High-velocity teams,|
|                    | + short-lived (<24h) br | (Multiple/Hour)    | elite DORA metrics  |
+--------------------+-------------------------+--------------------+---------------------+

1. Git Flow (Vincent Driessen, 2010)#

Git Flow is a formal, highly structured branching model with strict long-lived branches:

  • main: Reflects production-ready state. Only receives merges from release and hotfix branches.
  • develop: The primary integration branch for ongoing development. Feature branches fork from develop and merge back into develop.
  • release/*: Cut from develop when features are frozen for testing; merges into both main and develop.
  • hotfix/*: Cut directly from main to patch production emergencies.
  • Critique: Git Flow introduces immense merge overhead and conflicts in modern continuous deployment (CD) pipelines. It was engineered for boxed desktop software with rigid release schedules, not cloud-native microservices.

2. GitHub Flow (Scott Chacon, 2011)#

GitHub Flow simplifies collaboration into a lightweight, pull-request-driven model:

  1. Anything in the main branch is deployable to production at all times.
  2. To work on something new, create a descriptive branch off of main (e.g., feature/user-auth).
  3. Commit locally and regularly push your work to the identically named branch on the remote server.
  4. Open a Pull Request (PR) to request peer review, run CI test suites, and discuss implementation.
  5. Once approved and green in CI, merge into main and deploy immediately to production.

3. Trunk-Based Development (TBD)#

Trunk-Based Development is the standard practiced by elite engineering organizations (Google, Meta, Netflix) and is strongly correlated with top-tier DORA (DevOps Research and Assessment) performance metrics.

  • Developers commit in very small batches directly to main (the trunk) or work on short-lived feature branches that merge into main multiple times per day (lifespans under 24 hours).
  • Feature Flags / Toggles: How do engineers merge incomplete features into main without breaking production? Incomplete code paths are wrapped in runtime feature flags. The code is continuously integrated and deployed, but the user-facing functionality remains dormant until enabled in production configurations.
  • Eliminates "merge hell" entirely by ensuring no developer's code diverges from the trunk for more than a few hours.

6. Industrial Implementation: Three-Way Merge Engine & Conflict Resolver#

The following complete, runnable TypeScript implementation demonstrates the operational logic of a Three-Way Line-Based Merge Algorithm. It takes three text strings—representing the Common Ancestor (base), Local Branch (ours), and Incoming Branch (theirs)—and algorithmically computes the merged result or injects standard Git conflict markers.

typescript
// threeWayMergeEngine.ts
// Production-grade line-by-line Three-Way Merge Engine and Conflict Synthesizer.

export interface MergeResult {
  hasConflicts: boolean;
  conflictCount: number;
  mergedContent: string;
}

export class ThreeWayMergeEngine {
  /**
   * Executes a line-based three-way merge between Base, Ours, and Theirs.
   */
  public static merge(baseText: string, oursText: string, theirsText: string): MergeResult {
    const baseLines = baseText.split('\n');
    const oursLines = oursText.split('\n');
    const theirsLines = theirsText.split('\n');

    const outputLines: string[] = [];
    let hasConflicts = false;
    let conflictCount = 0;

    let bIdx = 0;
    let oIdx = 0;
    let tIdx = 0;

    while (bIdx < baseLines.length || oIdx < oursLines.length || tIdx < theirsLines.length) {
      const bLine = bIdx < baseLines.length ? baseLines[bIdx] : null;
      const oLine = oIdx < oursLines.length ? oursLines[oIdx] : null;
      const tLine = tIdx < theirsLines.length ? theirsLines[tIdx] : null;

      // Case 1: All three sides are identical
      if (bLine === oLine && bLine === tLine) {
        if (oLine !== null) outputLines.push(oLine);
        bIdx++;
        oIdx++;
        tIdx++;
        continue;
      }

      // Case 2: Ours changed relative to Base; Theirs remained identical to Base
      if (bLine !== oLine && bLine === tLine) {
        if (oLine !== null) outputLines.push(oLine);
        bIdx++;
        oIdx++;
        tIdx++;
        continue;
      }

      // Case 3: Theirs changed relative to Base; Ours remained identical to Base
      if (bLine === oLine && bLine !== tLine) {
        if (tLine !== null) outputLines.push(tLine);
        bIdx++;
        oIdx++;
        tIdx++;
        continue;
      }

      // Case 4: Both Ours and Theirs changed identically
      if (oLine === tLine && bLine !== oLine) {
        if (oLine !== null) outputLines.push(oLine);
        bIdx++;
        oIdx++;
        tIdx++;
        continue;
      }

      // Case 5: DIVERGENT MODIFICATIONS -> CONFLICT!
      hasConflicts = true;
      conflictCount++;

      outputLines.push('<<<<<<< HEAD (Ours)');
      if (oLine !== null) outputLines.push(oLine);
      outputLines.push('=======');
      if (tLine !== null) outputLines.push(tLine);
      outputLines.push('>>>>>>> Incoming (Theirs)');

      bIdx++;
      oIdx++;
      tIdx++;
    }

    return {
      hasConflicts,
      conflictCount,
      mergedContent: outputLines.join('\n'),
    };
  }
}

7. Real-World Case Studies in Branching & Merges#

Case Study 1: The Linux Kernel's 20-Parent Octopus Merges#

During the Linux kernel merge window, Linus Torvalds merges hundreds of discrete git branches submitted by regional subsystem maintainers (networking, memory management, filesystems, ARM drivers).

  • The Technique: Rather than executing dozens of sequential pairwise merges that pollute the commit log with endless two-parent merge bubbles, Torvalds frequently utilizes Octopus Merges (git merge -s octopus branch1 branch2 branch3 ...).
  • An octopus merge synthesizes a single commit node that possesses 5, 10, or 20 distinct parent commit pointers.
  • Strict Operational Constraint: Git's octopus merge strategy refuses to resolve complex conflicts automatically. If any two branches modify overlapping files, the entire octopus merge aborts, requiring the maintainer to resolve the conflict manually in an isolated branch prior to integration.

Case Study 2: The "Ghost Semantic Conflict" in Production#

A fintech team maintained two concurrent branches:

  • Branch A refactored an internal payment helper:

function processPayment(userId: string, amountCents: number): void

Renaming it to:

function processPayment(account: UserAccount, amountCents: number): void

  • Branch B created a new recurring subscription handler and invoked the old signature:

processPayment("usr_123", 4500);

  • The Disaster:

Because Branch A modified paymentHelper.ts and Branch B modified subscriptionEngine.ts, Git's line-based merge engine encountered zero line collisions. The merge succeeded cleanly without a single conflict marker.

When deployed to production, the runtime failed because the newly introduced call in Branch B passed a string to a function expecting a UserAccount object.

  • The Architectural Remedy:

Line-based text merging guarantees syntactic isolation, but cannot guarantee semantic correctness. Modern continuous integration pipelines enforce strict pre-merge compilation, automated integration test suites, and semantic AST-aware linters prior to completing pull request merges.

Case Study 3: The Broken Main Branch & `git push --force-with-lease`#

A senior engineer completed a complex rebase of a release candidate branch. They intended to push the rebased branch to GitHub, executing:

bash
git push --force origin release-v2.1

Unbeknownst to the engineer, a colleague had pushed a critical hotfix commit (Hotfix #412) to origin/release-v2.1 five minutes earlier. Because --force unconditionally forces the remote repository reference to match the local branch pointer, the colleague's commit was instantly wiped from the remote branch, breaking the staging deployment.

  • The Architectural Solution:

Never use bare --force. Use --force-with-lease:

bash
git push --force-with-lease origin release-v2.1

With --force-with-lease, Git checks whether the remote tracking ref (refs/remotes/origin/release-v2.1) matches the actual state on the remote server before overwriting. If another engineer pushed new commits in the interim, Git rejects the push with a safety lock, preventing accidental destruction of colleagues' work.


8. Common Branching & Merging Pitfalls#

1. Long-Lived Feature Branches ("Merge Hell")#

Allowing a feature branch to survive unmerged for multiple months guarantees severe merge conflicts. As the main trunk evolves, the architectural distance between the branch and trunk widens exponentially.

  • Remedy: Integrate continuously via trunk-based development, or rebase feature branches against origin/main daily to resolve incremental conflicts in small, manageable chunks.

2. Blindly Accepting "Ours" or "Theirs" During Complex Conflicts#

When exhausted by dozens of conflict markers, developers frequently resort to:

bash
git checkout --ours .
# or
git checkout --theirs .

This brute-force command unconditionally accepts one side of every conflict across the entire repository. This frequently reverts critical security patches, breaks imported module references, and injects runtime regressions.

3. Merging Instead of Rebasing (or Vice Versa) Inconsistently#

Projects that lack clear branching governance devolve into tangled "railroad track" commit histories filled with redundant merge commits (e.g., "Merge branch 'main' into feature" every few hours). Establish an explicit repository standard: either rebase feature branches before merging, or enforce squash-and-merge at the pull request boundary.


9. Checkpoint Practice Problems with Full Step-by-Step Solutions#

Problem 1: Lowest Common Ancestor (LCA) Graph Traversal & Merge Base Identification#

Scenario:

Consider the following Directed Acyclic Graph (DAG) representing repository commit history. Each commit is denoted by an uppercase letter, and arrows represent parent pointers (e.g., B -> A means commit B has parent A):

text
       C ---> D ---> E (main)
      /
A ---> B
      \
       F ---> G (feature)

Task:

  1. Identify the Lowest Common Ancestor (LCA) between main (commit E) and feature (commit G).
  2. List all commits that will be included in the diff comparison when executing git diff main...feature (the three-dot diff notation).
  3. If an engineer executes git merge feature while on main, state the two parent commits of the resulting merge commit M.

Solution:

Step 1: Identify the Lowest Common Ancestor (LCA)

To find the LCA (the merge base), trace the backward ancestry paths of both branch tips to locate their earliest shared intersection:

  • Ancestry of E (main): {E, D, C, B, A}
  • Ancestry of G (feature): {G, F, B, A}
  • Common Ancestors: {B, A}

The lowest (most recent) common ancestor in the topological ordering is Commit `B`.

Therefore: git merge-base main feature outputs the hash of Commit `B`.

Step 2: Evaluate the Three-Dot Diff (`main...feature`)

The three-dot diff notation git diff A...B computes the diff between the merge base of A and B and the tip of B:

  • Merge Base = B
  • Tip of feature = G

Therefore, git diff main...feature displays only the modifications introduced between commit B and commit G (the changes introduced by commits F and G). It completely excludes changes introduced on main (commits C, D, E).

Step 3: Determine Parents of Merge Commit `M`

When git merge feature is executed while checked out on main:

  • Parent 1 (current HEAD): Commit `E`
  • Parent 2 (incoming branch): Commit `G`

Merge commit M will have parent list: [E, G].


Problem 2: Three-Way Line-by-Line Diff Evaluation & Conflict Marker Synthesis#

Scenario:

During a merge, Git analyzes line 42 of a configuration file env.config.

The contents of line 42 across the three trees are:

  • Base (LCA): PORT=3000
  • Ours (main): PORT=8080
  • Theirs (feature): PORT=9000

Directly adjacent at line 43, the contents are:

  • Base (LCA): DEBUG=false
  • Ours (main): DEBUG=false
  • Theirs (feature): DEBUG=true

Task:

  1. Apply the three-way merge decision matrix to Line 42 and Line 43.
  2. Determine whether a conflict occurs on either line.
  3. Write the exact text output that Git will write to the working file in the event of any conflict.

Solution:

Step 1: Apply Decision Matrix to Line 42

  • Base: PORT=3000
  • Ours: PORT=8080 (Modified relative to Base: 3000 -> 8080)
  • Theirs: PORT=9000 (Modified relative to Base: 3000 -> 9000)
  • Analysis: Both branches modified the same line relative to the base, and their modifications differ (8080 != 9000).
  • Verdict: Line 42 triggers a Merge Conflict.

Step 2: Apply Decision Matrix to Line 43

  • Base: DEBUG=false
  • Ours: DEBUG=false (Unchanged relative to Base)
  • Theirs: DEBUG=true (Modified relative to Base: false -> true)
  • Analysis: Ours made no change. Theirs made an intentional modification.
  • Verdict: Clean automatic merge. Git accepts Theirs (DEBUG=true).

Step 3: Synthesize Working Tree Output

Git injects standard conflict markers around the conflicting hunk on Line 42, followed cleanly by the merged Line 43:

text
<<<<<<< HEAD
PORT=8080
=======
PORT=9000
>>>>>>> feature
DEBUG=true

Problem 3: Rebase History Replay & SHA-1 Mutation Verification#

Scenario:

A repository history consists of:

  • main: Commit M1 -> Commit M2 (hash: 2222...)
  • feature (branched from M1): Commit F1 (hash: aaaa...) -> Commit F2 (hash: bbbb...)

An engineer executes:

bash
git switch feature
git rebase main

Assume no merge conflicts occur during the rebase.

Task:

  1. Describe the exact step-by-step state changes that Git executes during this operation.
  2. Explain why the resulting commits F1' and F2' receive brand new SHA-1 hashes, even though their file diffs and author timestamps remain identical.
  3. State what happens to the original commit objects F1 (aaaa...) and F2 (bbbb...) in .git/objects.

Solution:

Step 1: Step-by-Step Rebase Execution

  1. Git identifies the common ancestor of main and feature: M1.
  2. Git computes the commits in feature not in main: [F1, F2].
  3. Git extracts the diff patches for F1 and F2 and saves them in memory/temporary storage.
  4. Git resets HEAD and the working tree to the tip of main (Commit M2).
  5. Git applies the patch of F1 onto M2, generating a new commit F1'.
  6. Git applies the patch of F2 onto F1', generating a new commit F2'.
  7. Git updates the branch pointer .git/refs/heads/feature to point to F2'.

Step 2: Mathematical Proof of SHA-1 Hash Mutation

A Git commit object's SHA-1 is computed from its serialized text representation:

commit [length]\0tree [treeSHA]\nparent [parentSHA]\nauthor ...\ncommitter ...\n\n[message]

  • In the original commit F1, the parent line was:

parent [M1_SHA]

  • In the rebased commit F1', the parent line is:

parent 2222... (the SHA-1 of M2)

  • In addition, the committer timestamp is updated to the current time of the rebase operation.

Because the input byte stream to the SHA-1 function has changed, the resulting cryptographic hash for F1' must be completely distinct from F1.

Similarly, because F2' now has parent F1' (which has a new hash), F2' also receives a completely new cryptographic hash.

Step 3: Fate of Original Commit Objects F1 and F2

The original loose objects aaaa... (F1) and bbbb... (F2) are not deleted immediately.

  • They remain in .git/objects as unreachable dangling objects.
  • Their hashes remain accessible via the local reference log (git reflog) for default retention windows (typically 90 days for reachable refs, 30 days for unreachable refs).
  • Only when git gc runs after the reflog expiration window will Git prune and permanently delete those loose objects from disk.

Problem 4: Git Rerere Cache Signature Formulation & Resolution Automation#

Scenario:

An engineer rebases a long-lived feature branch containing 8 commits onto main. Commits 2, 4, and 7 all collide with the same recurring database migration configuration change in db/config.yml.

The engineer has enabled git config --global rerere.enabled true.

Task:

  1. Explain what internal artifacts Git writes to .git/rr-cache/ when Commit 2 encounters the initial conflict.
  2. Describe what happens when the engineer manually resolves the conflict in Commit 2 and stages the file.
  3. Detail how Git automatically handles the subsequent conflict in Commit 4 without human manual editing.

Solution:

Step 1: Conflict Fingerprinting in `.git/rr-cache/`

When Commit 2 hits the merge conflict in db/config.yml:

  1. The rerere engine strips out all non-conflicting lines and extracts the raw conflict hunk (the pre-image containing the ancestor, ours, and theirs markers).
  2. It passes this normalized pre-image through SHA-1 to compute an identifier hash H (e.g., c4f19b...).
  3. It creates a directory: .git/rr-cache/c4f19b.../.
  4. Inside that directory, it writes a file named preimage containing the unmerged conflict hunk.

Step 2: Recording the Post-Image Resolution

The engineer opens db/config.yml, manually edits the file to reconcile the database configuration, and executes:

bash
git add db/config.yml
git rebase --continue
  1. Upon staging, rerere detects that the file previously had conflict markers associated with hash H.
  2. It compares the newly staged file against the preimage to determine how the engineer resolved the collision.
  3. It writes a file named postimage inside .git/rr-cache/c4f19b.../, recording the exact resolved replacement hunk.

Step 3: Automatic Resolution at Commit 4

When the rebase proceeds and reaches Commit 4, the exact same conflict in db/config.yml occurs:

  1. rerere immediately intercepts the conflict and normalizes the hunk into a pre-image.
  2. It computes the SHA-1 hash of the hunk, matching the previously recorded hash c4f19b....
  3. It checks .git/rr-cache/c4f19b.../ and finds the existing postimage.
  4. It automatically applies the postimage hunk directly into the working directory copy of db/config.yml, displaying the console message:

Resolved 'db/config.yml' using previous resolution.

  1. The engineer is spared from manually resolving the duplicate conflict for the third and fourth time.

Key Architectural Takeaways

  • Git branches are lightweight 41-byte text pointers containing a 40-character commit SHA-1.
  • Fast-forward merges simply advance the branch pointer without creating a merge commit.
  • A 3-way merge diffs both diverged branches against their Lowest Common Ancestor (Merge Base).
  • Rebasing replays commit patches onto a new base parent, producing new commit SHA-1 hashes.

Hands-on Code Playground Available

Execute, edit, and experiment with code for this topic in the live sandbox.

Practice Check • Quick Quiz

Test Your Understanding

What happens under the hood when you execute `git rebase main` while on a feature branch?

A
Git merges main into feature and creates a two-parent merge commit
B
Git computes the patch range main..feature, resets HEAD to main, and applies each commit patch sequentially onto the new base
C
Git overwrites main with feature without running conflict detection
D
Git deletes the feature branch and creates an annotated tag