Commits

Anonymous committed 8aed4a5 Merge with conflicts

Merge branch 'jn/merge-renormalize'

* jn/merge-renormalize:
merge-recursive --renormalize
rerere: never renormalize
rerere: migrate to parse-options API
t4200 (rerere): modernize style
ll-merge: let caller decide whether to renormalize
ll-merge: make flag easier to populate
Documentation/technical: document ll_merge
merge-trees: let caller decide whether to renormalize
merge-trees: push choice to renormalize away from low level
t6038 (merge.renormalize): check that it can be turned off
t6038 (merge.renormalize): try checkout -m and cherry-pick
t6038 (merge.renormalize): style nitpicks
Don't expand CRLFs when normalizing text during merge
Try normalizing files to avoid delete/modify conflicts when merging
Avoid conflicts when merging branches with mixed normalization

Conflicts:
builtin/rerere.c
t/t4200-rerere.sh

  • Participants
  • Parent commits 22ffc39, 7610fa5

Comments (0)

Files changed (18)

File Documentation/gitattributes.txt

 	smudge = cat
 ------------------------
 
+For best results, `clean` should not alter its output further if it is
+run twice ("clean->clean" should be equivalent to "clean"), and
+multiple `smudge` commands should not alter `clean`'s output
+("smudge->smudge->clean" should be equivalent to "clean").  See the
+section on merging below.
+
+The "indent" filter is well-behaved in this regard: it will not modify
+input that is already correctly indented.  In this case, the lack of a
+smudge filter means that the clean filter _must_ accept its own output
+without modifying it.
+
 
 Interaction between checkin/checkout attributes
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 with `text`, and then `ident` and fed to `filter`.
 
 
+Merging branches with differing checkin/checkout attributes
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+If you have added attributes to a file that cause the canonical
+repository format for that file to change, such as adding a
+clean/smudge filter or text/eol/ident attributes, merging anything
+where the attribute is not in place would normally cause merge
+conflicts.
+
+To prevent these unnecessary merge conflicts, git can be told to run a
+virtual check-out and check-in of all three stages of a file when
+resolving a three-way merge by setting the `merge.renormalize`
+configuration variable.  This prevents changes caused by check-in
+conversion from causing spurious merge conflicts when a converted file
+is merged with an unconverted file.
+
+As long as a "smudge->clean" results in the same output as a "clean"
+even on files that are already smudged, this strategy will
+automatically resolve all filter-related conflicts.  Filters that do
+not act in this way may cause additional merge conflicts that must be
+resolved manually.
+
+
 Generating diff text
 ~~~~~~~~~~~~~~~~~~~~
 

File Documentation/merge-config.txt

 	during a merge; if not specified, defaults to the value of
 	diff.renameLimit.
 
+merge.renormalize::
+	Tell git that canonical representation of files in the
+	repository has changed over time (e.g. earlier commits record
+	text files with CRLF line endings, but recent ones use LF line
+	endings).  In such a repository, git can convert the data
+	recorded in commits to a canonical form before performing a
+	merge to reduce unnecessary conflicts.  For more information,
+	see section "Merging branches with differing checkin/checkout
+	attributes" in linkgit:gitattributes[5].
+
 merge.stat::
 	Whether to print the diffstat between ORIG_HEAD and the merge result
 	at the end of the merge.  True by default.

File Documentation/merge-strategies.txt

 theirs;;
 	This is opposite of 'ours'.
 
+renormalize;;
+	This runs a virtual check-out and check-in of all three stages
+	of a file when resolving a three-way merge.  This option is
+	meant to be used when merging branches with different clean
+	filters or end-of-line normalization rules.  See "Merging
+	branches with differing checkin/checkout attributes" in
+	linkgit:gitattributes[5] for details.
+
+no-renormalize;;
+	Disables the `renormalize` option.  This overrides the
+	`merge.renormalize` configuration variable.
+
 subtree[=path];;
 	This option is a more advanced form of 'subtree' strategy, where
 	the strategy makes a guess on how two trees must be shifted to

File Documentation/technical/api-merge.txt

+merge API
+=========
+
+The merge API helps a program to reconcile two competing sets of
+improvements to some files (e.g., unregistered changes from the work
+tree versus changes involved in switching to a new branch), reporting
+conflicts if found.  The library called through this API is
+responsible for a few things.
+
+ * determining which trees to merge (recursive ancestor consolidation);
+
+ * lining up corresponding files in the trees to be merged (rename
+   detection, subtree shifting), reporting edge cases like add/add
+   and rename/rename conflicts to the user;
+
+ * performing a three-way merge of corresponding files, taking
+   path-specific merge drivers (specified in `.gitattributes`)
+   into account.
+
+Low-level (single file) merge
+-----------------------------
+
+`ll_merge`::
+
+	Perform a three-way single-file merge in core.  This is
+	a thin wrapper around `xdl_merge` that takes the path and
+	any merge backend specified in `.gitattributes` or
+	`.git/info/attributes` into account.  Returns 0 for a
+	clean merge.
+
+The caller:
+
+1. allocates an mmbuffer_t variable for the result;
+2. allocates and fills variables with the file's original content
+   and two modified versions (using `read_mmfile`, for example);
+3. calls ll_merge();
+4. reads the output from result_buf.ptr and result_buf.size;
+5. releases buffers when finished (free(ancestor.ptr); free(ours.ptr);
+   free(theirs.ptr); free(result_buf.ptr);).
+
+If the modifications do not merge cleanly, `ll_merge` will return a
+nonzero value and `result_buf` will generally include a description of
+the conflict bracketed by markers such as the traditional `<<<<<<<`
+and `>>>>>>>`.
+
+The `ancestor_label`, `our_label`, and `their_label` parameters are
+used to label the different sides of a conflict if the merge driver
+supports this.
+
+The `flag` parameter is a bitfield:
+
+ - The `LL_OPT_VIRTUAL_ANCESTOR` bit indicates whether this is an
+   internal merge to consolidate ancestors for a recursive merge.
+
+ - The `LL_OPT_FAVOR_MASK` bits allow local conflicts to be automatically
+   resolved in favor of one side or the other (as in 'git merge-file'
+   `--ours`/`--theirs`/`--union`).
+   They can be populated by `create_ll_flag`, whose argument can be
+   `XDL_MERGE_FAVOR_OURS`, `XDL_MERGE_FAVOR_THEIRS`, or
+   `XDL_MERGE_FAVOR_UNION`.
+
+Everything else
+---------------
+
+Talk about <merge-recursive.h> and merge_file():
+
+ - merge_trees() to merge with rename detection
+ - merge_recursive() for ancestor consolidation
+ - try_merge_command() for other strategies
+ - conflict format
+ - merge options
+
+(Daniel, Miklos, Stephan, JC)

