How are conflicts detected in git

intro

diff is a core function of version control systems like git. It's so important that it deserves a single directory under git's root source directory, as xdiff.

One of programmers' nightmares is merge conflict, which is usually caused by merge/cherry-pick/rebase. These operations share a common 3-way-merge under the hood. Aside from how the diff algorithm works, which could be very complicated, two preliminary questions are: how changes are represented and how conflicts are detected?

The key conception

For two files comparison, if there are difference, the content in the left-side file is always removed whereas the content in the right-side is always added. As a corollary, the script only needs to record the deleted range (in the left side file) and the corresonding added range (in the right side file).

In xdchange_t, we can see two ranges: i1 + chg1 and i2 + chg2, which represent the deletion and additon on each file.

///@file: xdiffi.h
typedef struct s_xdchange {
	struct s_xdchange *next;
	long i1, i2;
	long chg1, chg2;
	int ignore;
} xdchange_t;

How are conflicts detected

The key observation is: Only the set 1 changes, which are deletion, matter. On the contrary, the addition set is ignored for conflicts detection, which seems very sound because no matter how many lines are added, it should not make any conflicts.

Note that s_xdmerge is produced by diff algorithm and we are here discussing the coflict detection based on two set of diff output.

The first two ifs are all no-conflict as the deletion range doesn't overlap (because the arguments to xdl_append_merge are 1 and 2 respectively, which means no no conflict as in the comment). This is what I've emphasized: conflicts detection only depends on deletion overlap and have nothing to do with additon.

///@file: xdiff/xmerge.c
typedef struct s_xdmerge {
	struct s_xdmerge *next;
	/*
	 * 0 = conflict,
	 * 1 = no conflict, take first,
	 * 2 = no conflict, take second.
	 * 3 = no conflict, take both.
	 */
	int mode;
	/*
	 * These point at the respective postimages.  E.g. <i1,chg1> is
	 * how side #1 wants to change the common ancestor; if there is no
	 * overlap, lines before i1 in the postimage of side #1 appear
	 * in the merge result as a region touched by neither side.
	 */
	long i1, i2;
	long chg1, chg2;
	/*
	 * These point at the preimage; of course there is just one
	 * preimage, that is from the shared common ancestor.
	 */
	long i0;
	long chg0;
} xdmerge_t;

static int xdl_do_merge(xdfenv_t *xe1, xdchange_t *xscr1,
		xdfenv_t *xe2, xdchange_t *xscr2,
		xmparam_t const *xmp, mmbuffer_t *result)
{
///...
	static int xdl_do_merge(xdfenv_t *xe1, xdchange_t *xscr1,
		xdfenv_t *xe2, xdchange_t *xscr2,
		xmparam_t const *xmp, mmbuffer_t *result)
{
	xdmerge_t *changes, *c;
	xpparam_t const *xpp = &xmp->xpp;
	const char *const ancestor_name = xmp->ancestor;
	const char *const name1 = xmp->file1;
	const char *const name2 = xmp->file2;
	int i0, i1, i2, chg0, chg1, chg2;
	int level = xmp->level;
	int style = xmp->style;
	int favor = xmp->favor;

	/*
	 * XDL_MERGE_DIFF3 does not attempt to refine conflicts by looking
	 * at common areas of sides 1 & 2, because the base (side 0) does
	 * not match and is being shown.  Similarly, simplification of
	 * non-conflicts is also skipped due to the skipping of conflict
	 * refinement.
	 *
	 * XDL_MERGE_ZEALOUS_DIFF3, on the other hand, will attempt to
	 * refine conflicts looking for common areas of sides 1 & 2.
	 * However, since the base is being shown and does not match,
	 * it will only look for common areas at the beginning or end
	 * of the conflict block.  Since XDL_MERGE_ZEALOUS_DIFF3's
	 * conflict refinement is much more limited in this fashion, the
	 * conflict simplification will be skipped.
	 */
	if (style == XDL_MERGE_DIFF3 || style == XDL_MERGE_ZEALOUS_DIFF3) {
		/*
		 * "diff3 -m" output does not make sense for anything
		 * more aggressive than XDL_MERGE_EAGER.
		 */
		if (XDL_MERGE_EAGER < level)
			level = XDL_MERGE_EAGER;
	}

	c = changes = NULL;

	while (xscr1 && xscr2) {
		if (!changes)
			changes = c;
		if (xscr1->i1 + xscr1->chg1 < xscr2->i1) {
			i0 = xscr1->i1;
			i1 = xscr1->i2;
			i2 = xscr2->i2 - xscr2->i1 + xscr1->i1;
			chg0 = xscr1->chg1;
			chg1 = xscr1->chg2;
			chg2 = xscr1->chg1;
			if (xdl_append_merge(&c, 1,
					     i0, chg0, i1, chg1, i2, chg2)) {
				xdl_cleanup_merge(changes);
				return -1;
			}
			xscr1 = xscr1->next;
			continue;
		}
		if (xscr2->i1 + xscr2->chg1 < xscr1->i1) {
			i0 = xscr2->i1;
			i1 = xscr1->i2 - xscr1->i1 + xscr2->i1;
			i2 = xscr2->i2;
			chg0 = xscr2->chg1;
			chg1 = xscr2->chg1;
			chg2 = xscr2->chg2;
			if (xdl_append_merge(&c, 2,
					     i0, chg0, i1, chg1, i2, chg2)) {
				xdl_cleanup_merge(changes);
				return -1;
			}
			xscr2 = xscr2->next;
			continue;
		}
		if (level == XDL_MERGE_MINIMAL || xscr1->i1 != xscr2->i1 ||
				xscr1->chg1 != xscr2->chg1 ||
				xscr1->chg2 != xscr2->chg2 ||
				xdl_merge_cmp_lines(xe1, xscr1->i2,
					xe2, xscr2->i2,
					xscr1->chg2, xpp->flags)) {
			/* conflict */
			int off = xscr1->i1 - xscr2->i1;
			int ffo = off + xscr1->chg1 - xscr2->chg1;

			i0 = xscr1->i1;
			i1 = xscr1->i2;
			i2 = xscr2->i2;
			if (off > 0) {
				i0 -= off;
				i1 -= off;
			}
			else
				i2 += off;
			chg0 = xscr1->i1 + xscr1->chg1 - i0;
			chg1 = xscr1->i2 + xscr1->chg2 - i1;
			chg2 = xscr2->i2 + xscr2->chg2 - i2;
			if (ffo < 0) {
				chg0 -= ffo;
				chg1 -= ffo;
			} else
				chg2 += ffo;
			if (xdl_append_merge(&c, 0,
					     i0, chg0, i1, chg1, i2, chg2)) {
				xdl_cleanup_merge(changes);
				return -1;
			}
		}

		i1 = xscr1->i1 + xscr1->chg1;
		i2 = xscr2->i1 + xscr2->chg1;

		if (i1 >= i2)
			xscr2 = xscr2->next;
		if (i2 >= i1)
			xscr1 = xscr1->next;
	}
///...
}

Some subtleties worth noticing here:

  • The condition of if (xscr1->i1 + xscr1->chg1 < xscr2->i1) is strictly less and doesn't include equality, which means adding at the same line is a conflict.

  • The i is always based on the original file but never the modified file.

A experiment

Both chg1 are 0, which means there is not deletion to the original file. chg2 is 5 and 7 respectively, which means the added line count.

tsecer@harry: git diff HEAD~ HEAD
diff --git a/txt b/txt
index e0037f0..4848180 100644
--- a/txt
+++ b/txt
@@ -1,3 +1,8 @@
 1111
+aaaa
+aaaa
+aaaa
+aaaa
+aaaa
 2222
 3333
tsecer@harry: git diff f759596a3068eaa22a79e38409c837cab31c6322~ f759596a3068eaa22a79e38409c837cab31c6322
diff --git a/txt b/txt
index e0037f0..bdf4698 100644
--- a/txt
+++ b/txt
@@ -1,3 +1,10 @@
 1111
 2222
+bbbb
+bbbb
+bbbb
+bbbb
+bbbb
+bbbb
+bbbb
 3333
tsecer@harry: gdb --quiet git
(gdb) b xdl_do_merge
Breakpoint 1 at 0x3b277f: file xdiff/xmerge.c, line 508.
(gdb) r cherry-pick f759596a3068eaa22a79e38409c837cab31c6322
Breakpoint 1, xdl_do_merge (xe1=0x7fffffffc060, xscr1=0x555555a8eaf0, xe2=0x7fffffffc0d0, xscr2=0x555555a8eb30, xmp=0x7fffffffc1c0, result=0x7fffffffc450) at xdiff/xmerge.c:508
508	{
(gdb) p *xscr1
$1 = {next = 0x0, i1 = 1, i2 = 1, chg1 = 0, chg2 = 5, ignore = 0}
(gdb) p *xscr2
$2 = {next = 0x0, i1 = 2, i2 = 2, chg1 = 0, chg2 = 7, ignore = 0}
(gdb) 

outro

diff algorithm is relatively a separate function, whereas conflict detection is a basic function of version control system. As the comment in xdiff/xdiffi.c says, the main two files diff detection is based on an O(ND) algorithm:

See "An O(ND) Difference Algorithm and its Variations", by Eugene Myers.

Git's merge and conflict detection are based on its output.

posted on 2026-09-28 20:45  tsecer  阅读(8)  评论(0)    收藏  举报

导航