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.
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,
HEADcontains a symbolic reference:ref: refs/heads/main. - When you switch branches via
git switch feature-login(or legacygit 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:
HEADpoints directly to the commit SHA-1 rather than to a branch reference inrefs/heads/.- If you make commits while in a detached
HEADstate, those commits are valid DAG nodes with parent pointers. However, no branch reference points to them. - If you switch back to
mainwithout creating a branch, those detached commits become unreachable DAG nodes (dangling commits). Unless rescued viagit branch rescue-branch <SHA>or recovered from the reference log (git reflog), they will eventually be pruned and permanently deleted bygit gcduring 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.
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
mainsincefeaturebranched off, Git does not need to perform diff algorithms or synthesize a merge commit. - Git simply advances the reference pointer of
mainto point to the latest commit offeature. - The Trade-off: Fast-forward merges keep history clean and linear, but they destroy the contextual encapsulation of the feature. Looking back at
git logsix 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:
- The Base Tree (`B`): The Lowest Common Ancestor (LCA) of
mainandfeature. - The Ours Tree (`O`): The snapshot of the target branch (
HEAD/main). - 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:
+-----------------------------------------------------------------------------------------+
| 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:
git merge-base main featureIn 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,
ortcompletely 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.
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:
- Git identifies the common ancestor commit:
LCA = git merge-base main feature. - Git computes the list of commits belonging exclusively to
feature:LCA..feature(commitsDandE). - Git stores the diffs of these commits in a temporary memory queue.
- Git resets
HEADto the tip ofmain(commitC). - Git iterates through the queued patches, applying each diff sequentially onto the new base.
- For each applied patch, Git writes a brand new commit object. Even if the code diff is identical, the commit's
parentpointer has changed fromBtoC. By virtue of the Merkle DAG cascading property, changing the parent hash completely changes the commit's SHA-1 cryptographic identifier! CommitsD'andE'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:
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 statementsSupported 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.
<<<<<<< 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:
# 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.tsProgrammatic Conflict Resolution Workflow#
- Inspect the conflict markers inside the working tree file.
- Edit the file to produce the correct unified logic.
- Remove all conflict markers (
<<<<<<<,=======,>>>>>>>). - 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).
- Execute
git commit(orgit 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):
git config --global rerere.enabled trueWhen enabled, Git:
- Records a fingerprint (pre-image) of any unresolved conflict hunk in
.git/rr-cache/. - When the user successfully resolves the conflict, Git records the post-image resolution.
- 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.
+-----------------------------------------------------------------------------------------+
| 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 fromreleaseandhotfixbranches.develop: The primary integration branch for ongoing development. Feature branches fork fromdevelopand merge back intodevelop.release/*: Cut fromdevelopwhen features are frozen for testing; merges into bothmainanddevelop.hotfix/*: Cut directly frommainto 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:
- Anything in the
mainbranch is deployable to production at all times. - To work on something new, create a descriptive branch off of
main(e.g.,feature/user-auth). - Commit locally and regularly push your work to the identically named branch on the remote server.
- Open a Pull Request (PR) to request peer review, run CI test suites, and discuss implementation.
- Once approved and green in CI, merge into
mainand 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 intomainmultiple times per day (lifespans under 24 hours). - Feature Flags / Toggles: How do engineers merge incomplete features into
mainwithout 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.
// 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:
git push --force origin release-v2.1Unbeknownst 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:
git push --force-with-lease origin release-v2.1With --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/maindaily 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:
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):
C ---> D ---> E (main)
/
A ---> B
\
F ---> G (feature)Task:
- Identify the Lowest Common Ancestor (LCA) between
main(commitE) andfeature(commitG). - List all commits that will be included in the diff comparison when executing
git diff main...feature(the three-dot diff notation). - If an engineer executes
git merge featurewhile onmain, state the two parent commits of the resulting merge commitM.
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:
- Apply the three-way merge decision matrix to Line 42 and Line 43.
- Determine whether a conflict occurs on either line.
- 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=3000Ours: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=falseOurs:DEBUG=false(Unchanged relative to Base)Theirs:DEBUG=true(Modified relative to Base:false -> true)- Analysis:
Oursmade no change.Theirsmade 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:
<<<<<<< HEAD
PORT=8080
=======
PORT=9000
>>>>>>> feature
DEBUG=trueProblem 3: Rebase History Replay & SHA-1 Mutation Verification#
Scenario:
A repository history consists of:
main: CommitM1-> CommitM2(hash:2222...)feature(branched fromM1): CommitF1(hash:aaaa...) -> CommitF2(hash:bbbb...)
An engineer executes:
git switch feature
git rebase mainAssume no merge conflicts occur during the rebase.
Task:
- Describe the exact step-by-step state changes that Git executes during this operation.
- Explain why the resulting commits
F1'andF2'receive brand new SHA-1 hashes, even though their file diffs and author timestamps remain identical. - State what happens to the original commit objects
F1(aaaa...) andF2(bbbb...) in.git/objects.
Solution:
Step 1: Step-by-Step Rebase Execution
- Git identifies the common ancestor of
mainandfeature:M1. - Git computes the commits in
featurenot inmain:[F1, F2]. - Git extracts the diff patches for
F1andF2and saves them in memory/temporary storage. - Git resets
HEADand the working tree to the tip ofmain(CommitM2). - Git applies the patch of
F1ontoM2, generating a new commitF1'. - Git applies the patch of
F2ontoF1', generating a new commitF2'. - Git updates the branch pointer
.git/refs/heads/featureto point toF2'.
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, theparentline was:
parent [M1_SHA]
- In the rebased commit
F1', theparentline 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/objectsas 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 gcruns 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:
- Explain what internal artifacts Git writes to
.git/rr-cache/when Commit 2 encounters the initial conflict. - Describe what happens when the engineer manually resolves the conflict in Commit 2 and stages the file.
- 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:
- The
rerereengine strips out all non-conflicting lines and extracts the raw conflict hunk (the pre-image containing the ancestor, ours, and theirs markers). - It passes this normalized pre-image through SHA-1 to compute an identifier hash
H(e.g.,c4f19b...). - It creates a directory:
.git/rr-cache/c4f19b.../. - Inside that directory, it writes a file named
preimagecontaining 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:
git add db/config.yml
git rebase --continue- Upon staging,
rereredetects that the file previously had conflict markers associated with hashH. - It compares the newly staged file against the
preimageto determine how the engineer resolved the collision. - It writes a file named
postimageinside.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:
rerereimmediately intercepts the conflict and normalizes the hunk into a pre-image.- It computes the SHA-1 hash of the hunk, matching the previously recorded hash
c4f19b.... - It checks
.git/rr-cache/c4f19b.../and finds the existingpostimage. - It automatically applies the
postimagehunk directly into the working directory copy ofdb/config.yml, displaying the console message:
Resolved 'db/config.yml' using previous resolution.
- The engineer is spared from manually resolving the duplicate conflict for the third and fourth time.