File builtin/checkout.c

 	read_mmblob(&ours, active_cache[pos+1]->sha1);
 	read_mmblob(&theirs, active_cache[pos+2]->sha1);
 
+	/*
+	 * NEEDSWORK: re-create conflicts from merges with
+	 * merge.renormalize set, too
+	 */
 	status = ll_merge(&result_buf, path, &ancestor, "base",
 			  &ours, "ours", &theirs, "theirs", 0);
 	free(ancestor.ptr);
 			 */
 
 			add_files_to_cache(NULL, NULL, 0);
+			/*
+			 * NEEDSWORK: carrying over local changes
+			 * when branches have different end-of-line
+			 * normalization (or clean+smudge rules) is
+			 * a pain; plumb in an option to set
+			 * o.renormalize?
+			 */
 			init_merge_options(&o);
 			o.verbosity = 0;
 			work = write_tree_from_memory(&o);

File builtin/merge-recursive.c

 				o.subtree_shift = "";
 			else if (!prefixcmp(arg+2, "subtree="))
 				o.subtree_shift = arg + 10;
+			else if (!strcmp(arg+2, "renormalize"))
+				o.renormalize = 1;
+			else if (!strcmp(arg+2, "no-renormalize"))
+				o.renormalize = 0;
 			else
 				die("Unknown option %s", arg);
 			continue;

File builtin/merge.c

 static const char **xopts;
 static size_t xopts_nr, xopts_alloc;
 static const char *branch;
+static int option_renormalize;
 static int verbosity;
 static int allow_rerere_auto;
 
 		return git_config_string(&pull_octopus, k, v);
 	else if (!strcmp(k, "merge.log") || !strcmp(k, "merge.summary"))
 		option_log = git_config_bool(k, v);
+	else if (!strcmp(k, "merge.renormalize"))
+		option_renormalize = git_config_bool(k, v);
 	return git_diff_ui_config(k, v, cb);
 }
 
 		if (!strcmp(strategy, "subtree"))
 			o.subtree_shift = "";
 
+		o.renormalize = option_renormalize;
+
+		/*
+		 * NEEDSWORK: merge with table in builtin/merge-recursive
+		 */
 		for (x = 0; x < xopts_nr; x++) {
 			if (!strcmp(xopts[x], "ours"))
 				o.recursive_variant = MERGE_RECURSIVE_OURS;
 				o.subtree_shift = "";
 			else if (!prefixcmp(xopts[x], "subtree="))
 				o.subtree_shift = xopts[x]+8;
+			else if (!strcmp(xopts[x], "renormalize"))
+				o.renormalize = 1;
+			else if (!strcmp(xopts[x], "no-renormalize"))
+				o.renormalize = 0;
 			else
 				die("Unknown option for merge-recursive: -X%s", xopts[x]);
 		}
 	return 0;
 }
 
-static int suggest_conflicts(void)
+static int suggest_conflicts(int renormalizing)
 {
 	FILE *fp;
 	int pos;
 			"stopped before committing as requested\n");
 		return 0;
 	} else
-		return suggest_conflicts();
+		return suggest_conflicts(option_renormalize);
 }

File builtin/rerere.c

 #include "builtin.h"
 #include "cache.h"
 #include "dir.h"
+#include "parse-options.h"
 #include "string-list.h"
 #include "rerere.h"
 #include "xdiff/xdiff.h"
 #include "xdiff-interface.h"
 
