diff --git a/read-tree.c b/read-tree.c index ee7a46c909..f2a8bb53a1 100644 --- a/read-tree.c +++ b/read-tree.c @@ -209,6 +209,58 @@ static int twoway_merge(struct cache_entry **src, struct cache_entry **dst) return deleted_entry(oldtree, current, dst); } +/* + * Two-way merge emulated with three-way merge. + * + * This treats "read-tree -m H M" by transforming it internally + * into "read-tree -m H I+H M", where I+H is a tree that would + * contain the contents of the current index file, overlayed on + * top of H. Unlike the traditional two-way merge, this leaves + * the stages in the resulting index file and lets the user resolve + * the merge conflicts using standard tools for three-way merge. + * + * This function is just to set-up such an arrangement, and the + * actual merge uses threeway_merge() function. + */ +static void setup_emu23(void) +{ + /* stage0 contains I, stage1 H, stage2 M. + * move stage2 to stage3, and create stage2 entries + * by scanning stage0 and stage1 entries. + */ + int i, namelen, size; + struct cache_entry *ce, *stage2; + + for (i = 0; i < active_nr; i++) { + ce = active_cache[i]; + if (ce_stage(ce) != 2) + continue; + /* hoist them up to stage 3 */ + namelen = ce_namelen(ce); + ce->ce_flags = create_ce_flags(namelen, 3); + } + + for (i = 0; i < active_nr; i++) { + ce = active_cache[i]; + if (ce_stage(ce) > 1) + continue; + namelen = ce_namelen(ce); + size = cache_entry_size(namelen); + stage2 = xmalloc(size); + memcpy(stage2, ce, size); + stage2->ce_flags = create_ce_flags(namelen, 2); + if (add_cache_entry(stage2, ADD_CACHE_OK_TO_ADD) < 0) + die("cannot merge index and our head tree"); + + /* We are done with this name, so skip to next name */ + while (i < active_nr && + ce_namelen(active_cache[i]) == namelen && + !memcmp(active_cache[i]->name, ce->name, namelen)) + i++; + i--; /* compensate for the loop control */ + } +} + /* * One-way merge. * @@ -315,7 +367,7 @@ static struct cache_file cache_file; int main(int argc, char **argv) { - int i, newfd, merge, reset; + int i, newfd, merge, reset, emu23; unsigned char sha1[20]; newfd = hold_index_file_for_update(&cache_file, get_index_file()); @@ -324,6 +376,7 @@ int main(int argc, char **argv) merge = 0; reset = 0; + emu23 = 0; for (i = 1; i < argc; i++) { const char *arg = argv[i]; @@ -335,7 +388,7 @@ int main(int argc, char **argv) /* This differs from "-m" in that we'll silently ignore unmerged entries */ if (!strcmp(arg, "--reset")) { - if (stage || merge) + if (stage || merge || emu23) usage(read_tree_usage); reset = 1; merge = 1; @@ -345,7 +398,7 @@ int main(int argc, char **argv) /* "-m" stands for "merge", meaning we start in stage 1 */ if (!strcmp(arg, "-m")) { - if (stage || merge) + if (stage || merge || emu23) usage(read_tree_usage); if (read_cache_unmerged()) die("you need to resolve your current index first"); @@ -353,6 +406,17 @@ int main(int argc, char **argv) merge = 1; continue; } + + /* "-emu23" uses 3-way merge logic to perform fast-forward */ + if (!strcmp(arg, "--emu23")) { + if (stage || merge || emu23) + usage(read_tree_usage); + if (read_cache_unmerged()) + die("you need to resolve your current index first"); + merge = emu23 = stage = 1; + continue; + } + if (get_sha1(arg, sha1) < 0) usage(read_tree_usage); if (stage > 3) @@ -369,9 +433,18 @@ int main(int argc, char **argv) [2] = twoway_merge, [3] = threeway_merge, }; + merge_fn_t fn; + if (stage < 2 || stage > 4) die("just how do you expect me to merge %d trees?", stage-1); - merge_cache(active_cache, active_nr, merge_function[stage-1]); + if (emu23 && stage != 3) + die("--emu23 takes only two trees"); + fn = merge_function[stage-1]; + if (stage == 3 && emu23) { + setup_emu23(); + fn = merge_function[3]; + } + merge_cache(active_cache, active_nr, fn); } if (write_cache(newfd, active_cache, active_nr) || commit_index_file(&cache_file)) diff --git a/t/t1005-read-tree-m-2way-emu23.sh b/t/t1005-read-tree-m-2way-emu23.sh new file mode 100644 index 0000000000..3345c9dd3f --- /dev/null +++ b/t/t1005-read-tree-m-2way-emu23.sh @@ -0,0 +1,316 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# + +test_description='Two way merge with read-tree --emu23 $H $M + +This test tries two-way merge (aka fast forward with carry forward). + +There is the head (called H) and another commit (called M), which is +simply ahead of H. The index and the work tree contains a state that +is derived from H, but may also have local changes. This test checks +all the combinations described in the two-tree merge "carry forward" +rules, found in . + +In the test, these paths are used: + bozbar - in H, stays in M, modified from bozbar to gnusto + frotz - not in H added in M + nitfol - in H, stays in M unmodified + rezrov - in H, deleted in M + yomin - not in H nor M +' +. ./test-lib.sh + +read_tree_twoway () { + git-read-tree --emu23 "$1" "$2" && + git-ls-files --stage && + git-merge-cache git-merge-one-file-script -a && + git-ls-files --stage +} + +_x40='[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]' +_x40="$_x40$_x40$_x40$_x40$_x40$_x40$_x40$_x40" +compare_change () { + cat current + sed -n >current \ + -e '/^--- /d; /^+++ /d; /^@@ /d;' \ + -e 's/^\([-+][0-7][0-7][0-7][0-7][0-7][0-7]\) '"$_x40"' /\1 X /p' \ + "$1" + diff -u expected current +} + +check_cache_at () { + clean_if_empty=`git-diff-files "$1"` + case "$clean_if_empty" in + '') echo "$1: clean" ;; + ?*) echo "$1: dirty" ;; + esac + case "$2,$clean_if_empty" in + clean,) : ;; + clean,?*) false ;; + dirty,) false ;; + dirty,?*) : ;; + esac +} + +check_stages () { + cat >expected_stages + git-ls-files --stage | sed -e "s/ $_x40 / X /" >current_stages + diff -u expected_stages current_stages +} + +test_expect_success \ + setup \ + 'echo frotz >frotz && + echo nitfol >nitfol && + echo bozbar >bozbar && + echo rezrov >rezrov && + echo yomin >yomin && + git-update-cache --add nitfol bozbar rezrov && + treeH=`git-write-tree` && + echo treeH $treeH && + git-ls-tree $treeH && + + echo gnusto >bozbar && + git-update-cache --add frotz bozbar --force-remove rezrov && + git-ls-files --stage >M.out && + treeM=`git-write-tree` && + echo treeM $treeM && + git-ls-tree $treeM && + git-diff-tree $treeH $treeM' + +test_expect_success \ + '1, 2, 3 - no carry forward' \ + 'rm -f .git/index && + read_tree_twoway $treeH $treeM && + git-ls-files --stage >1-3.out && + diff -u M.out 1-3.out && + check_cache_at bozbar dirty && + check_cache_at frotz clean && # different from pure 2-way + check_cache_at nitfol dirty' + +echo '+100644 X 0 yomin' >expected + +test_expect_success \ + '4 - carry forward local addition.' \ + 'rm -f .git/index && + git-update-cache --add yomin && + read_tree_twoway $treeH $treeM && + git-ls-files --stage >4.out || exit + diff -u M.out 4.out >4diff.out + compare_change 4diff.out expected && + check_cache_at yomin clean' + +# "read-tree -m H I+H M" where !H && !M; so (I+H) not being up-to-date +# should not matter, but without #3ALT this does not work. +: test_expect_success \ + '5 - carry forward local addition.' \ + 'rm -f .git/index && + echo yomin >yomin && + git-update-cache --add yomin && + echo yomin yomin >yomin && + read_tree_twoway $treeH $treeM && + git-ls-files --stage >5.out || exit + diff -u M.out 5.out >5diff.out + compare_change 5diff.out expected && + check_cache_at yomin dirty' + +# "read-tree -m H I+H M" where !H && M && (I+H) == M, so this should +# succeed (even the entry is clean), but without #5ALT this does not +# work. +: test_expect_success \ + '6 - local addition already has the same.' \ + 'rm -f .git/index && + git-update-cache --add frotz && + read_tree_twoway $treeH $treeM && + git-ls-files --stage >6.out && + diff -u M.out 6.out && + check_cache_at frotz clean' + +# Exactly the same pattern as above but with dirty cache. This also +# should succeed, but without #5ALT it does not. +: test_expect_success \ + '7 - local addition already has the same.' \ + 'rm -f .git/index && + echo frotz >frotz && + git-update-cache --add frotz && + echo frotz frotz >frotz && + read_tree_twoway $treeH $treeM && + git-ls-files --stage >7.out && + diff -u M.out 7.out && + check_cache_at frotz dirty' + +test_expect_success \ + '8 - conflicting addition.' \ + 'rm -f .git/index && + echo frotz frotz >frotz && + git-update-cache --add frotz && + if read_tree_twoway $treeH $treeM; then false; else :; fi' + +test_expect_success \ + '9 - conflicting addition.' \ + 'rm -f .git/index && + echo frotz frotz >frotz && + git-update-cache --add frotz && + echo frotz >frotz && + if read_tree_twoway $treeH $treeM; then false; else :; fi' + +test_expect_success \ + '10 - path removed.' \ + 'rm -f .git/index && + echo rezrov >rezrov && + git-update-cache --add rezrov && + read_tree_twoway $treeH $treeM && + git-ls-files --stage >10.out && + diff -u M.out 10.out' + +test_expect_success \ + '11 - dirty path removed.' \ + 'rm -f .git/index && + echo rezrov >rezrov && + git-update-cache --add rezrov && + echo rezrov rezrov >rezrov && + if read_tree_twoway $treeH $treeM; then false; else :; fi' + +test_expect_success \ + '12 - unmatching local changes being removed.' \ + 'rm -f .git/index && + echo rezrov rezrov >rezrov && + git-update-cache --add rezrov && + if read_tree_twoway $treeH $treeM; then false; else :; fi' + +test_expect_success \ + '13 - unmatching local changes being removed.' \ + 'rm -f .git/index && + echo rezrov rezrov >rezrov && + git-update-cache --add rezrov && + echo rezrov >rezrov && + if read_tree_twoway $treeH $treeM; then false; else :; fi' + +cat >expected <nitfol && + git-update-cache --add nitfol && + read_tree_twoway $treeH $treeM && + git-ls-files --stage >14.out || exit + diff -u M.out 14.out >14diff.out + compare_change 14diff.out expected && + check_cache_at nitfol clean' + +test_expect_success \ + '15 - unchanged in two heads.' \ + 'rm -f .git/index && + echo nitfol nitfol >nitfol && + git-update-cache --add nitfol && + echo nitfol nitfol nitfol >nitfol && + read_tree_twoway $treeH $treeM && + git-ls-files --stage >15.out || exit + diff -u M.out 15.out >15diff.out + compare_change 15diff.out expected && + check_cache_at nitfol dirty' + +# This is different from straight 2-way merge in that it leaves +# three stages of bozbar in the index file without failing, so +# the user can run git-diff-stages to examine the situation. +test_expect_success \ + '16 - conflicting local change.' \ + 'rm -f .git/index && + echo bozbar bozbar >bozbar && + git-update-cache --add bozbar && + git-read-tree --emu23 $treeH $treeM && + check_stages' <<\EOF +100644 X 1 bozbar +100644 X 2 bozbar +100644 X 3 bozbar +100644 X 3 frotz +100644 X 0 nitfol +100644 X 1 rezrov +100644 X 2 rezrov +EOF + +test_expect_success \ + '17 - conflicting local change.' \ + 'rm -f .git/index && + echo bozbar bozbar >bozbar && + git-update-cache --add bozbar && + echo bozbar bozbar bozbar >bozbar && + if read_tree_twoway $treeH $treeM; then false; else :; fi' + +test_expect_success \ + '18 - local change already having a good result.' \ + 'rm -f .git/index && + echo gnusto >bozbar && + git-update-cache --add bozbar && + read_tree_twoway $treeH $treeM && + git-ls-files --stage >18.out && + diff -u M.out 18.out && + check_cache_at bozbar clean' + +test_expect_success \ + '19 - local change already having a good result, further modified.' \ + 'rm -f .git/index && + echo gnusto >bozbar && + git-update-cache --add bozbar && + echo gnusto gnusto >bozbar && + read_tree_twoway $treeH $treeM && + git-ls-files --stage >19.out && + diff -u M.out 19.out && + check_cache_at bozbar dirty' + +test_expect_success \ + '20 - no local change, use new tree.' \ + 'rm -f .git/index && + echo bozbar >bozbar && + git-update-cache --add bozbar && + read_tree_twoway $treeH $treeM && + git-ls-files --stage >20.out && + diff -u M.out 20.out && + check_cache_at bozbar dirty' + +test_expect_success \ + '21 - no local change, dirty cache.' \ + 'rm -f .git/index && + echo bozbar >bozbar && + git-update-cache --add bozbar && + echo gnusto gnusto >bozbar && + if read_tree_twoway $treeH $treeM; then false; else :; fi' + +# Also make sure we did not break DF vs DF/DF case. +test_expect_success \ + 'DF vs DF/DF case setup.' \ + 'rm -f .git/index && + echo DF >DF && + git-update-cache --add DF && + treeDF=`git-write-tree` && + echo treeDF $treeDF && + git-ls-tree $treeDF && + + rm -f DF && + mkdir DF && + echo DF/DF >DF/DF && + git-update-cache --add --remove DF DF/DF && + treeDFDF=`git-write-tree` && + echo treeDFDF $treeDFDF && + git-ls-tree $treeDFDF && + git-ls-files --stage >DFDF.out' + +test_expect_success \ + 'DF vs DF/DF case test.' \ + 'rm -f .git/index && + rm -fr DF && + echo DF >DF && + git-update-cache --add DF && + read_tree_twoway $treeDF $treeDFDF && + git-ls-files --stage >DFDFcheck.out && + diff -u DFDF.out DFDFcheck.out && + check_cache_at DF/DF clean && # different from pure 2-way + :' + +test_done