[PATCH] read-tree --emu23.

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>
This commit is contained in:
Junio C Hamano 2005-06-10 18:36:08 -07:00 committed by Linus Torvalds
parent 76bc82ca0e
commit 03efa6d9a4
2 changed files with 393 additions and 4 deletions

View 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))

View File

@ -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