-static const char git_rerere_usage[] =
-"git rerere [clear | status | diff | gc]";
+static const char * const rerere_usage[] = {
+	"git rerere [clear | status | diff | gc]",
+	NULL,
+};
 
 /* these values are days */
 static int cutoff_noresolve = 15;
 int cmd_rerere(int argc, const char **argv, const char *prefix)
 {
 	struct string_list merge_rr = STRING_LIST_INIT_DUP;
-	int i, fd, flags = 0;
-
-	if (2 < argc) {
-		if (!strcmp(argv[1], "-h"))
-			usage(git_rerere_usage);
-		if (!strcmp(argv[1], "--rerere-autoupdate"))
-			flags = RERERE_AUTOUPDATE;
-		else if (!strcmp(argv[1], "--no-rerere-autoupdate"))
-			flags = RERERE_NOAUTOUPDATE;
-		if (flags) {
-			argc--;
-			argv++;
-		}
-	}
-	if (argc < 2)
+	int i, fd, autoupdate = -1, flags = 0;
+
+	struct option options[] = {
+		OPT_SET_INT(0, "rerere-autoupdate", &autoupdate,
+			"register clean resolutions in index", 1),
+		OPT_END(),
+	};
+
+	argc = parse_options(argc, argv, prefix, options, rerere_usage, 0);
+
+	if (autoupdate == 1)
+		flags = RERERE_AUTOUPDATE;
+	if (autoupdate == 0)
+		flags = RERERE_NOAUTOUPDATE;
+
+	if (argc < 1)
 		return rerere(flags);
 
-	if (!strcmp(argv[1], "forget")) {
-		const char **pathspec = get_pathspec(prefix, argv + 2);
+	if (!strcmp(argv[0], "forget")) {
+		const char **pathspec = get_pathspec(prefix, argv + 1);
 		return rerere_forget(pathspec);
 	}
 
 	if (fd < 0)
 		return 0;
 
-	if (!strcmp(argv[1], "clear")) {
+	if (!strcmp(argv[0], "clear")) {
 		for (i = 0; i < merge_rr.nr; i++) {
 			const char *name = (const char *)merge_rr.items[i].util;
 			if (!has_rerere_resolution(name))
 				unlink_rr_item(name);
 		}
 		unlink_or_warn(git_path("MERGE_RR"));
-	} else if (!strcmp(argv[1], "gc"))
+	} else if (!strcmp(argv[0], "gc"))
 		garbage_collect(&merge_rr);
-	else if (!strcmp(argv[1], "status"))
+	else if (!strcmp(argv[0], "status"))
 		for (i = 0; i < merge_rr.nr; i++)
 			printf("%s\n", merge_rr.items[i].string);
-	else if (!strcmp(argv[1], "diff"))
+	else if (!strcmp(argv[0], "diff"))
 		for (i = 0; i < merge_rr.nr; i++) {
 			const char *path = merge_rr.items[i].string;
 			const char *name = (const char *)merge_rr.items[i].util;
 			diff_two(rerere_path(name, "preimage"), path, path, path);
 		}
 	else
-		usage(git_rerere_usage);
+		usage_with_options(rerere_usage, options);
 
 	string_list_clear(&merge_rr, 1);
 	return 0;

File builtin/revert.c

 	index_fd = hold_locked_index(&index_lock, 1);
 
 	read_cache();
+
+	/*
+	 * NEEDSWORK: cherry-picking between branches with
+	 * different end-of-line normalization is a pain;
+	 * plumb in an option to set o.renormalize?
+	 * (or better: arbitrary -X options)
+	 */
 	init_merge_options(&o);
 	o.ancestor = base ? base_label : "(empty tree)";
 	o.branch1 = "HEAD";
 extern int convert_to_git(const char *path, const char *src, size_t len,
                           struct strbuf *dst, enum safe_crlf checksafe);
 extern int convert_to_working_tree(const char *path, const char *src, size_t len, struct strbuf *dst);
+extern int renormalize_buffer(const char *path, const char *src, size_t len, struct strbuf *dst);
 
 /* add */
 /*
 	return 0;
 }
 
-static enum eol determine_output_conversion(enum action action) {
+static enum eol determine_output_conversion(enum action action)
+{
 	switch (action) {
 	case CRLF_BINARY:
 		return EOL_UNSET;
 	return !!ATTR_TRUE(value);
 }
 
-enum action determine_action(enum action text_attr, enum eol eol_attr) {
+static enum action determine_action(enum action text_attr, enum eol eol_attr)
+{
 	if (text_attr == CRLF_BINARY)
 		return CRLF_BINARY;
 	if (eol_attr == EOL_LF)
 	return ret | ident_to_git(path, src, len, dst, ident);
 }
 
-int convert_to_working_tree(const char *path, const char *src, size_t len, struct strbuf *dst)
+static int convert_to_working_tree_internal(const char *path, const char *src,
+					    size_t len, struct strbuf *dst,
+					    int normalizing)
 {
 	struct git_attr_check check[5];
 	enum action action = CRLF_GUESS;
 		src = dst->buf;
 		len = dst->len;
 	}
-	action = determine_action(action, eol_attr);
-	ret |= crlf_to_worktree(path, src, len, dst, action);
+	/*
+	 * CRLF conversion can be skipped if normalizing, unless there
+	 * is a smudge filter.  The filter might expect CRLFs.
+	 */
+	if (filter || !normalizing) {
+		action = determine_action(action, eol_attr);
+		ret |= crlf_to_worktree(path, src, len, dst, action);
+		if (ret) {
+			src = dst->buf;
+			len = dst->len;
+		}
+	}
+	return ret | apply_filter(path, src, len, dst, filter);
+}
+
+int convert_to_working_tree(const char *path, const char *src, size_t len, struct strbuf *dst)
+{
+	return convert_to_working_tree_internal(path, src, len, dst, 0);
+}
+
+int renormalize_buffer(const char *path, const char *src, size_t len, struct strbuf *dst)
+{
+	int ret = convert_to_working_tree_internal(path, src, len, dst, 1);
 	if (ret) {
 		src = dst->buf;
 		len = dst->len;
 	}
-	return ret | apply_filter(path, src, len, dst, filter);
+	return ret | convert_to_git(path, src, len, dst, 0);
 }
 	 * or common ancestor for an internal merge.  Still return
 	 * "conflicted merge" status.
 	 */
-	mmfile_t *stolen = (flag & 01) ? orig : src1;
+	mmfile_t *stolen = (flag & LL_OPT_VIRTUAL_ANCESTOR) ? orig : src1;
 
 	result->ptr = stolen->ptr;
 	result->size = stolen->size;
 
 	memset(&xmp, 0, sizeof(xmp));
 	xmp.level = XDL_MERGE_ZEALOUS;
-	xmp.favor= (flag >> 1) & 03;
+	xmp.favor = ll_opt_favor(flag);
 	if (git_xmerge_style >= 0)
 		xmp.style = git_xmerge_style;
 	if (marker_size > 0)
 			  int flag, int marker_size)
 {
 	/* Use union favor */
-	flag = (flag & 1) | (XDL_MERGE_FAVOR_UNION << 1);
+	flag &= ~LL_OPT_FAVOR_MASK;
+	flag |= create_ll_flag(XDL_MERGE_FAVOR_UNION);
 	return ll_xdl_merge(drv_unused, result, path_unused,
 			    orig, NULL, src1, NULL, src2, NULL,
 			    flag, marker_size);
 	return git_checkattr(path, 2, check);
 }
 
