Git & GitHub Module 10 – Git internals
How Git computes diffs and merges
See how Git turns two snapshots into a diff with Myers or histogram, finds renames, merges from a merge base, and previews merges with git merge-tree.
What you will learn
- Compare the Myers, patience and histogram diff algorithms on the same change
- Explain rename detection and its similarity threshold
- Trace a three-way merge from the merge base and handle criss-cross histories
- Compute a merge without touching the working tree with git merge-tree --write-tree
- Find manual changes inside a merge commit with git log --remerge-diff
Before you start
What you’ll learn · Before you start
What you will learn
- Compare the Myers, patience and histogram diff algorithms on the same change
- Explain rename detection and its similarity threshold
- Trace a three-way merge from the merge base and handle criss-cross histories
- Compute a merge without touching the working tree with git merge-tree --write-tree
- Find manual changes inside a merge commit with git log --remerge-diff
Before you start
On this page
Git stores snapshots, not changes, so every diff you read is computed when you ask for it. git diff compares two
trees to find the paths that changed, pairs deleted and added files that are similar enough into renames, and then
runs a line diff algorithm on each changed file: Myers by default, which looks for the fewest deleted and added
lines, or minimal, patience or histogram, which can describe the same change differently. A merge
compares three snapshots: the merge base, the newest commit both branches share, and the two branch tips. Wherever
only one side changed something since the base, that change wins; where both sides changed the same region
differently, there is a conflict. This lesson runs each step in real sessions: four algorithms on one change, rename
detection at its threshold, five small merges to predict, a criss-cross history with two merge bases, a merge
computed without a working tree, and a merge commit that quietly dropped a change.
From two snapshots to a list of changes
A commit records a tree, so a diff starts by comparing two trees entry by entry. An entry with the same ID on both sides holds the same content, so Git can skip it, folder and all, without reading it. What is left is a list of modified, added and deleted paths; rename detection then pairs some deletions with additions, and only after that does a line algorithm look inside each changed file.
The steps from two snapshots to a patch
Text description of the diagram
The diagram shows the four steps git diff takes, from top to bottom.
- It starts from two snapshots: the root trees of the two commits being compared.
- It compares the trees entry by entry. An entry whose ID is the same in both trees is skipped whole, folder and all; the rest become a list of modified, added and deleted paths.
- It compares deleted paths with added paths and pairs those that are similar enough into renames, and, when asked, finds copies. The options -M and -C and the setting diff.renameLimit control this step.
- For each changed file, a line diff algorithm, myers by default, or minimal, patience or histogram, finds which lines to delete and which to add.
- The result is the patch you read: for each file, hunks of deleted lines marked with a minus and added lines marked with a plus.
Two consequences follow. A rename is never stored anywhere, so whether you see one depends on the options of the command that computes the diff. And the line algorithm is a choice too: the same two snapshots can produce different diffs that are all correct.
Four line-diff algorithms on one change
The billing module of a canteen has five functions. A programmer moved receipt() below tax() and, in the same
commit, changed two of its lines to align the columns. Here are the two versions, then four algorithms on that
commit:
Python (billing_v1.py) · billing_v1.py
MENU = {"idli": 30, "vada": 20, "chai": 10}
def price(item):
return MENU[item]
def total(order):
amount = 0
for item in order:
amount += price(item)
return amount
def receipt(order):
lines = []
for item in order:
lines.append(f"{item}: {price(item)}")
lines.append(f"Total: {total(order)}")
return "\n".join(lines)
def tax(order):
amount = total(order)
return round(amount * 0.05)
def count(order):
amount = 0
for item in order:
amount += 1
return amount Python (billing_v2.py) · billing_v2.py
MENU = {"idli": 30, "vada": 20, "chai": 10}
def price(item):
return MENU[item]
def total(order):
amount = 0
for item in order:
amount += price(item)
return amount
def tax(order):
amount = total(order)
return round(amount * 0.05)
def receipt(order):
lines = []
for item in order:
lines.append(f"{item:<6}{price(item):>4}")
lines.append(f"{'Total':<6}{total(order):>4}")
return "\n".join(lines)
def count(order):
amount = 0
for item in order:
amount += 1
return amount from gitsession import session
session(r"""
git config set --global user.name "Ravi Kumar"
git config set --global user.email "ravi@example.com"
git init -q -b main canteen
cd canteen
cp ../../billing_v1.py billing.py
git add billing.py
git commit -q -m "Billing helpers"
cp ../../billing_v2.py billing.py
git commit -q -am "Move receipt below tax and align its columns"
# The same two snapshots under four algorithms: lines added, lines deleted
for algo in myers minimal patience histogram; do printf '%-10s' $algo; git diff --numstat --diff-algorithm=$algo HEAD~1 HEAD | cut -f1,2; done
git diff --diff-algorithm=myers HEAD~1 HEAD
git diff --diff-algorithm=histogram HEAD~1 HEAD
# --anchored keeps a line that occurs once on each side from appearing as deleted and added
git diff --numstat --anchored='def tax(order):' HEAD~1 HEAD
# --color-moved gives moved lines colours of their own; show_colors.py names them in words
git diff --color=always --color-moved=plain HEAD~1 HEAD | python3 ../../show_colors.py
""") Output
$ git config set --global user.name "Ravi Kumar"
$ git config set --global user.email "ravi@example.com"
$ git init -q -b main canteen
$ cd canteen
$ cp ../../billing_v1.py billing.py
$ git add billing.py
$ git commit -q -m "Billing helpers"
$ cp ../../billing_v2.py billing.py
$ git commit -q -am "Move receipt below tax and align its columns"
# The same two snapshots under four algorithms: lines added, lines deleted
$ for algo in myers minimal patience histogram; do printf '%-10s' $algo; git diff --numstat --diff-algorithm=$algo HEAD~1 HEAD | cut -f1,2; done
myers 7 7
minimal 7 7
patience 8 8
histogram 8 8
$ git diff --diff-algorithm=myers HEAD~1 HEAD
diff --git a/billing.py b/billing.py
index 76d8fcd..e7d2b4f 100644
--- a/billing.py
+++ b/billing.py
@@ -12,19 +12,19 @@ def total(order):
return amount
+def tax(order):
+ amount = total(order)
+ return round(amount * 0.05)
+
+
def receipt(order):
lines = []
for item in order:
- lines.append(f"{item}: {price(item)}")
- lines.append(f"Total: {total(order)}")
+ lines.append(f"{item:<6}{price(item):>4}")
+ lines.append(f"{'Total':<6}{total(order):>4}")
return "\n".join(lines)
-def tax(order):
- amount = total(order)
- return round(amount * 0.05)
-
-
def count(order):
amount = 0
for item in order:
$ git diff --diff-algorithm=histogram HEAD~1 HEAD
diff --git a/billing.py b/billing.py
index 76d8fcd..e7d2b4f 100644
--- a/billing.py
+++ b/billing.py
@@ -12,19 +12,19 @@ def total(order):
return amount
-def receipt(order):
- lines = []
- for item in order:
- lines.append(f"{item}: {price(item)}")
- lines.append(f"Total: {total(order)}")
- return "\n".join(lines)
-
-
def tax(order):
amount = total(order)
return round(amount * 0.05)
+def receipt(order):
+ lines = []
+ for item in order:
+ lines.append(f"{item:<6}{price(item):>4}")
+ lines.append(f"{'Total':<6}{total(order):>4}")
+ return "\n".join(lines)
+
+
def count(order):
amount = 0
for item in order:
# --anchored keeps a line that occurs once on each side from appearing as deleted and added
$ git diff --numstat --anchored='def tax(order):' HEAD~1 HEAD
8 8 billing.py
# --color-moved gives moved lines colours of their own; show_colors.py names them in words
$ git diff --color=always --color-moved=plain HEAD~1 HEAD | python3 ../../show_colors.py
diff --git a/billing.py b/billing.py
index 76d8fcd..e7d2b4f 100644
--- a/billing.py
+++ b/billing.py
@@ -12,19 +12,19 @@ def total(order):
return amount
moved to: +def tax(order):
moved to: + amount = total(order)
moved to: + return round(amount * 0.05)
moved to: +
moved to: +
def receipt(order):
lines = []
for item in order:
deleted: - lines.append(f"{item}: {price(item)}")
deleted: - lines.append(f"Total: {total(order)}")
added: + lines.append(f"{item:<6}{price(item):>4}")
added: + lines.append(f"{'Total':<6}{total(order):>4}")
return "\n".join(lines)
moved from: -def tax(order):
moved from: - amount = total(order)
moved from: - return round(amount * 0.05)
moved from: -
moved from: -
def count(order):
amount = 0
for item in order:
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 algorithms.py
Myers and minimal deleted 7 lines and added 7, 14 in all; patience and histogram needed 16. Both diffs are correct,
and they tell different stories. Myers left receipt() where it was, with its two edited lines as the only change
inside it, and reported that tax() moved up: five lines, counting its two blank lines. Histogram reported what the
programmer actually did: all of receipt() moved down, edits included.
- Myers is the default. It finds a shortest edit script, the fewest deleted plus added lines, after Eugene Myers’s paper “An O(ND) Difference Algorithm and its Variations”. On very large inputs Git cuts the search short with heuristics and accepts a result that may not be the shortest.
- minimal is Myers without those shortcuts: it spends extra time to guarantee the smallest diff. Git 2.50 fixed a
case in which
--minimalcould still give a non-minimal result. - patience first matches the lines that occur exactly once in both versions, such as
def tax(order):, keeps the longest run of such pairs that appear in the same order in both files, and then works recursively between them, falling back to Myers where no unique lines are left. - histogram extends patience to lines that are rare but not unique. It is the algorithm that the
ortmerge strategy uses by default, which matters in the next sections.
Which to use? Myers is fine for most edits and is what git diff uses unless told otherwise. Minimal is for the rare
case where the size of the diff really matters, such as a patch you will edit by hand, and it can be slow on large
files. Patience and histogram suit code, where shared lines such as braces and blank lines can mislead Myers;
histogram is a common choice for reviews, and ort already merges with it.
The last two commands of the session show tools for steering the result. --anchored='def tax(order):' asks Git
never to show that line as deleted and added, which gave the histogram-like 16-line diff. And --color-moved colours
lines that moved differently from lines that changed; show_colors.py names the colours in words. With it, even the
Myers diff reads correctly: the five lines of tax() are marked as moved, and only the two edited lines of
receipt() stand out as real changes.
To make an algorithm your default, set diff.algorithm, for example to histogram; since Git 2.53, git blame also
accepts --diff-algorithm, so that it matches lines the way your diffs do.
Renames are found, not stored
Because commits record snapshots, Git finds a rename by comparing each deleted path with each added one and measuring how similar their contents are:
from gitsession import session
session(r"""
git config set --global user.name "Ravi Kumar"
git config set --global user.email "ravi@example.com"
git init -q -b main canteen
cd canteen
printf 'idli 30\nvada 20\nchai 10\ndosa 40\nupma 25\npoha 20\nsambar 15\nchutney 5\n' > menu.txt
git add .
git commit -q -m "Base menu"
# Rename the file and change one line of eight
git mv menu.txt breakfast.txt
printf 'idli 30\nvada 20\nchai 12\ndosa 40\nupma 25\npoha 20\nsambar 15\nchutney 5\n' > breakfast.txt
git add -A
git diff --staged --name-status
git diff --staged | head -n 4
# Above the threshold it is a rename; demand 90 % and it becomes a deletion and an addition
git diff --staged -M90% --name-status
git diff --staged --no-renames --name-status
git commit -q -m "Rename and edit"
git log --oneline --name-status --follow -- breakfast.txt
# Copies are found only when asked, and an unchanged source only with --find-copies-harder
cp breakfast.txt lunch.txt
git add lunch.txt
git diff --staged --name-status -C
git diff --staged --name-status -C --find-copies-harder
git commit -q -m "Copy for lunch"
# Rename detection compares every deleted file with every added one, so Git caps the work
for i in 1 2 3; do printf "item $i\nprice $i\nnote $i\n" > a$i.txt; done
git add . && git commit -q -m "Three files"
for i in 1 2 3; do git mv a$i.txt b$i.txt; printf "extra\n" >> b$i.txt; done
git add -A
git diff --staged --name-status -l1
""") Output
$ git config set --global user.name "Ravi Kumar" $ git config set --global user.email "ravi@example.com" $ git init -q -b main canteen $ cd canteen $ printf 'idli 30\nvada 20\nchai 10\ndosa 40\nupma 25\npoha 20\nsambar 15\nchutney 5\n' > menu.txt $ git add . $ git commit -q -m "Base menu" # Rename the file and change one line of eight $ git mv menu.txt breakfast.txt $ printf 'idli 30\nvada 20\nchai 12\ndosa 40\nupma 25\npoha 20\nsambar 15\nchutney 5\n' > breakfast.txt $ git add -A $ git diff --staged --name-status R088 menu.txt breakfast.txt $ git diff --staged | head -n 4 diff --git a/menu.txt b/breakfast.txt similarity index 88% rename from menu.txt rename to breakfast.txt # Above the threshold it is a rename; demand 90 % and it becomes a deletion and an addition $ git diff --staged -M90% --name-status A breakfast.txt D menu.txt $ git diff --staged --no-renames --name-status A breakfast.txt D menu.txt $ git commit -q -m "Rename and edit" $ git log --oneline --name-status --follow -- breakfast.txt 2a5d9ec Rename and edit R088 menu.txt breakfast.txt 43a53fb Base menu A menu.txt # Copies are found only when asked, and an unchanged source only with --find-copies-harder $ cp breakfast.txt lunch.txt $ git add lunch.txt $ git diff --staged --name-status -C A lunch.txt $ git diff --staged --name-status -C --find-copies-harder C100 breakfast.txt lunch.txt $ git commit -q -m "Copy for lunch" # Rename detection compares every deleted file with every added one, so Git caps the work $ for i in 1 2 3; do printf "item $i\nprice $i\nnote $i\n" > a$i.txt; done $ git add . && git commit -q -m "Three files" $ for i in 1 2 3; do git mv a$i.txt b$i.txt; printf "extra\n" >> b$i.txt; done $ git add -A $ git diff --staged --name-status -l1 D a1.txt D a2.txt D a3.txt A b1.txt A b2.txt A b3.txt warning: exhaustive rename detection was skipped due to too many files. warning: you may want to set your diff.renameLimit variable to at least 3 and retry the command.
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 renames.py
menu.txt became breakfast.txt with one of its eight lines changed, and --name-status printed R088: a rename
with a similarity index of 88 %. The similarity index measures how much of the file is unchanged, and the default
threshold is 50 %. With -M90% the same pair fell below the threshold and became D and A, which is also what
--no-renames shows. git log --follow uses the same detection to keep a file’s history across a rename.
Copies are found only when you ask. -C looks for copy sources among the files modified in the same change, so a
copy of an unchanged file stayed a plain A; --find-copies-harder also considers unmodified files and found
C100, a copy with 100 % similarity, at a cost that grows with the size of the project.
The last step shows the limit. Comparing every deleted path with every added path takes time proportional to their
product, so Git caps the exhaustive search: diff.renameLimit defaults to 1,000 files, and merges use
merge.renameLimit, 7,000 unless set. With -l1, three renamed files were reported as three deletions and three
additions, and Git printed the limit you would need. A refactor that moves thousands of files is exactly where the
limit bites, so raise it for that diff when the renames matter.
The three-way merge, region by region
A merge of two branches compares three versions of each file: the merge base and the two tips. A file that only one
side changed is taken from that side whole. A file that both sides changed is merged region by region, and
git merge-file runs that same file merge on three plain files. Before reading the output, predict each result:
from gitsession import session
session(r"""
mkdir merges
cd merges
printf 'idli 30\nvada 20\nchai 10\n' > base.txt
# 1. Two changes with an unchanged line between them: merged cleanly
printf 'idli 35\nvada 20\nchai 10\n' > ours.txt
printf 'idli 30\nvada 20\nchai 15\n' > theirs.txt
git merge-file -p ours.txt base.txt theirs.txt
# 2. Changes to neighbouring lines conflict, although no line was changed twice
printf 'idli 30\nvada 25\nchai 10\n' > theirs.txt
! git merge-file -p -L ours -L base -L theirs ours.txt base.txt theirs.txt
# The diff3 style adds the base's version between the two sides
! git merge-file -p --diff3 -L ours -L base -L theirs ours.txt base.txt theirs.txt
# 3. Both sides made the same change: merged cleanly
cp ours.txt theirs.txt
git merge-file -p ours.txt base.txt theirs.txt
# 4. Both sides added a different last line: a conflict
printf 'idli 30\nvada 20\nchai 10\ndosa 40\n' > ours.txt
printf 'idli 30\nvada 20\nchai 10\nupma 25\n' > theirs.txt
! git merge-file -p -L ours -L base -L theirs ours.txt base.txt theirs.txt
# 5. Ours raised idli to 35 and then reverted it; theirs raised it to 35. Only the end states count
cp base.txt ours.txt
printf 'idli 35\nvada 20\nchai 10\n' > theirs.txt
git merge-file -p ours.txt base.txt theirs.txt
""") Output
$ mkdir merges $ cd merges $ printf 'idli 30\nvada 20\nchai 10\n' > base.txt # 1. Two changes with an unchanged line between them: merged cleanly $ printf 'idli 35\nvada 20\nchai 10\n' > ours.txt $ printf 'idli 30\nvada 20\nchai 15\n' > theirs.txt $ git merge-file -p ours.txt base.txt theirs.txt idli 35 vada 20 chai 15 # 2. Changes to neighbouring lines conflict, although no line was changed twice $ printf 'idli 30\nvada 25\nchai 10\n' > theirs.txt $ git merge-file -p -L ours -L base -L theirs ours.txt base.txt theirs.txt <<<<<<< ours idli 35 vada 20 ======= idli 30 vada 25 >>>>>>> theirs chai 10 [exit status 1] # The diff3 style adds the base's version between the two sides $ git merge-file -p --diff3 -L ours -L base -L theirs ours.txt base.txt theirs.txt <<<<<<< ours idli 35 vada 20 ||||||| base idli 30 vada 20 ======= idli 30 vada 25 >>>>>>> theirs chai 10 [exit status 1] # 3. Both sides made the same change: merged cleanly $ cp ours.txt theirs.txt $ git merge-file -p ours.txt base.txt theirs.txt idli 35 vada 20 chai 10 # 4. Both sides added a different last line: a conflict $ printf 'idli 30\nvada 20\nchai 10\ndosa 40\n' > ours.txt $ printf 'idli 30\nvada 20\nchai 10\nupma 25\n' > theirs.txt $ git merge-file -p -L ours -L base -L theirs ours.txt base.txt theirs.txt idli 30 vada 20 chai 10 <<<<<<< ours dosa 40 ======= upma 25 >>>>>>> theirs [exit status 1] # 5. Ours raised idli to 35 and then reverted it; theirs raised it to 35. Only the end states count $ cp base.txt ours.txt $ printf 'idli 35\nvada 20\nchai 10\n' > theirs.txt $ git merge-file -p ours.txt base.txt theirs.txt idli 35 vada 20 chai 10
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 hunks.py
idlichanged on our side andchaion theirs, with the unchangedvadaline between them, so both changes arrived.idlichanged on our side andvadaon theirs. No line was changed by both, yet this is a conflict: with no unchanged line between them, the two changes form one region, and Git will not guess how they combine. Thediff3style, whichmerge.conflictStyle = diff3makes the default, adds the base’s version of the region, which makes such conflicts much easier to resolve.zdiff3does the same, and it also takes lines that both sides share at the start or the end of a conflict out of the conflicting region.- Both sides made the same change, so there is nothing to choose.
- Both sides added a different line at the end, and an addition at the same place on both sides conflicts.
- Ours had raised
idlito 35 and reverted it; theirs raised it to 35. The merge compares only the end states with the base, so ours looks unchanged and theirs wins: the reverted change comes back without a conflict. The Git documentation warns about exactly this. If you meant to keep the old value, check the merged file and change it again after the merge.
git merge-file exits with the number of conflicts, which is why the conflicting cases end with exit status 1.
Criss-cross histories and the virtual merge base
Usually two branches have one best common ancestor. When each branch has merged the other, they can have two, and choosing between them is not safe:
from gitsession import session
session(r"""
git config set --global user.name "Ravi Kumar"
git config set --global user.email "ravi@example.com"
git config set --global merge.conflictStyle diff3
git init -q -b main canteen
cd canteen
printf 'open 8\nidli 30\nclose 22\n' > menu.txt
git add menu.txt
git commit -q -m "Base"
git branch feature
printf 'open 8\nidli 32\nclose 22\n' > menu.txt
git commit -q -am "Idli 32"
git switch -q feature
printf 'open 8\nidli 35\nclose 22\n' > menu.txt
git commit -q -am "Idli 35"
# Each branch merges the other's commit and settles the conflict its own way
git switch -q main
! git merge -q feature
printf 'open 8\nidli 32\nclose 22\n' > menu.txt
git commit -q -am "Merge feature, keep 32"
git switch -q feature
! git merge -q main~1
printf 'open 8\nidli 35\nclose 22\n' > menu.txt
git commit -q -am "Merge main, keep 35"
git log --oneline --graph --all
# Two commits are equally good merge bases now
git merge-base --all main feature
# With either base alone the merge would look clean, and the two give opposite answers
git switch -q main
git show main:menu.txt > ../ours.txt
git show feature:menu.txt > ../theirs.txt
git show aba782f:menu.txt > ../base-a.txt
git show 2f1c5fb:menu.txt > ../base-b.txt
git merge-file -p ../ours.txt ../base-a.txt ../theirs.txt
git merge-file -p ../ours.txt ../base-b.txt ../theirs.txt
# ort first merges the two bases; their own conflict appears inside the merged common ancestors
! git merge feature
cat menu.txt
""") Output
$ git config set --global user.name "Ravi Kumar" $ git config set --global user.email "ravi@example.com" $ git config set --global merge.conflictStyle diff3 $ git init -q -b main canteen $ cd canteen $ printf 'open 8\nidli 30\nclose 22\n' > menu.txt $ git add menu.txt $ git commit -q -m "Base" $ git branch feature $ printf 'open 8\nidli 32\nclose 22\n' > menu.txt $ git commit -q -am "Idli 32" $ git switch -q feature $ printf 'open 8\nidli 35\nclose 22\n' > menu.txt $ git commit -q -am "Idli 35" # Each branch merges the other's commit and settles the conflict its own way $ git switch -q main $ git merge -q feature Auto-merging menu.txt CONFLICT (content): Merge conflict in menu.txt Automatic merge failed; fix conflicts and then commit the result. [exit status 1] $ printf 'open 8\nidli 32\nclose 22\n' > menu.txt $ git commit -q -am "Merge feature, keep 32" $ git switch -q feature $ git merge -q main~1 Auto-merging menu.txt CONFLICT (content): Merge conflict in menu.txt Automatic merge failed; fix conflicts and then commit the result. [exit status 1] $ printf 'open 8\nidli 35\nclose 22\n' > menu.txt $ git commit -q -am "Merge main, keep 35" $ git log --oneline --graph --all * c7651d7 Merge main, keep 35 |\ | | * c1641f4 Merge feature, keep 32 | |/| | |/ |/| * | 2f1c5fb Idli 35 | * aba782f Idli 32 |/ * 6a1b727 Base # Two commits are equally good merge bases now $ git merge-base --all main feature aba782fd50c8227c3734edf167b25451869a2a1c 2f1c5fb55c5cfc7d6d4090d03375e608565a54dd # With either base alone the merge would look clean, and the two give opposite answers $ git switch -q main $ git show main:menu.txt > ../ours.txt $ git show feature:menu.txt > ../theirs.txt $ git show aba782f:menu.txt > ../base-a.txt $ git show 2f1c5fb:menu.txt > ../base-b.txt $ git merge-file -p ../ours.txt ../base-a.txt ../theirs.txt open 8 idli 35 close 22 $ git merge-file -p ../ours.txt ../base-b.txt ../theirs.txt open 8 idli 32 close 22 # ort first merges the two bases; their own conflict appears inside the merged common ancestors $ git merge feature Auto-merging menu.txt CONFLICT (content): Merge conflict in menu.txt Automatic merge failed; fix conflicts and then commit the result. [exit status 1] $ cat menu.txt open 8 <<<<<<< HEAD idli 32 ||||||| merged common ancestors <<<<<<<<< Temporary merge branch 1 idli 35 ||||||||| 6a1b727 idli 30 ========= idli 32 >>>>>>>>> Temporary merge branch 2 ======= idli 35 >>>>>>> feature close 22
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 crisscross.py
main raised idli to 32 and feature to 35, and then each merged the other’s commit and kept its own price. Both
of those commits, aba782f and 2f1c5fb, are now common ancestors of the two tips, and neither is an ancestor of the
other, so git merge-base --all printed both.
A criss-cross history with two merge bases
Text description of the diagram
The diagram shows the history of the criss-cross session, oldest commit at the bottom.
- 6a1b727, "Base", has idli at 30.
- Two commits were made on top of it: aba782f, "Idli 32", on main, and 2f1c5fb, "Idli 35", on feature. Both are drawn with double borders because they become the merge bases.
- c1641f4 on main merged feature's commit and kept 32. Its parents are aba782f and 2f1c5fb.
- c7651d7 on feature merged main's first commit and kept 35. Its parents are 2f1c5fb and aba782f.
Both aba782f and 2f1c5fb are reachable from both tips, and neither is reachable from the other, so git merge-base --all prints both: there is no single best common ancestor.
The two git merge-file lines show the danger. With aba782f (32) as the base, our side looks unchanged and theirs
looks changed, so the merge quietly picks 35. With 2f1c5fb (35) as the base, it quietly picks 32. Either choice
gives a clean merge and a wrong one, because the two branches really disagree. The ort strategy, Git’s default since
2.34, does neither: it first merges the merge bases into a virtual merge base, using their own common ancestor
6a1b727, and merges the tips against that. The bases’ own disagreement became a conflict inside the virtual base,
which the diff3 style shows nested under merged common ancestors, and the final merge reported a conflict for you to
settle. It is the only honest answer.
Preview a merge without a working tree
A server that shows whether a pull request can merge has no working tree to merge in.
git merge-tree --write-tree, added in Git 2.38, performs the whole merge in memory and writes only objects:
from gitsession import session
session(r"""
git config set --global user.name "Ravi Kumar"
git config set --global user.email "ravi@example.com"
git init -q -b main canteen
cd canteen
printf 'idli 30\nvada 20\nchai 10\n' > menu.txt
git add menu.txt
git commit -q -m "Base menu"
git branch late
git switch -q -c feature
printf 'idli 30\nvada 20\nchai 15\n' > menu.txt
git commit -q -am "Chai 15"
git switch -q main
printf 'idli 35\nvada 20\nchai 10\n' > menu.txt
git commit -q -am "Idli 35"
# The merge is computed without touching the index or the files; the output is the merged tree's ID
git merge-tree --write-tree main feature
git cat-file -p aa709450422b9957acb8bca9f62337a3cc0cbdb2:menu.txt
git merge-tree --quiet main feature; echo "exit status $?"
git status --short
# Turn the tree into a merge commit and move main, still without a working tree
git commit-tree aa70945 -p main -p feature -m "Merge feature"
git update-ref refs/heads/main e5b0be580cc5c02f65d93999b3085e1eea96902e f41cd78d3bb58b06c4e15dd80891d8f9bb16e35e
git log --oneline --graph
# The files and the index still hold the old main; a server has no working tree, and here reset catches up
git status --short
git reset -q --hard
# A merge that conflicts: exit status 1, the conflicted entries with their stages, then the messages
git switch -q late
printf 'idli 30\nvada 22\nchai 10\n' > menu.txt
git commit -q -am "Vada 22"
! git merge-tree --write-tree main late
! git merge-tree --write-tree --name-only main late
# The tree it wrote holds the file with conflict markers, ready for someone to resolve
git cat-file -p 744ab29830f286bac96a81ce41473f4d1e189c7a:menu.txt
""") Output
$ git config set --global user.name "Ravi Kumar" $ git config set --global user.email "ravi@example.com" $ git init -q -b main canteen $ cd canteen $ printf 'idli 30\nvada 20\nchai 10\n' > menu.txt $ git add menu.txt $ git commit -q -m "Base menu" $ git branch late $ git switch -q -c feature $ printf 'idli 30\nvada 20\nchai 15\n' > menu.txt $ git commit -q -am "Chai 15" $ git switch -q main $ printf 'idli 35\nvada 20\nchai 10\n' > menu.txt $ git commit -q -am "Idli 35" # The merge is computed without touching the index or the files; the output is the merged tree's ID $ git merge-tree --write-tree main feature aa709450422b9957acb8bca9f62337a3cc0cbdb2 $ git cat-file -p aa709450422b9957acb8bca9f62337a3cc0cbdb2:menu.txt idli 35 vada 20 chai 15 $ git merge-tree --quiet main feature; echo "exit status $?" exit status 0 $ git status --short # Turn the tree into a merge commit and move main, still without a working tree $ git commit-tree aa70945 -p main -p feature -m "Merge feature" e5b0be580cc5c02f65d93999b3085e1eea96902e $ git update-ref refs/heads/main e5b0be580cc5c02f65d93999b3085e1eea96902e f41cd78d3bb58b06c4e15dd80891d8f9bb16e35e $ git log --oneline --graph * e5b0be5 Merge feature |\ | * 1144951 Chai 15 * | f41cd78 Idli 35 |/ * f6cbbc9 Base menu # The files and the index still hold the old main; a server has no working tree, and here reset catches up $ git status --short M menu.txt $ git reset -q --hard # A merge that conflicts: exit status 1, the conflicted entries with their stages, then the messages $ git switch -q late $ printf 'idli 30\nvada 22\nchai 10\n' > menu.txt $ git commit -q -am "Vada 22" $ git merge-tree --write-tree main late 744ab29830f286bac96a81ce41473f4d1e189c7a 100644 0cc7caaf7dc0c437bcc54bd321d4ce98e69741a5 1 menu.txt 100644 c6fd34a3244626f8bd83bca20f3a603378df0cce 2 menu.txt 100644 594d9cb645b62d6fa51462dc1168bcc1d3f4a99b 3 menu.txt Auto-merging menu.txt CONFLICT (content): Merge conflict in menu.txt [exit status 1] $ git merge-tree --write-tree --name-only main late 744ab29830f286bac96a81ce41473f4d1e189c7a menu.txt Auto-merging menu.txt CONFLICT (content): Merge conflict in menu.txt [exit status 1] # The tree it wrote holds the file with conflict markers, ready for someone to resolve $ git cat-file -p 744ab29830f286bac96a81ce41473f4d1e189c7a:menu.txt <<<<<<< main idli 35 vada 20 chai 15 ======= idli 30 vada 22 chai 10 >>>>>>> late
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 mergetree.py
For the clean merge, the output was one line, the ID of the merged tree, and git cat-file showed its menu.txt with
both changes. --quiet, added in Git 2.50, prints nothing and only sets the exit status, 0 for clean, and it may stop
early and skip writing most objects. git status showed that the files and the index were untouched.
git commit-tree then made a merge commit with both parents from that tree, and git update-ref moved main, the
same sequence the git merge-tree documentation gives. The checkout then lagged behind, which a server with a bare
repository never has to worry about.
The conflicting merge exited with status 1 and printed three sections: the tree, the conflicted entries with their
stage numbers (1 base, 2 ours, 3 theirs, as in the index), and the messages that git merge would have printed. The
tree it wrote holds menu.txt with conflict markers. The documentation’s advice is to rely on the exit status and the
conflicted-file list, never on searching that tree for markers, because some conflicts, such as one side deleting a
file the other changed, leave no markers at all.
Find the hand-made part of a merge
A merge commit can contain changes that the automatic merge did not make, sometimes called an evil merge: a fix made while resolving conflicts, or a change dropped by mistake. This session makes one of each:
from gitsession import session
session(r"""
git config set --global user.name "Ravi Kumar"
git config set --global user.email "ravi@example.com"
git init -q -b main canteen
cd canteen
printf 'idli 30\nvada 20\nchai 10\n' > menu.txt
printf 'open 8\nclose 22\n' > hours.txt
git add .
git commit -q -m "Base"
git switch -q -c feature
printf 'idli 30\nvada 20\nchai 15\n' > menu.txt
git commit -q -am "Chai 15"
git switch -q main
printf 'idli 35\nvada 20\nchai 10\n' > menu.txt
git commit -q -am "Idli 35"
# The merge itself is clean, but before committing, someone also changes the opening hour
git merge -q --no-commit feature
printf 'open 7\nclose 22\n' > hours.txt
git commit -q -am "Merge feature"
# The usual view of a merge shows every file that differs from both parents
git show --format='%h %s' HEAD
# --remerge-diff merges the parents again and shows only what differs from that automatic result
git show --format='%h %s' --remerge-diff HEAD
""") Output
$ git config set --global user.name "Ravi Kumar" $ git config set --global user.email "ravi@example.com" $ git init -q -b main canteen $ cd canteen $ printf 'idli 30\nvada 20\nchai 10\n' > menu.txt $ printf 'open 8\nclose 22\n' > hours.txt $ git add . $ git commit -q -m "Base" $ git switch -q -c feature $ printf 'idli 30\nvada 20\nchai 15\n' > menu.txt $ git commit -q -am "Chai 15" $ git switch -q main $ printf 'idli 35\nvada 20\nchai 10\n' > menu.txt $ git commit -q -am "Idli 35" # The merge itself is clean, but before committing, someone also changes the opening hour $ git merge -q --no-commit feature Auto-merging menu.txt Automatic merge went well; stopped before committing as requested $ printf 'open 7\nclose 22\n' > hours.txt $ git commit -q -am "Merge feature" # The usual view of a merge shows every file that differs from both parents $ git show --format='%h %s' HEAD a27df26 Merge feature diff --cc hours.txt index 1b2619f,1b2619f..ec21b45 --- a/hours.txt +++ b/hours.txt @@@ -1,2 -1,2 +1,2 @@@ --open 8 ++open 7 close 22 diff --cc menu.txt index 14a6fde,065cff3..c6fd34a --- a/menu.txt +++ b/menu.txt @@@ -1,3 -1,3 +1,3 @@@ -idli 30 +idli 35 vada 20 - chai 10 + chai 15 # --remerge-diff merges the parents again and shows only what differs from that automatic result $ git show --format='%h %s' --remerge-diff HEAD a27df26 Merge feature diff --git a/hours.txt b/hours.txt index 1b2619f..ec21b45 100644 --- a/hours.txt +++ b/hours.txt @@ -1,2 +1,2 @@ -open 8 +open 7 close 22
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 remerge.py
The automatic merge produced chai 15, but the person committing it put chai 10 back and also changed the opening
hour. The usual view of a merge, the dense combined diff of git show, lists only files that differ from every
parent. menu.txt now matches the first parent exactly, so it is not shown at all, and feature’s change has vanished
without a trace. --remerge-diff, added in Git 2.36, merges the parents again and diffs that automatic result against
the commit: it showed the edited hour and the dropped chai 15. Use it on git log or git show whenever a bug
appears in code that neither branch changed.
Strategy options: -X ours is not -s ours
Two spellings that look alike do very different things:
from gitsession import session
session(r"""
git config set --global user.name "Ravi Kumar"
git config set --global user.email "ravi@example.com"
git init -q -b main canteen
cd canteen
printf 'idli 30\nvada 20\nchai 10\n' > menu.txt
printf 'open 8\nclose 22\n' > hours.txt
git add .
git commit -q -m "Base"
git switch -q -c feature
printf 'idli 32\nvada 20\nchai 15\n' > menu.txt
printf 'open 7\nclose 22\n' > hours.txt
git commit -q -am "Feature prices and hours"
git switch -q main
printf 'idli 35\nvada 20\nchai 10\n' > menu.txt
git commit -q -am "Idli 35"
git branch before
# -X ours: only the conflicting idli line is settled in our favour; chai 15 and open 7 still arrive
git merge -q -X ours feature -m "Merge with -X ours"
git diff --stat before HEAD
cat menu.txt hours.txt
# -s ours: the result is our tree exactly, and feature's work is recorded as merged but thrown away
git reset -q --hard before
git merge -q -s ours feature -m "Merge with -s ours"
git diff --stat before HEAD
cat menu.txt hours.txt
git log --oneline --graph -3
""") Output
$ git config set --global user.name "Ravi Kumar" $ git config set --global user.email "ravi@example.com" $ git init -q -b main canteen $ cd canteen $ printf 'idli 30\nvada 20\nchai 10\n' > menu.txt $ printf 'open 8\nclose 22\n' > hours.txt $ git add . $ git commit -q -m "Base" $ git switch -q -c feature $ printf 'idli 32\nvada 20\nchai 15\n' > menu.txt $ printf 'open 7\nclose 22\n' > hours.txt $ git commit -q -am "Feature prices and hours" $ git switch -q main $ printf 'idli 35\nvada 20\nchai 10\n' > menu.txt $ git commit -q -am "Idli 35" $ git branch before # -X ours: only the conflicting idli line is settled in our favour; chai 15 and open 7 still arrive $ git merge -q -X ours feature -m "Merge with -X ours" Auto-merging menu.txt $ git diff --stat before HEAD hours.txt | 2 +- menu.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) $ cat menu.txt hours.txt idli 35 vada 20 chai 15 open 7 close 22 # -s ours: the result is our tree exactly, and feature's work is recorded as merged but thrown away $ git reset -q --hard before $ git merge -q -s ours feature -m "Merge with -s ours" $ git diff --stat before HEAD $ cat menu.txt hours.txt idli 35 vada 20 chai 10 open 8 close 22 $ git log --oneline --graph -3 * d480fce Merge with -s ours |\ | * c45598f Feature prices and hours * | b17dd0e Idli 35 |/
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 strategies.py
-X ours is an option of the ort strategy: in a conflicting region it takes our side, and every change of theirs
that does not conflict still arrives, here chai 15 and open 7. -s ours is a different strategy: the result is our
tree exactly, git diff --stat shows no change at all, and yet the commit has two parents, so Git records feature’s
work as merged while throwing it away. It exists for retiring an old branch, and it is almost never what someone
looking for “keep my version” wants. -X theirs mirrors -X ours; there is no -s theirs.
Practise: count the lines of a shortest diff
A shortest diff keeps the longest sequence of lines that both versions share in order and deletes or adds everything else. Count its size, and check yourself against the numbers Git printed, including the 14 lines of the billing change.
Exercise · Medium · Python
Count the lines of a shortest diff
A diff is a list of lines to delete and lines to add, and the shortest possible one is what git diff --minimal looks for. Write edit_size(old, new) that returns its size: the number of lines it deletes plus the number it adds, which is what the two numbers of git diff --minimal --numstat add up to.
old and new are lists of lines, without newlines. Every expected number in the tests was counted by Git 2.54 on the same lines, including the billing change of the lesson, where --minimal and Myers needed 14 lines and histogram 16. For example, changing the middle line of three gives 2: one line deleted, one added.
Starter code · edit_size.py
def edit_size(old, new):
"""Return how many lines a shortest diff from old to new deletes plus adds.
old and new are lists of lines without their newlines, such as ["idli 30", "vada 20"].
"""
# Replace this line with your code.
return -1 The sample tests · test_edit_size.py
from edit_size import edit_size
# Each expected number is the sum of the two numbers that git diff --minimal --numstat printed (Git 2.54).
MENU = ['MENU = {"idli": 30, "vada": 20, "chai": 10}']
PRICE = ["def price(item):", " return MENU[item]"]
TOTAL = ["def total(order):", " amount = 0", " for item in order:", " amount += price(item)", " return amount"]
TAX = ["def tax(order):", " amount = total(order)", " return round(amount * 0.05)"]
COUNT = ["def count(order):", " amount = 0", " for item in order:", " amount += 1", " return amount"]
RECEIPT_OLD = [
"def receipt(order):",
" lines = []",
" for item in order:",
' lines.append(f"{item}: {price(item)}")',
' lines.append(f"Total: {total(order)}")',
' return "\\n".join(lines)',
]
RECEIPT_NEW = [
"def receipt(order):",
" lines = []",
" for item in order:",
' lines.append(f"{item:<6}{price(item):>4}")',
" lines.append(f\"{'Total':<6}{total(order):>4}\")",
' return "\\n".join(lines)',
]
def billing(*functions):
"""billing.py of the lesson: the menu, then each function after two blank lines."""
lines = list(MENU)
for function in functions:
lines += ["", ""] + function
return lines
def test_same():
"""counts nothing for two equal versions"""
assert edit_size(["idli 30", "vada 20"], ["idli 30", "vada 20"]) == 0
def test_one_line_changed():
"""counts a changed line as one deletion and one addition"""
assert edit_size(["idli 30", "vada 20", "chai 10"], ["idli 30", "vada 25", "chai 10"]) == 2
def test_from_nothing():
"""counts every line of a new file"""
assert edit_size([], ["idli 30", "vada 20", "chai 10"]) == 3
def test_edit_and_append():
"""handles a change and an added last line together"""
assert edit_size(["idli 30", "vada 20", "chai 10", "dosa 40"], ["idli 30", "vada 25", "chai 10", "dosa 40", "upma 25"]) == 3
def test_repeated_lines():
"""keeps the longest run of shared lines when lines repeat"""
assert edit_size(["x", "y", "x", "y", "x"], ["y", "x", "y", "x", "y"]) == 2
def test_swapped_functions():
"""moves the shorter of two swapped blocks"""
old = ["def tax():", " return 5", "", "def tip():", " return 10", "", "def total():", " return 15"]
new = ["def tip():", " return 10", "", "def tax():", " return 5", "", "def total():", " return 15"]
assert edit_size(old, new) == 6
def test_billing_change():
"""finds the 14 lines that git diff --minimal needed for the billing change"""
old = billing(PRICE, TOTAL, RECEIPT_OLD, TAX, COUNT)
new = billing(PRICE, TOTAL, TAX, RECEIPT_NEW, COUNT)
assert edit_size(old, new) == 14 A hint
Every line that a shortest diff keeps belongs to the longest sequence of lines that both versions share in the same order, the longest common subsequence. Everything else is deleted from old or added from new, so the answer is len(old) + len(new) - 2 * common. A table of the common length for every pair of prefixes, filled row by row, finds common.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Check yourself
6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Run the sessions yourself
To repeat a session, place gitsession.py beside it; the algorithms session also needs billing_v1.py,
billing_v2.py and show_colors.py. Then run, say, python3 hunks.py with Python 3 and Git installed, on macOS or
Linux. The helper creates a throwaway home folder called learner, runs the session’s commands there one after
another, echoing each behind a $ sign, and gives every commit the same fixed timestamp, which is why your commit IDs
will equal the ones above.
"""Plays a terminal session with real Git: each command is printed after "$ ", then everything it printed.
The session runs in a new, empty folder called learner/, which is also its home folder, so your own Git settings
change nothing, and GIT_CONFIG_NOSYSTEM=1 makes Git skip the computer's system-wide settings file too. Git never
looks for a repository above that folder, so a session cannot touch a repository you run it from. In the output,
"~" stands for that folder. Commit times are fixed, because a commit's ID covers the time it was made: with the
same files, message, name and time, every run makes the same commit IDs.
Each line of a script is one /bin/sh command (macOS and Linux), except "cd", which the session does itself. A line
that starts with "# " is printed as a comment. A command written as "! command" is meant to fail: its exit status
is printed and the session goes on. Any other command that fails stops the session with exit status 1.
"""
import os
import shutil
import subprocess
import sys
from pathlib import Path
HOME = Path("learner").resolve()
# One fixed moment in Git's own date format: seconds since 1970, then the time zone (India, UTC+05:30).
FIXED_TIME = "@1767240000 +0530"
def _environment():
# Git's own folder comes first, so every command runs that Git directly (on macOS, /usr/bin/git is a small
# launcher that looks Git up again on every call).
git_folder = subprocess.run(["git", "--exec-path"], capture_output=True, text=True, check=True).stdout.strip()
env = {
"PATH": os.pathsep.join([git_folder, os.environ.get("PATH", "/usr/bin:/bin")]),
"HOME": str(HOME),
"XDG_CONFIG_HOME": str(HOME / ".config"),
"GIT_CONFIG_NOSYSTEM": "1",
"GIT_CEILING_DIRECTORIES": str(HOME.parent),
"GIT_AUTHOR_DATE": FIXED_TIME,
"GIT_COMMITTER_DATE": FIXED_TIME,
"LANG": "C",
"LC_ALL": "C",
"TERM": "dumb",
}
if "TMPDIR" in os.environ:
env["TMPDIR"] = os.environ["TMPDIR"]
return env
def _with_tilde(text):
"""Writes the home folder as "~", however the operating system spells its path."""
for spelling in sorted({str(HOME), os.path.realpath(HOME)}, key=len, reverse=True):
text = text.replace(spelling, "~")
return text
def session(script):
"""Runs the script's commands one by one, starting in an empty home folder."""
shutil.rmtree(HOME, ignore_errors=True)
HOME.mkdir()
env = _environment()
folder = HOME
for line in script.strip().splitlines():
command = line.strip()
if not command:
continue
if command.startswith("# "):
print(command)
continue
meant_to_fail = command.startswith("! ")
if meant_to_fail:
command = command[2:]
print("$", command)
if command.startswith("cd "):
folder = (folder / command[3:].strip()).resolve()
continue
done = subprocess.run(command, shell=True, cwd=folder, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
print(_with_tilde(done.stdout), end="")
if done.returncode != 0:
print(f"[exit status {done.returncode}]")
if not meant_to_fail:
sys.exit(1)
elif meant_to_fail:
sys.exit(f"{command} was meant to fail, but it worked") """Reads git diff --color=always output and prints it as plain text, naming the colour of each changed line.
With --color-moved, Git gives lines that moved their own colours (bold magenta where they left, bold cyan where they
arrived), apart from the red and green of lines that really changed. Usage: git diff --color=always … | python3 show_colors.py
"""
import re
import sys
NAMES = {"1;35": "moved from: ", "1;36": "moved to: ", "31": "deleted: ", "32": "added: "}
for line in sys.stdin.read().splitlines():
match = re.match(r"\x1b\[([0-9;]*)m", line)
plain = re.sub(r"\x1b\[[0-9;]*m", "", line)
label = NAMES.get(match.group(1), "") if match and plain[:1] in "+-" else ""
print(f"{label:<12}{plain}") Interview questions
Fresher to mid-level · What is the difference between git merge -X ours and git merge -s ours?
-X ours is an option of the default ort strategy. Git merges as usual, and only where a region conflicts does it
take our side instead of stopping; every non-conflicting change from the other branch is still merged. -s ours is a
separate strategy that ignores the other branch’s content entirely: the merge commit’s tree is identical to our
current tree, but the commit still has the other branch as a parent, so Git treats that branch as merged. Use
-X ours to settle conflicts in your favour, and -s ours only to record that an old line of work is superseded.
There is a -X theirs but no -s theirs.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to senior
A reviewer says your refactor's diff is unreadable: a moved function appears as a long deletion and addition with your two real edits buried inside, and blame now points every moved line at you. What do you change on the command line, in configuration, and in how you split the commits?
-
Mid to staff
After a merge, a bug appears in code that neither branch changed on its own. How do you prove whether the merge commit itself introduced it, and how do you find the lines involved?
-
Senior to staff
Your code-review service must show, for 20,000 open pull requests, whether each still merges cleanly into main, and refresh that whenever main moves. Checking out each one is far too slow. How would you design the check?
Key takeaways
- A diff is computed from two snapshots: trees first, then rename pairing, then a line algorithm per file.
- Myers and minimal minimise changed lines; patience and histogram anchor on unique or rare lines and often follow
moved code better;
ortmerges with histogram. - Renames are inferred from similarity, 50 % by default, within
diff.renameLimit;-Cand--find-copies-harderadd copies. - A three-way merge compares the base with both tips region by region: neighbouring changes conflict, and a change reverted on one side comes back.
- With criss-cross histories there are several merge bases;
ortmerges them into a virtual base instead of picking one. git merge-tree --write-treemerges without a working tree; trust its exit status and conflicted-file list.--remerge-diffshows what a merge commit changed beyond the automatic merge, including dropped changes thatgit showhides.-X ourssettles conflicts in your favour;-s oursdiscards the other branch.
References
- git-diff documentation (diff algorithms, rename and copy detection) (The Git project)
- Common diff options (--diff-algorithm, --anchored, -M, -C, -l) (The Git project)
- git-config documentation (diff.algorithm, diff.renameLimit, merge.conflictStyle) (The Git project)
- xdiff/xdiffi.c in the Git source (the Myers algorithm and its heuristics) (The Git project)
- xdiff/xpatience.c in the Git source (how patience diff picks unique lines) (The Git project)
- git-merge documentation (The Git project)
- Merge strategies (ort, -X ours and -s ours) (The Git project)
- git-merge-base documentation (criss-cross merges) (The Git project)
- git-merge-file documentation (The Git project)
- git-merge-tree documentation (The Git project)
- git-log documentation (--remerge-diff and combined diffs) (The Git project)
- gitdatamodel, Git's core data model (The Git project)
- Git 2.34 release notes (ort becomes the default strategy) (The Git project)
- Git 2.36 release notes (--remerge-diff) (The Git project)
- Git 2.38 release notes (git merge-tree --write-tree) (The Git project)
- Git 2.50 release notes (--minimal fix, merge-tree --quiet) (The Git project)
- Git 2.53 release notes (git blame --diff-algorithm) (The Git project)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress