diff --git a/Documentation/merge-strategies.txt b/Documentation/merge-strategies.txt index 7bbd19b300..2eb92b9327 100644 --- a/Documentation/merge-strategies.txt +++ b/Documentation/merge-strategies.txt @@ -81,9 +81,17 @@ no-renormalize;; Disables the `renormalize` option. This overrides the `merge.renormalize` configuration variable. +no-renames;; + Turn off rename detection. + See also linkgit:git-diff[1] `--no-renames`. + +find-renames[=];; + Turn on rename detection, optionally setting the similarity + threshold. This is the default. + See also linkgit:git-diff[1] `--find-renames`. + rename-threshold=;; - Controls the similarity threshold used for rename detection. - See also linkgit:git-diff[1] `-M`. + Deprecated synonym for `find-renames=`. subtree[=];; This option is a more advanced form of 'subtree' strategy, where diff --git a/merge-recursive.c b/merge-recursive.c index 8eabde20fb..b880ae50e7 100644 --- a/merge-recursive.c +++ b/merge-recursive.c @@ -482,6 +482,9 @@ static struct string_list *get_renames(struct merge_options *o, struct diff_options opts; renames = xcalloc(1, sizeof(struct string_list)); + if (!o->detect_rename) + return renames; + diff_setup(&opts); DIFF_OPT_SET(&opts, RECURSIVE); DIFF_OPT_CLR(&opts, RENAME_EMPTY); @@ -2039,6 +2042,7 @@ void init_merge_options(struct merge_options *o) o->diff_rename_limit = -1; o->merge_rename_limit = -1; o->renormalize = 0; + o->detect_rename = 1; merge_recursive_config(o); if (getenv("GIT_MERGE_VERBOSITY")) o->verbosity = @@ -2088,9 +2092,17 @@ int parse_merge_opt(struct merge_options *o, const char *s) o->renormalize = 1; else if (!strcmp(s, "no-renormalize")) o->renormalize = 0; - else if (skip_prefix(s, "rename-threshold=", &arg)) { + else if (!strcmp(s, "no-renames")) + o->detect_rename = 0; + else if (!strcmp(s, "find-renames")) { + o->detect_rename = 1; + o->rename_score = 0; + } + else if (skip_prefix(s, "find-renames=", &arg) || + skip_prefix(s, "rename-threshold=", &arg)) { if ((o->rename_score = parse_rename_score(&arg)) == -1 || *arg != 0) return -1; + o->detect_rename = 1; } else return -1; diff --git a/merge-recursive.h b/merge-recursive.h index 9e090a3470..52f0201f68 100644 --- a/merge-recursive.h +++ b/merge-recursive.h @@ -17,6 +17,7 @@ struct merge_options { unsigned renormalize : 1; long xdl_opts; int verbosity; + int detect_rename; int diff_rename_limit; int merge_rename_limit; int rename_score; diff --git a/t/t3032-merge-recursive-options.sh b/t/t3032-merge-recursive-space-options.sh similarity index 99% rename from t/t3032-merge-recursive-options.sh rename to t/t3032-merge-recursive-space-options.sh index 4029c9c8c0..b56180ee4a 100755 --- a/t/t3032-merge-recursive-options.sh +++ b/t/t3032-merge-recursive-space-options.sh @@ -1,6 +1,6 @@ #!/bin/sh -test_description='merge-recursive options +test_description='merge-recursive space options * [master] Clarify ! [remote] Remove cruft diff --git a/t/t3034-merge-recursive-rename-options.sh b/t/t3034-merge-recursive-rename-options.sh new file mode 100755 index 0000000000..b9c4028496 --- /dev/null +++ b/t/t3034-merge-recursive-rename-options.sh @@ -0,0 +1,312 @@ +#!/bin/sh + +test_description='merge-recursive rename options + +Test rename detection by examining rename/delete conflicts. + +* (HEAD -> rename) rename +| * (master) delete +|/ +* base + +git diff --name-status base master +D 0-old +D 1-old +D 2-old +D 3-old + +git diff --name-status -M01 base rename +R025 0-old 0-new +R050 1-old 1-new +R075 2-old 2-new +R100 3-old 3-new + +Actual similarity indices are parsed from diff output. We rely on the fact that +they are rounded down (see, e.g., Documentation/diff-generate-patch.txt, which +mentions this in a different context). +' + +. ./test-lib.sh + +get_expected_stages () { + git checkout rename -- $1-new && + git ls-files --stage $1-new >expected-stages-undetected-$1 && + sed "s/ 0 / 2 /" expected-stages-detected-$1 && + git read-tree -u --reset HEAD +} + +rename_detected () { + git ls-files --stage $1-old $1-new >stages-actual-$1 && + test_cmp expected-stages-detected-$1 stages-actual-$1 +} + +rename_undetected () { + git ls-files --stage $1-old $1-new >stages-actual-$1 && + test_cmp expected-stages-undetected-$1 stages-actual-$1 +} + +check_common () { + git ls-files --stage >stages-actual && + test_line_count = 4 stages-actual +} + +check_threshold_0 () { + check_common && + rename_detected 0 && + rename_detected 1 && + rename_detected 2 && + rename_detected 3 +} + +check_threshold_1 () { + check_common && + rename_undetected 0 && + rename_detected 1 && + rename_detected 2 && + rename_detected 3 +} + +check_threshold_2 () { + check_common && + rename_undetected 0 && + rename_undetected 1 && + rename_detected 2 && + rename_detected 3 +} + +check_exact_renames () { + check_common && + rename_undetected 0 && + rename_undetected 1 && + rename_undetected 2 && + rename_detected 3 +} + +check_no_renames () { + check_common && + rename_undetected 0 && + rename_undetected 1 && + rename_undetected 2 && + rename_undetected 3 +} + +test_expect_success 'setup repo' ' + cat <<-\EOF >3-old && + 33a + 33b + 33c + 33d + EOF + sed s/33/22/ <3-old >2-old && + sed s/33/11/ <3-old >1-old && + sed s/33/00/ <3-old >0-old && + git add [0-3]-old && + git commit -m base && + git rm [0-3]-old && + git commit -m delete && + git checkout -b rename HEAD^ && + cp 3-old 3-new && + sed 1,1s/./x/ <2-old >2-new && + sed 1,2s/./x/ <1-old >1-new && + sed 1,3s/./x/ <0-old >0-new && + git add [0-3]-new && + git rm [0-3]-old && + git commit -m rename && + get_expected_stages 0 && + get_expected_stages 1 && + get_expected_stages 2 && + get_expected_stages 3 && + check_50="false" && + tail="HEAD^ -- HEAD master" +' + +test_expect_success 'setup thresholds' ' + git diff --name-status -M01 HEAD^ HEAD >diff-output && + test_debug "cat diff-output" && + test_line_count = 4 diff-output && + grep "R[0-9][0-9][0-9] \([0-3]\)-old \1-new" diff-output \ + >grep-output && + test_cmp diff-output grep-output && + th0=$(sed -n "s/R\(...\) 0-old 0-new/\1/p" diff-output-0 && + git diff --name-status -M$th1 --diff-filter=R HEAD^ HEAD \ + >diff-output-1 && + git diff --name-status -M$th2 --diff-filter=R HEAD^ HEAD \ + >diff-output-2 && + git diff --name-status -M100% --diff-filter=R HEAD^ HEAD \ + >diff-output-3 && + test_line_count = 4 diff-output-0 && + test_line_count = 3 diff-output-1 && + test_line_count = 2 diff-output-2 && + test_line_count = 1 diff-output-3 +' + +test_expect_success 'default similarity threshold is 50%' ' + git read-tree --reset -u HEAD && + test_must_fail git merge-recursive $tail && + $check_50 +' + +test_expect_success 'low rename threshold' ' + git read-tree --reset -u HEAD && + test_must_fail git merge-recursive --find-renames=$th0 $tail && + check_threshold_0 +' + +test_expect_success 'medium rename threshold' ' + git read-tree --reset -u HEAD && + test_must_fail git merge-recursive --find-renames=$th1 $tail && + check_threshold_1 +' + +test_expect_success 'high rename threshold' ' + git read-tree --reset -u HEAD && + test_must_fail git merge-recursive --find-renames=$th2 $tail && + check_threshold_2 +' + +test_expect_success 'exact renames only' ' + git read-tree --reset -u HEAD && + test_must_fail git merge-recursive --find-renames=100% $tail && + check_exact_renames +' + +test_expect_success 'rename threshold is truncated' ' + git read-tree --reset -u HEAD && + test_must_fail git merge-recursive --find-renames=200% $tail && + check_exact_renames +' + +test_expect_success 'disabled rename detection' ' + git read-tree --reset -u HEAD && + git merge-recursive --no-renames $tail && + check_no_renames +' + +test_expect_success 'last wins in --find-renames= --find-renames=' ' + git read-tree --reset -u HEAD && + test_must_fail git merge-recursive \ + --find-renames=$th0 --find-renames=$th2 $tail && + check_threshold_2 +' + +test_expect_success '--find-renames resets threshold' ' + git read-tree --reset -u HEAD && + test_must_fail git merge-recursive \ + --find-renames=$th0 --find-renames $tail && + $check_50 +' + +test_expect_success 'last wins in --no-renames --find-renames' ' + git read-tree --reset -u HEAD && + test_must_fail git merge-recursive --no-renames --find-renames $tail && + $check_50 +' + +test_expect_success 'last wins in --find-renames --no-renames' ' + git read-tree --reset -u HEAD && + git merge-recursive --find-renames --no-renames $tail && + check_no_renames +' + +test_expect_success 'assumption for further tests: trivial merge succeeds' ' + git read-tree --reset -u HEAD && + git merge-recursive HEAD -- HEAD HEAD && + git diff --quiet --cached && + git merge-recursive --find-renames=$th0 HEAD -- HEAD HEAD && + git diff --quiet --cached && + git merge-recursive --find-renames=$th2 HEAD -- HEAD HEAD && + git diff --quiet --cached && + git merge-recursive --find-renames=100% HEAD -- HEAD HEAD && + git diff --quiet --cached && + git merge-recursive --no-renames HEAD -- HEAD HEAD && + git diff --quiet --cached +' + +test_expect_success '--find-renames rejects negative argument' ' + git read-tree --reset -u HEAD && + test_must_fail git merge-recursive --find-renames=-25 \ + HEAD -- HEAD HEAD && + git diff --quiet --cached +' + +test_expect_success '--find-renames rejects non-numbers' ' + git read-tree --reset -u HEAD && + test_must_fail git merge-recursive --find-renames=0xf \ + HEAD -- HEAD HEAD && + git diff --quiet --cached +' + +test_expect_success 'rename-threshold= is a synonym for find-renames=' ' + git read-tree --reset -u HEAD && + test_must_fail git merge-recursive --rename-threshold=$th0 $tail && + check_threshold_0 +' + +test_expect_success 'last wins in --no-renames --rename-threshold=' ' + git read-tree --reset -u HEAD && + test_must_fail git merge-recursive --no-renames --rename-threshold=$th0 $tail && + check_threshold_0 +' + +test_expect_success 'last wins in --rename-threshold= --no-renames' ' + git read-tree --reset -u HEAD && + git merge-recursive --rename-threshold=$th0 --no-renames $tail && + check_no_renames +' + +test_expect_success '--rename-threshold= rejects negative argument' ' + git read-tree --reset -u HEAD && + test_must_fail git merge-recursive --rename-threshold=-25 \ + HEAD -- HEAD HEAD && + git diff --quiet --cached +' + +test_expect_success '--rename-threshold= rejects non-numbers' ' + git read-tree --reset -u HEAD && + test_must_fail git merge-recursive --rename-threshold=0xf \ + HEAD -- HEAD HEAD && + git diff --quiet --cached +' + +test_expect_success 'last wins in --rename-threshold= --find-renames=' ' + git read-tree --reset -u HEAD && + test_must_fail git merge-recursive \ + --rename-threshold=$th0 --find-renames=$th2 $tail && + check_threshold_2 +' + +test_expect_success 'last wins in --find-renames= --rename-threshold=' ' + git read-tree --reset -u HEAD && + test_must_fail git merge-recursive \ + --find-renames=$th2 --rename-threshold=$th0 $tail && + check_threshold_0 +' + +test_done