+static void normalize_file(mmfile_t *mm, const char *path)
+{
+	struct strbuf strbuf = STRBUF_INIT;
+	if (renormalize_buffer(path, mm->ptr, mm->size, &strbuf)) {
+		free(mm->ptr);
+		mm->size = strbuf.len;
+		mm->ptr = strbuf_detach(&strbuf, NULL);
+	}
+}
+
 int ll_merge(mmbuffer_t *result_buf,
 	     const char *path,
 	     mmfile_t *ancestor, const char *ancestor_label,
 	const char *ll_driver_name = NULL;
 	int marker_size = DEFAULT_CONFLICT_MARKER_SIZE;
 	const struct ll_merge_driver *driver;
-	int virtual_ancestor = flag & 01;
+	int virtual_ancestor = flag & LL_OPT_VIRTUAL_ANCESTOR;
 
+	if (flag & LL_OPT_RENORMALIZE) {
+		normalize_file(ancestor, path);
+		normalize_file(ours, path);
+		normalize_file(theirs, path);
+	}
 	if (!git_path_check_merge(path, check)) {
 		ll_driver_name = check[0].value;
 		if (check[1].value) {
 #ifndef LL_MERGE_H
 #define LL_MERGE_H
 
+#define LL_OPT_VIRTUAL_ANCESTOR	(1 << 0)
+#define LL_OPT_FAVOR_MASK	((1 << 1) | (1 << 2))
+#define LL_OPT_FAVOR_SHIFT 1
+#define LL_OPT_RENORMALIZE	(1 << 3)
+
+static inline int ll_opt_favor(int flag)
+{
+	return (flag & LL_OPT_FAVOR_MASK) >> LL_OPT_FAVOR_SHIFT;
+}
+
+static inline int create_ll_flag(int favor)
+{
+	return ((favor << LL_OPT_FAVOR_SHIFT) & LL_OPT_FAVOR_MASK);
+}
+
 int ll_merge(mmbuffer_t *result_buf,
 	     const char *path,
 	     mmfile_t *ancestor, const char *ancestor_label,

File merge-recursive.c

 
 	merge_status = ll_merge(result_buf, a->path, &orig, base_name,
 				&src1, name1, &src2, name2,
-				(!!o->call_depth) | (favor << 1));
+				((o->call_depth ? LL_OPT_VIRTUAL_ANCESTOR : 0) |
+				 (o->renormalize ? LL_OPT_RENORMALIZE : 0) |
+				 create_ll_flag(favor)));
 
 	free(name1);
 	free(name2);
 	return (is_null_sha1(sha) || mode == 0) ? NULL: (unsigned char *)sha;
 }
 
+static int read_sha1_strbuf(const unsigned char *sha1, struct strbuf *dst)
+{
+	void *buf;
+	enum object_type type;
+	unsigned long size;
+	buf = read_sha1_file(sha1, &type, &size);
+	if (!buf)
+		return error("cannot read object %s", sha1_to_hex(sha1));
+	if (type != OBJ_BLOB) {
+		free(buf);
+		return error("object %s is not a blob", sha1_to_hex(sha1));
+	}
+	strbuf_attach(dst, buf, size, size + 1);
+	return 0;
+}
+
+static int blob_unchanged(const unsigned char *o_sha,
+			  const unsigned char *a_sha,
+			  int renormalize, const char *path)
+{
+	struct strbuf o = STRBUF_INIT;
+	struct strbuf a = STRBUF_INIT;
+	int ret = 0; /* assume changed for safety */
+
+	if (sha_eq(o_sha, a_sha))
+		return 1;
+	if (!renormalize)
+		return 0;
+
+	assert(o_sha && a_sha);
+	if (read_sha1_strbuf(o_sha, &o) || read_sha1_strbuf(a_sha, &a))
+		goto error_return;
+	/*
+	 * Note: binary | is used so that both renormalizations are
+	 * performed.  Comparison can be skipped if both files are
+	 * unchanged since their sha1s have already been compared.
+	 */
+	if (renormalize_buffer(path, o.buf, o.len, &o) |
+	    renormalize_buffer(path, a.buf, o.len, &a))
+		ret = (o.len == a.len && !memcmp(o.buf, a.buf, o.len));
+
+error_return:
+	strbuf_release(&o);
+	strbuf_release(&a);
+	return ret;
+}
+
 /* Per entry merge function */
 static int process_entry(struct merge_options *o,
 			 const char *path, struct stage_data *entry)
 	print_index_entry("\tpath: ", entry);
 	*/
 	int clean_merge = 1;
+	int normalize = o->renormalize;
 	unsigned o_mode = entry->stages[1].mode;
 	unsigned a_mode = entry->stages[2].mode;
 	unsigned b_mode = entry->stages[3].mode;
 	if (o_sha && (!a_sha || !b_sha)) {
 		/* Case A: Deleted in one */
 		if ((!a_sha && !b_sha) ||
-		    (sha_eq(a_sha, o_sha) && !b_sha) ||
-		    (!a_sha && sha_eq(b_sha, o_sha))) {
+		    (!b_sha && blob_unchanged(o_sha, a_sha, normalize, path)) ||
+		    (!a_sha && blob_unchanged(o_sha, b_sha, normalize, path))) {
 			/* Deleted in both or deleted in one and
 			 * unchanged in the other */
 			if (a_sha)
 	o->buffer_output = 1;
 	o->diff_rename_limit = -1;
 	o->merge_rename_limit = -1;
+	o->renormalize = 0;
 	git_config(merge_recursive_config, o);
 	if (getenv("GIT_MERGE_VERBOSITY"))
 		o->verbosity =

File merge-recursive.h

 	} recursive_variant;
 	const char *subtree_shift;
 	unsigned buffer_output : 1;
+	unsigned renormalize : 1;
 	int verbosity;
 	int diff_rename_limit;
 	int merge_rename_limit;
 		if (!mmfile[i].ptr && !mmfile[i].size)
 			mmfile[i].ptr = xstrdup("");
 	}
+	/*
+	 * NEEDSWORK: handle conflicts from merges with
+	 * merge.renormalize set, too
+	 */
 	ll_merge(&result, path, &mmfile[0], NULL,
 		 &mmfile[1], "ours",
 		 &mmfile[2], "theirs", 0);

File t/t4200-rerere.sh

 #
 
 test_description='git rerere
+
+! [fifth] version1
+ ! [first] first
+  ! [fourth] version1
+   ! [master] initial
+    ! [second] prefer first over second
+     ! [third] version2
+------
+     + [third] version2
++      [fifth] version1
+  +    [fourth] version1
++ +  + [third^] third
+    -  [second] prefer first over second
+ +  +  [first] first
+    +  [second^] second
+++++++ [master] initial
 '
 
 . ./test-lib.sh
 
-test_expect_success 'setup' "
-	cat > a1 <<- EOF &&
+test_expect_success 'setup' '
+	cat >a1 <<-\EOF &&
 	Some title
 	==========
-	Whether 'tis nobler in the mind to suffer
+	Whether '\''tis nobler in the mind to suffer
 	The slings and arrows of outrageous fortune,
 	Or to take arms against a sea of troubles,
 	And by opposing end them? To die: to sleep;
 	No more; and by a sleep to say we end
 	The heart-ache and the thousand natural shocks
-	That flesh is heir to, 'tis a consummation
-	Devoutly to be wish'd.
+	That flesh is heir to, '\''tis a consummation
+	Devoutly to be wish'\''d.
 	EOF
 
 	git add a1 &&
+	test_tick &&
 	git commit -q -a -m initial &&
 
-	git checkout -b first &&
-	cat >> a1 <<- EOF &&
+	cat >>a1 <<-\EOF &&
 	Some title
 	==========
 	To die, to sleep;
-	To sleep: perchance to dream: ay, there's the rub;
+	To sleep: perchance to dream: ay, there'\''s the rub;
 	For in that sleep of death what dreams may come
 	When we have shuffled off this mortal coil,
-	Must give us pause: there's the respect
+	Must give us pause: there'\''s the respect
 	That makes calamity of so long life;
 	EOF
+
+	git checkout -b first &&
+	test_tick &&
 	git commit -q -a -m first &&
 
 	git checkout -b second master &&
 	git show first:a1 |
-	sed -e 's/To die, t/To die! T/' -e 's/Some title/Some Title/' > a1 &&
-	echo '* END *' >>a1 &&
+	sed -e "s/To die, t/To die! T/" -e "s/Some title/Some Title/" >a1 &&
+	echo "* END *" >>a1 &&
+	test_tick &&
 	git commit -q -a -m second
-"
+'
 
 test_expect_success 'nothing recorded without rerere' '
-	(rm -rf .git/rr-cache; git config rerere.enabled false) &&
+	rm -rf .git/rr-cache &&
+	git config rerere.enabled false &&
 	test_must_fail git merge first &&
 	! test -d .git/rr-cache
 '
 
-# activate rerere, old style
-test_expect_success 'conflicting merge' '
+test_expect_success 'activate rerere, old style (conflicting merge)' '
 	git reset --hard &&
 	mkdir .git/rr-cache &&
-	git config --unset rerere.enabled &&
-	test_must_fail git merge first
-'
+	test_might_fail git config --unset rerere.enabled &&
+	test_must_fail git merge first &&
 
-sha1=$(perl -pe 's/	.*//' .git/MERGE_RR)
-rr=.git/rr-cache/$sha1
-test_expect_success 'recorded preimage' "grep ^=======$ $rr/preimage"
+	sha1=$(perl -pe "s/	.*//" .git/MERGE_RR) &&
+	rr=.git/rr-cache/$sha1 &&
+	grep "^=======\$" $rr/preimage &&
+	! test -f $rr/postimage &&
+	! test -f $rr/thisimage
+'
 
 test_expect_success 'rerere.enabled works, too' '
 	rm -rf .git/rr-cache &&
 	git config rerere.enabled true &&
 	git reset --hard &&
 	test_must_fail git merge first &&
+
+	sha1=$(perl -pe "s/	.*//" .git/MERGE_RR) &&
+	rr=.git/rr-cache/$sha1 &&
 	grep ^=======$ $rr/preimage
 '
 
-test_expect_success 'no postimage or thisimage yet' \
-	"test ! -f $rr/postimage -a ! -f $rr/thisimage"
+test_expect_success 'set up rr-cache' '
+	rm -rf .git/rr-cache &&
+	git config rerere.enabled true &&
+	git reset --hard &&
+	test_must_fail git merge first &&
+	sha1=$(perl -pe "s/	.*//" .git/MERGE_RR) &&
+	rr=.git/rr-cache/$sha1
+'
 
-test_expect_success 'preimage has right number of lines' '
+test_expect_success 'rr-cache looks sane' '
+	# no postimage or thisimage yet
+	! test -f $rr/postimage &&
+	! test -f $rr/thisimage &&
 
+	# preimage has right number of lines
 	cnt=$(sed -ne "/^<<<<<<</,/^>>>>>>>/p" $rr/preimage | wc -l) &&
+	echo $cnt &&
 	test $cnt = 13
-
 '
 
-git show first:a1 > a1
-
-cat > expect << EOF
---- a/a1
-+++ b/a1
-@@ -1,4 +1,4 @@
--Some Title
-+Some title
- ==========
- Whether 'tis nobler in the mind to suffer
- The slings and arrows of outrageous fortune,
-@@ -8,21 +8,11 @@
- The heart-ache and the thousand natural shocks
- That flesh is heir to, 'tis a consummation
- Devoutly to be wish'd.
--<<<<<<<
--Some Title
--==========
--To die! To sleep;
--=======
- Some title
- ==========
- To die, to sleep;
-->>>>>>>
- To sleep: perchance to dream: ay, there's the rub;
- For in that sleep of death what dreams may come
- When we have shuffled off this mortal coil,
- Must give us pause: there's the respect
- That makes calamity of so long life;
--<<<<<<<
--=======
--* END *
-->>>>>>>
-EOF
-git rerere diff > out
-
-test_expect_success 'rerere diff' 'test_cmp expect out'
-
-cat > expect << EOF
-a1
-EOF
-
-git rerere status > out
-
-test_expect_success 'rerere status' 'test_cmp expect out'
-
-test_expect_success 'commit succeeds' \
-	"git commit -q -a -m 'prefer first over second'"
-
-test_expect_success 'recorded postimage' "test -f $rr/postimage"
-
-oldmtimepost=$(test-chmtime -v -60 $rr/postimage |cut -f 1)
-
-test_expect_success 'another conflicting merge' '
-	git checkout -b third master &&
-	git show second^:a1 | sed "s/To die: t/To die! T/" > a1 &&
-	git commit -q -a -m third &&
-	test_must_fail git pull . first
+test_expect_success 'rerere diff' '
+	git show first:a1 >a1 &&
+	cat >expect <<-\EOF &&
+	--- a/a1
+	+++ b/a1
+	@@ -1,4 +1,4 @@
+	-Some Title
+	+Some title
+	 ==========
+	 Whether '\''tis nobler in the mind to suffer
+	 The slings and arrows of outrageous fortune,
+	@@ -8,21 +8,11 @@
+	 The heart-ache and the thousand natural shocks
+	 That flesh is heir to, '\''tis a consummation
+	 Devoutly to be wish'\''d.
+	-<<<<<<<
+	-Some Title
+	-==========
+	-To die! To sleep;
+	-=======
+	 Some title
+	 ==========
+	 To die, to sleep;
+	->>>>>>>
+	 To sleep: perchance to dream: ay, there'\''s the rub;
+	 For in that sleep of death what dreams may come
+	 When we have shuffled off this mortal coil,
+	 Must give us pause: there'\''s the respect
+	 That makes calamity of so long life;
+	-<<<<<<<
+	-=======
+	-* END *
+	->>>>>>>
+	EOF
+	git rerere diff >out &&
+	test_cmp expect out
 '
 
-git show first:a1 | sed 's/To die: t/To die! T/' > expect
-test_expect_success 'rerere kicked in' "! grep ^=======$ a1"
-
-test_expect_success 'rerere prefers first change' 'test_cmp a1 expect'
-
-test_expect_success 'rerere updates postimage timestamp' '
-	newmtimepost=$(test-chmtime -v +0 $rr/postimage |cut -f 1) &&
-	test $oldmtimepost -lt $newmtimepost
+test_expect_success 'rerere status' '
+	echo a1 >expect &&
+	git rerere status >out &&
+	test_cmp expect out
 '
 
-rm $rr/postimage
-echo "$sha1	a1" | perl -pe 'y/\012/\000/' > .git/MERGE_RR
+test_expect_success 'first postimage wins' '
+	git show first:a1 | sed "s/To die: t/To die! T/" >expect &&
 
-test_expect_success 'rerere clear' 'git rerere clear'
+	git commit -q -a -m "prefer first over second" &&
+	test -f $rr/postimage &&
 
-test_expect_success 'clear removed the directory' "test ! -d $rr"
+	oldmtimepost=$(test-chmtime -v -60 $rr/postimage | cut -f 1) &&
 
-mkdir $rr
-echo Hello > $rr/preimage
-echo World > $rr/postimage
+	git checkout -b third master &&
+	git show second^:a1 | sed "s/To die: t/To die! T/" >a1 &&
+	git commit -q -a -m third &&
 
-sha2=4000000000000000000000000000000000000000
-rr2=.git/rr-cache/$sha2
-mkdir $rr2
-echo Hello > $rr2/preimage
+	test_must_fail git pull . first &&
+	# rerere kicked in
+	! grep "^=======\$" a1 &&
+	test_cmp expect a1
+'
 
-almost_15_days_ago=$((60-15*86400))
-just_over_15_days_ago=$((-1-15*86400))
-almost_60_days_ago=$((60-60*86400))
-just_over_60_days_ago=$((-1-60*86400))
+test_expect_success 'rerere updates postimage timestamp' '
+	newmtimepost=$(test-chmtime -v +0 $rr/postimage | cut -f 1) &&
+	test $oldmtimepost -lt $newmtimepost
+'
 
-test-chmtime =$just_over_60_days_ago $rr/preimage
-test-chmtime =$almost_60_days_ago $rr/postimage
-test-chmtime =$almost_15_days_ago $rr2/preimage
+test_expect_success 'rerere clear' '
+	rm $rr/postimage &&
+	echo "$sha1	a1" | perl -pe "y/\012/\000/" >.git/MERGE_RR &&
+	git rerere clear &&
+	! test -d $rr
+'
 
-test_expect_success 'garbage collection (part1)' 'git rerere gc'
+test_expect_success 'set up for garbage collection tests' '
+	mkdir -p $rr &&
+	echo Hello >$rr/preimage &&
+	echo World >$rr/postimage &&
 
-test_expect_success 'young or recently used records still live' \
-	"test -f $rr/preimage && test -f $rr2/preimage"
+	sha2=4000000000000000000000000000000000000000 &&
+	rr2=.git/rr-cache/$sha2 &&
+	mkdir $rr2 &&
+	echo Hello >$rr2/preimage &&
 
-test-chmtime =$just_over_60_days_ago $rr/postimage
-test-chmtime =$just_over_15_days_ago $rr2/preimage
+	almost_15_days_ago=$((60-15*86400)) &&
+	just_over_15_days_ago=$((-1-15*86400)) &&
+	almost_60_days_ago=$((60-60*86400)) &&
+	just_over_60_days_ago=$((-1-60*86400)) &&
 
-test_expect_success 'garbage collection (part2)' 'git rerere gc'
+	test-chmtime =$just_over_60_days_ago $rr/preimage &&
+	test-chmtime =$almost_60_days_ago $rr/postimage &&
+	test-chmtime =$almost_15_days_ago $rr2/preimage
+'
 
-test_expect_success 'old records rest in peace' \
-	"test ! -f $rr/preimage && test ! -f $rr2/preimage"
+test_expect_success 'gc preserves young or recently used records' '
+	git rerere gc &&
+	test -f $rr/preimage &&
+	test -f $rr2/preimage
+'
 
-test_expect_success 'file2 added differently in two branches' '
+test_expect_success 'old records rest in peace' '
+	test-chmtime =$just_over_60_days_ago $rr/postimage &&
+	test-chmtime =$just_over_15_days_ago $rr2/preimage &&
+	git rerere gc &&
+	! test -f $rr/preimage &&
+	! test -f $rr2/preimage
+'
+
+test_expect_success 'setup: file2 added differently in two branches' '
 	git reset --hard &&
+
 	git checkout -b fourth &&
-	echo Hallo > file2 &&
+	echo Hallo >file2 &&
 	git add file2 &&
+	test_tick &&
 	git commit -m version1 &&
+
 	git checkout third &&
-	echo Bello > file2 &&
+	echo Bello >file2 &&
 	git add file2 &&
+	test_tick &&
 	git commit -m version2 &&
+
 	test_must_fail git merge fourth &&
-	echo Cello > file2 &&
+	echo Cello >file2 &&
 	git add file2 &&
 	git commit -m resolution
 '
 
 test_expect_success 'resolution was recorded properly' '
+	echo Cello >expected &&
+
 	git reset --hard HEAD~2 &&
 	git checkout -b fifth &&
-	echo Hallo > file3 &&
+
+	echo Hallo >file3 &&
 	git add file3 &&
+	test_tick &&
 	git commit -m version1 &&
+
 	git checkout third &&
-	echo Bello > file3 &&
+	echo Bello >file3 &&
 	git add file3 &&
+	test_tick &&
 	git commit -m version2 &&
 	git tag version2 &&
+
 	test_must_fail git merge fifth &&
-	test Cello = "$(cat file3)" &&
-	test 0 != $(git ls-files -u | wc -l)
+	test_cmp expected file3 &&
+	test_must_fail git update-index --refresh
 '
 
 test_expect_success 'rerere.autoupdate' '
-	git config rerere.autoupdate true
+	git config rerere.autoupdate true &&
 	git reset --hard &&
 	git checkout version2 &&
 	test_must_fail git merge fifth &&
-	test 0 = $(git ls-files -u | wc -l)
+	git update-index --refresh
 '
 
 test_expect_success 'merge --rerere-autoupdate' '
-	git config --unset rerere.autoupdate
+	test_might_fail git config --unset rerere.autoupdate &&
 	git reset --hard &&
 	git checkout version2 &&
 	test_must_fail git merge --rerere-autoupdate fifth &&
-	test 0 = $(git ls-files -u | wc -l)
+	git update-index --refresh
 '
 
 test_expect_success 'merge --no-rerere-autoupdate' '
-	git config rerere.autoupdate true
+	headblob=$(git rev-parse version2:file3) &&
+	mergeblob=$(git rev-parse fifth:file3) &&
+	cat >expected <<-EOF &&
+	100644 $headblob 2	file3
+	100644 $mergeblob 3	file3
+	EOF
+
+	git config rerere.autoupdate true &&
 	git reset --hard &&
 	git checkout version2 &&
 	test_must_fail git merge --no-rerere-autoupdate fifth &&
-	test 2 = $(git ls-files -u | wc -l)
+	git ls-files -u >actual &&
+	test_cmp expected actual
+'
+
+test_expect_success 'set up an unresolved merge' '
+	headblob=$(git rev-parse version2:file3) &&
+	mergeblob=$(git rev-parse fifth:file3) &&
+	cat >expected.unresolved <<-EOF &&
+	100644 $headblob 2	file3
+	100644 $mergeblob 3	file3
+	EOF
+
+	test_might_fail git config --unset rerere.autoupdate &&
+	git reset --hard &&
+	git checkout version2 &&
+	fifth=$(git rev-parse fifth) &&
+	echo "$fifth		branch 'fifth' of ." |
+	git fmt-merge-msg >msg &&
+	ancestor=$(git merge-base version2 fifth) &&
+	test_must_fail git merge-recursive "$ancestor" -- HEAD fifth &&
+
+	git ls-files --stage >failedmerge &&
+	cp file3 file3.conflict &&
+
+	git ls-files -u >actual &&
+	test_cmp expected.unresolved actual
+'
+
+test_expect_success 'explicit rerere' '
+	test_might_fail git config --unset rerere.autoupdate &&
+	git rm -fr --cached . &&
+	git update-index --index-info <failedmerge &&
+	cp file3.conflict file3 &&
+	test_must_fail git update-index --refresh -q &&
+
+	git rerere &&
+	git ls-files -u >actual &&
+	test_cmp expected.unresolved actual
+'
+
+test_expect_success 'explicit rerere with autoupdate' '
+	git config rerere.autoupdate true &&
+	git rm -fr --cached . &&
+	git update-index --index-info <failedmerge &&
+	cp file3.conflict file3 &&
+	test_must_fail git update-index --refresh -q &&
+
+	git rerere &&
+	git update-index --refresh
+'
+
+test_expect_success 'explicit rerere --rerere-autoupdate overrides' '
+	git config rerere.autoupdate false &&
+	git rm -fr --cached . &&
+	git update-index --index-info <failedmerge &&
+	cp file3.conflict file3 &&
+	git rerere &&
+	git ls-files -u >actual1 &&
+
+	git rm -fr --cached . &&
+	git update-index --index-info <failedmerge &&
+	cp file3.conflict file3 &&
+	git rerere --rerere-autoupdate &&
+	git update-index --refresh &&
+
+	git rm -fr --cached . &&
+	git update-index --index-info <failedmerge &&
+	cp file3.conflict file3 &&
+	git rerere --rerere-autoupdate --no-rerere-autoupdate &&
+	git ls-files -u >actual2 &&
+
+	git rm -fr --cached . &&
+	git update-index --index-info <failedmerge &&
+	cp file3.conflict file3 &&
+	git rerere --rerere-autoupdate --no-rerere-autoupdate --rerere-autoupdate &&
+	git update-index --refresh &&
+
+	test_cmp expected.unresolved actual1 &&
+	test_cmp expected.unresolved actual2
+'
+
+test_expect_success 'rerere --no-no-rerere-autoupdate' '
+	git rm -fr --cached . &&
+	git update-index --index-info <failedmerge &&
+	cp file3.conflict file3 &&
+	test_must_fail git rerere --no-no-rerere-autoupdate 2>err &&
+	grep [Uu]sage err &&
+	test_must_fail git update-index --refresh
+'
+
+test_expect_success 'rerere -h' '
+	test_must_fail git rerere -h >help &&
+	grep [Uu]sage help
 '
 
 test_done

File t/t6038-merge-text-auto.sh

+#!/bin/sh
+
+test_description='CRLF merge conflict across text=auto change
+
+* [master] remove .gitattributes
+ ! [side] add line from b
+--
+ + [side] add line from b
+*  [master] remove .gitattributes
+*  [master^] add line from a
+*  [master~2] normalize file
+*+ [side^] Initial
+'
+
+. ./test-lib.sh
+
+test_expect_success setup '
+	git config core.autocrlf false &&
+
+	echo first line | append_cr >file &&
+	echo first line >control_file &&
+	echo only line >inert_file &&
+
+	git add file control_file inert_file &&
+	test_tick &&
+	git commit -m "Initial" &&
+	git tag initial &&
+	git branch side &&
+
+	echo "* text=auto" >.gitattributes &&
+	touch file &&
+	git add .gitattributes file &&
+	test_tick &&
+	git commit -m "normalize file" &&
+
+	echo same line | append_cr >>file &&
+	echo same line >>control_file &&
+	git add file control_file &&
+	test_tick &&
+	git commit -m "add line from a" &&
+	git tag a &&
+
+	git rm .gitattributes &&
+	rm file &&
+	git checkout file &&
+	test_tick &&
+	git commit -m "remove .gitattributes" &&
+	git tag c &&
+
+	git checkout side &&
+	echo same line | append_cr >>file &&
+	echo same line >>control_file &&
+	git add file control_file &&
+	test_tick &&
+	git commit -m "add line from b" &&
+	git tag b &&
+
+	git checkout master
+'
+
+test_expect_success 'set up fuzz_conflict() helper' '
+	fuzz_conflict() {
+		sed -e "s/^\([<>=]......\) .*/\1/" "$@"
+	}
+'
+
+test_expect_success 'Merge after setting text=auto' '
+	cat <<-\EOF >expected &&
+	first line
+	same line
+	EOF
+
+	git config merge.renormalize true &&
+	git rm -fr . &&
+	rm -f .gitattributes &&
+	git reset --hard a &&
+	git merge b &&
+	test_cmp expected file
+'
+
+test_expect_success 'Merge addition of text=auto' '
+	cat <<-\EOF >expected &&
+	first line
+	same line
+	EOF
+
+	git config merge.renormalize true &&
+	git rm -fr . &&
+	rm -f .gitattributes &&
+	git reset --hard b &&
+	git merge a &&
+	test_cmp expected file
+'
+
+test_expect_success 'Detect CRLF/LF conflict after setting text=auto' '
+	q_to_cr <<-\EOF >expected &&
+	<<<<<<<
+	first line
+	same line
+	=======
+	first lineQ
+	same lineQ
+	>>>>>>>
+	EOF
+
+	git config merge.renormalize false &&
+	rm -f .gitattributes &&
+	git reset --hard a &&
+	test_must_fail git merge b &&
+	fuzz_conflict file >file.fuzzy &&
+	test_cmp expected file.fuzzy
+'
+
+test_expect_success 'Detect LF/CRLF conflict from addition of text=auto' '
+	q_to_cr <<-\EOF >expected &&
+	<<<<<<<
+	first lineQ
+	same lineQ
+	=======
+	first line
+	same line
+	>>>>>>>
+	EOF
+
+	git config merge.renormalize false &&
+	rm -f .gitattributes &&
+	git reset --hard b &&
+	test_must_fail git merge a &&
+	fuzz_conflict file >file.fuzzy &&
+	test_cmp expected file.fuzzy
+'
+
+test_expect_failure 'checkout -m after setting text=auto' '
+	cat <<-\EOF >expected &&
+	first line
+	same line
+	EOF
+
+	git config merge.renormalize true &&
+	git rm -fr . &&
+	rm -f .gitattributes &&
+	git reset --hard initial &&
+	git checkout a -- . &&
+	git checkout -m b &&
+	test_cmp expected file
+'
+
+test_expect_failure 'checkout -m addition of text=auto' '
+	cat <<-\EOF >expected &&
+	first line
+	same line
+	EOF
+
+	git config merge.renormalize true &&
+	git rm -fr . &&
+	rm -f .gitattributes file &&
+	git reset --hard initial &&
+	git checkout b -- . &&
+	git checkout -m a &&
+	test_cmp expected file
+'
+
+test_expect_failure 'cherry-pick patch from after text=auto was added' '
+	append_cr <<-\EOF >expected &&
+	first line
+	same line
+	EOF
+
+	git config merge.renormalize true &&
+	git rm -fr . &&
+	git reset --hard b &&
+	test_must_fail git cherry-pick a >err 2>&1 &&
+	grep "[Nn]othing added" err &&
+	test_cmp expected file
+'
+
+test_expect_success 'Test delete/normalize conflict' '
+	git checkout -f side &&
+	git rm -fr . &&
+	rm -f .gitattributes &&
+	git reset --hard initial &&
+	git rm file &&
+	git commit -m "remove file" &&
+	git checkout master &&
+	git reset --hard a^ &&
+	git merge side
+'
+
+test_done