Skip to content

Commit f217f0e

Browse files
eyvindgitster
authored andcommitted
Avoid conflicts when merging branches with mixed normalization
Currently, merging across changes in line ending normalization is painful since files containing CRLF will conflict with normalized files, even if the only difference between the two versions is the line endings. Additionally, any "real" merge conflicts that exist are obscured because every line in the file has a conflict. Assume you start out with a repo that has a lot of text files with CRLF checked in (A): o---C / \ A---B---D B: Add "* text=auto" to .gitattributes and normalize all files to LF-only C: Modify some of the text files D: Try to merge C You will get a ridiculous number of LF/CRLF conflicts when trying to merge C into D, since the repository contents for C are "wrong" wrt the new .gitattributes file. Fix ll-merge so that the "base", "theirs" and "ours" stages are passed through convert_to_worktree() and convert_to_git() before a three-way merge. This ensures that all three stages are normalized in the same way, removing from consideration differences that are only due to normalization. This feature is optional for now since it changes a low-level mechanism and is not necessary for the majority of users. The "merge.renormalize" config variable enables it. Signed-off-by: Eyvind Bernhardsen <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 492b107 commit f217f0e

File tree

8 files changed

+143
-2
lines changed

8 files changed

+143
-2
lines changed

Documentation/gitattributes.txt

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,17 @@ command is "cat").
317317
smudge = cat
318318
------------------------
319319

320+
For best results, `clean` should not alter its output further if it is
321+
run twice ("clean->clean" should be equivalent to "clean"), and
322+
multiple `smudge` commands should not alter `clean`'s output
323+
("smudge->smudge->clean" should be equivalent to "clean"). See the
324+
section on merging below.
325+
326+
The "indent" filter is well-behaved in this regard: it will not modify
327+
input that is already correctly indented. In this case, the lack of a
328+
smudge filter means that the clean filter _must_ accept its own output
329+
without modifying it.
330+
320331

321332
Interaction between checkin/checkout attributes
322333
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -331,6 +342,29 @@ In the check-out codepath, the blob content is first converted
331342
with `text`, and then `ident` and fed to `filter`.
332343

333344

345+
Merging branches with differing checkin/checkout attributes
346+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
347+
348+
If you have added attributes to a file that cause the canonical
349+
repository format for that file to change, such as adding a
350+
clean/smudge filter or text/eol/ident attributes, merging anything
351+
where the attribute is not in place would normally cause merge
352+
conflicts.
353+
354+
To prevent these unnecessary merge conflicts, git can be told to run a
355+
virtual check-out and check-in of all three stages of a file when
356+
resolving a three-way merge by setting the `merge.renormalize`
357+
configuration variable. This prevents changes caused by check-in
358+
conversion from causing spurious merge conflicts when a converted file
359+
is merged with an unconverted file.
360+
361+
As long as a "smudge->clean" results in the same output as a "clean"
362+
even on files that are already smudged, this strategy will
363+
automatically resolve all filter-related conflicts. Filters that do
364+
not act in this way may cause additional merge conflicts that must be
365+
resolved manually.
366+
367+
334368
Generating diff text
335369
~~~~~~~~~~~~~~~~~~~~
336370

Documentation/merge-config.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,16 @@ merge.renameLimit::
1515
during a merge; if not specified, defaults to the value of
1616
diff.renameLimit.
1717

18+
merge.renormalize::
19+
Tell git that canonical representation of files in the
20+
repository has changed over time (e.g. earlier commits record
21+
text files with CRLF line endings, but recent ones use LF line
22+
endings). In such a repository, git can convert the data
23+
recorded in commits to a canonical form before performing a
24+
merge to reduce unnecessary conflicts. For more information,
25+
see section "Merging branches with differing checkin/checkout
26+
attributes" in linkgit:gitattributes[5].
27+
1828
merge.stat::
1929
Whether to print the diffstat between ORIG_HEAD and the merge result
2030
at the end of the merge. True by default.

builtin/merge.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,9 @@ static int git_merge_config(const char *k, const char *v, void *cb)
503503
return git_config_string(&pull_octopus, k, v);
504504
else if (!strcmp(k, "merge.log") || !strcmp(k, "merge.summary"))
505505
option_log = git_config_bool(k, v);
506+
else if (!strcmp(k, "merge.renormalize")) {
507+
merge_renormalize = git_config_bool(k, v);
508+
}
506509
return git_diff_ui_config(k, v, cb);
507510
}
508511

cache.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,7 @@ extern int read_replace_refs;
551551
extern int fsync_object_files;
552552
extern int core_preload_index;
553553
extern int core_apply_sparse_checkout;
554+
extern int merge_renormalize;
554555

555556
enum safe_crlf {
556557
SAFE_CRLF_FALSE = 0,
@@ -1054,6 +1055,7 @@ extern void trace_argv_printf(const char **argv, const char *format, ...);
10541055
extern int convert_to_git(const char *path, const char *src, size_t len,
10551056
struct strbuf *dst, enum safe_crlf checksafe);
10561057
extern int convert_to_working_tree(const char *path, const char *src, size_t len, struct strbuf *dst);
1058+
extern int renormalize_buffer(const char *path, const char *src, size_t len, struct strbuf *dst);
10571059

10581060
/* add */
10591061
/*

convert.c

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,8 @@ static int is_binary(unsigned long size, struct text_stat *stats)
9393
return 0;
9494
}
9595

96-
static enum eol determine_output_conversion(enum action action) {
96+
static enum eol determine_output_conversion(enum action action)
97+
{
9798
switch (action) {
9899
case CRLF_BINARY:
99100
return EOL_UNSET;
@@ -693,7 +694,8 @@ static int git_path_check_ident(const char *path, struct git_attr_check *check)
693694
return !!ATTR_TRUE(value);
694695
}
695696

696-
enum action determine_action(enum action text_attr, enum eol eol_attr) {
697+
static enum action determine_action(enum action text_attr, enum eol eol_attr)
698+
{
697699
if (text_attr == CRLF_BINARY)
698700
return CRLF_BINARY;
699701
if (eol_attr == EOL_LF)
@@ -773,3 +775,13 @@ int convert_to_working_tree(const char *path, const char *src, size_t len, struc
773775
}
774776
return ret | apply_filter(path, src, len, dst, filter);
775777
}
778+
779+
int renormalize_buffer(const char *path, const char *src, size_t len, struct strbuf *dst)
780+
{
781+
int ret = convert_to_working_tree(path, src, len, dst);
782+
if (ret) {
783+
src = dst->buf;
784+
len = dst->len;
785+
}
786+
return ret | convert_to_git(path, src, len, dst, 0);
787+
}

environment.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ enum object_creation_mode object_creation_mode = OBJECT_CREATION_MODE;
5353
char *notes_ref_name;
5454
int grafts_replace_parents = 1;
5555
int core_apply_sparse_checkout;
56+
int merge_renormalize;
5657

5758
/* Parallel index stat data preload? */
5859
int core_preload_index = 0;

ll-merge.c

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,16 @@ static int git_path_check_merge(const char *path, struct git_attr_check check[2]
321321
return git_checkattr(path, 2, check);
322322
}
323323

324+
static void normalize_file(mmfile_t *mm, const char *path)
325+
{
326+
struct strbuf strbuf = STRBUF_INIT;
327+
if (renormalize_buffer(path, mm->ptr, mm->size, &strbuf)) {
328+
free(mm->ptr);
329+
mm->size = strbuf.len;
330+
mm->ptr = strbuf_detach(&strbuf, NULL);
331+
}
332+
}
333+
324334
int ll_merge(mmbuffer_t *result_buf,
325335
const char *path,
326336
mmfile_t *ancestor, const char *ancestor_label,
@@ -334,6 +344,11 @@ int ll_merge(mmbuffer_t *result_buf,
334344
const struct ll_merge_driver *driver;
335345
int virtual_ancestor = flag & 01;
336346

347+
if (merge_renormalize) {
348+
normalize_file(ancestor, path);
349+
normalize_file(ours, path);
350+
normalize_file(theirs, path);
351+
}
337352
if (!git_path_check_merge(path, check)) {
338353
ll_driver_name = check[0].value;
339354
if (check[1].value) {

t/t6038-merge-text-auto.sh

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
#!/bin/sh
2+
3+
test_description='CRLF merge conflict across text=auto change'
4+
5+
. ./test-lib.sh
6+
7+
test_expect_success setup '
8+
git config merge.renormalize true &&
9+
git config core.autocrlf false &&
10+
echo first line | append_cr >file &&
11+
echo first line >control_file &&
12+
echo only line >inert_file &&
13+
git add file control_file inert_file &&
14+
git commit -m "Initial" &&
15+
git tag initial &&
16+
git branch side &&
17+
echo "* text=auto" >.gitattributes &&
18+
touch file &&
19+
git add .gitattributes file &&
20+
git commit -m "normalize file" &&
21+
echo same line | append_cr >>file &&
22+
echo same line >>control_file &&
23+
git add file control_file &&
24+
git commit -m "add line from a" &&
25+
git tag a &&
26+
git rm .gitattributes &&
27+
rm file &&
28+
git checkout file &&
29+
git commit -m "remove .gitattributes" &&
30+
git tag c &&
31+
git checkout side &&
32+
echo same line | append_cr >>file &&
33+
echo same line >>control_file &&
34+
git add file control_file &&
35+
git commit -m "add line from b" &&
36+
git tag b &&
37+
git checkout master
38+
'
39+
40+
test_expect_success 'Check merging after setting text=auto' '
41+
git reset --hard a &&
42+
git merge b &&
43+
cat file | remove_cr >file.temp &&
44+
test_cmp file file.temp
45+
'
46+
47+
test_expect_success 'Check merging addition of text=auto' '
48+
git reset --hard b &&
49+
git merge a &&
50+
cat file | remove_cr >file.temp &&
51+
test_cmp file file.temp
52+
'
53+
54+
test_expect_failure 'Test delete/normalize conflict' '
55+
git checkout side &&
56+
git reset --hard initial &&
57+
git rm file &&
58+
git commit -m "remove file" &&
59+
git checkout master &&
60+
git reset --hard a^ &&
61+
git merge side
62+
'
63+
64+
test_done

0 commit comments

Comments
 (0)