[PATCH] read-tree --emu23.
authorJunio C Hamano <junkio@cox.net>
Sat, 11 Jun 2005 01:36:08 +0000 (18:36 -0700)
committerLinus Torvalds <torvalds@ppc970.osdl.org>
Mon, 13 Jun 2005 03:40:20 +0000 (20:40 -0700)
This new flag causes two-way fast forward to internally use the
three-way merge mechanism.  This behaviour is intended to offer
a better fast forward semantics when used in a dirty work tree.

The new test t1005 is parallel to the existing t1001 "pure
2-way" tests, but some parts that are commented out would fail.
These failures are due to three-way merge enforcing too strict
index requirements for cases that could succeed.  This problem
will be addressed by later patches.

Without even changing three-way mechanism, the --emu23 two-way
fast forward already gives the user an easier-to-handle merge
result when a file that "merged head" updates has local
modifications.  This is demonstrated as "case 16" test in t1005.

Signed-off-by: Junio C Hamano <junkio@cox.net>
Signed-off-by: Linus Torvalds <torvalds@osdl.org>
read-tree.c
t/t1005-read-tree-m-2way-emu23.sh [new file with mode: 0644]

index ee7a46c909efbc78c59b49d2147165297158882e..f2a8bb53a1c4338b0bccb6a44df5cdcdb8d74aed 100644 (file)
@@ -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 (file)
index 0000000..3345c9d
--- /dev/null
@@ -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 <Documentation/git-rev-tree.txt>.
+
+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 <<EOF
+-100644 X 0    nitfol
++100644 X 0    nitfol
+EOF
+
+test_expect_success \
+    '14 - unchanged in two heads.' \
+    'rm -f .git/index &&
+     echo nitfol nitfol >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