Merge branch 'cc/reset-more'

* cc/reset-more:
  t7111: check that reset options work as described in the tables
  Documentation: reset: add some missing tables
  Fix bit assignment for CE_CONFLICTED
  "reset --merge": fix unmerged case
  reset: use "unpack_trees()" directly instead of "git read-tree"
  reset: add a few tests for "git reset --merge"
  Documentation: reset: add some tables to describe the different options
  reset: improve mixed reset error message when in a bare repo
This commit is contained in:
Junio C Hamano 2010-01-13 11:58:56 -08:00
commit dc96c5ee70
7 changed files with 435 additions and 20 deletions

View File

@ -68,6 +68,95 @@ linkgit:git-add[1]).
<commit>::
Commit to make the current HEAD. If not given defaults to HEAD.
DISCUSSION
----------
The tables below show what happens when running:
----------
git reset --option target
----------
to reset the HEAD to another commit (`target`) with the different
reset options depending on the state of the files.
In these tables, A, B, C and D are some different states of a
file. For example, the first line of the first table means that if a
file is in state A in the working tree, in state B in the index, in
state C in HEAD and in state D in the target, then "git reset --soft
target" will put the file in state A in the working tree, in state B
in the index and in state D in HEAD.
working index HEAD target working index HEAD
----------------------------------------------------
A B C D --soft A B D
--mixed A D D
--hard D D D
--merge (disallowed)
working index HEAD target working index HEAD
----------------------------------------------------
A B C C --soft A B C
--mixed A C C
--hard C C C
--merge (disallowed)
working index HEAD target working index HEAD
----------------------------------------------------
B B C D --soft B B D
--mixed B D D
--hard D D D
--merge D D D
working index HEAD target working index HEAD
----------------------------------------------------
B B C C --soft B B C
--mixed B C C
--hard C C C
--merge C C C
working index HEAD target working index HEAD
----------------------------------------------------
B C C D --soft B C D
--mixed B D D
--hard D D D
--merge (disallowed)
working index HEAD target working index HEAD
----------------------------------------------------
B C C C --soft B C C
--mixed B C C
--hard C C C
--merge B C C
"reset --merge" is meant to be used when resetting out of a conflicted
merge. Any mergy operation guarantees that the work tree file that is
involved in the merge does not have local change wrt the index before
it starts, and that it writes the result out to the work tree. So if
we see some difference between the index and the target and also
between the index and the work tree, then it means that we are not
resetting out from a state that a mergy operation left after failing
with a conflict. That is why we disallow --merge option in this case.
The following tables show what happens when there are unmerged
entries:
working index HEAD target working index HEAD
----------------------------------------------------
X U A B --soft (disallowed)
--mixed X B B
--hard B B B
--merge B B B
working index HEAD target working index HEAD
----------------------------------------------------
X U A A --soft (disallowed)
--mixed X A A
--hard A A A
--merge A A A
X means any state and U means an unmerged index.
Examples
--------

View File

@ -18,6 +18,8 @@
#include "tree.h"
#include "branch.h"
#include "parse-options.h"
#include "unpack-trees.h"
#include "cache-tree.h"
static const char * const git_reset_usage[] = {
"git reset [--mixed | --soft | --hard | --merge] [-q] [<commit>]",
@ -54,27 +56,44 @@ static inline int is_merge(void)
static int reset_index_file(const unsigned char *sha1, int reset_type, int quiet)
{
int i = 0;
const char *args[6];
int nr = 1;
int newfd;
struct tree_desc desc[2];
struct unpack_trees_options opts;
struct lock_file *lock = xcalloc(1, sizeof(struct lock_file));
args[i++] = "read-tree";
memset(&opts, 0, sizeof(opts));
opts.head_idx = 1;
opts.src_index = &the_index;
opts.dst_index = &the_index;
opts.fn = oneway_merge;
opts.merge = 1;
if (!quiet)
args[i++] = "-v";
opts.verbose_update = 1;
switch (reset_type) {
case MERGE:
args[i++] = "-u";
args[i++] = "-m";
opts.update = 1;
break;
case HARD:
args[i++] = "-u";
opts.update = 1;
/* fallthrough */
default:
args[i++] = "--reset";
opts.reset = 1;
}
args[i++] = sha1_to_hex(sha1);
args[i] = NULL;
return run_command_v_opt(args, RUN_GIT_CMD);
newfd = hold_locked_index(lock, 1);
read_cache_unmerged();
if (!fill_tree_descriptor(desc + nr - 1, sha1))
return error("Failed to find tree of %s.", sha1_to_hex(sha1));
if (unpack_trees(nr, desc, &opts))
return -1;
if (write_cache(newfd, active_cache, active_nr) ||
commit_locked_index(lock))
return error("Could not write new index file.");
return 0;
}
static void print_new_head_line(struct commit *commit)
@ -288,6 +307,10 @@ int cmd_reset(int argc, const char **argv, const char *prefix)
if (reset_type == HARD || reset_type == MERGE)
setup_work_tree();
if (reset_type == MIXED && is_bare_repository())
die("%s reset is not allowed in a bare repository",
reset_type_names[reset_type]);
/* Soft reset does not touch the index file nor the working tree
* at all, but requires them in a good order. Other resets reset
* the index file to the tree object we are switching to. */

View File

@ -177,6 +177,7 @@ struct cache_entry {
#define CE_HASHED (0x100000)
#define CE_UNHASHED (0x200000)
#define CE_CONFLICTED (0x800000)
/* Only remove in work directory, not index */
#define CE_WT_REMOVE (0x400000)

View File

@ -1617,9 +1617,8 @@ int read_index_unmerged(struct index_state *istate)
len = strlen(ce->name);
size = cache_entry_size(len);
new_ce = xcalloc(1, size);
hashcpy(new_ce->sha1, ce->sha1);
memcpy(new_ce->name, ce->name, len);
new_ce->ce_flags = create_ce_flags(len, 0);
new_ce->ce_flags = create_ce_flags(len, 0) | CE_CONFLICTED;
new_ce->ce_mode = ce->ce_mode;
if (add_index_entry(istate, new_ce, 0))
return error("%s: cannot drop to stage #0",

183
t/t7110-reset-merge.sh Executable file
View File

@ -0,0 +1,183 @@
#!/bin/sh
#
# Copyright (c) 2009 Christian Couder
#
test_description='Tests for "git reset --merge"'
. ./test-lib.sh
test_expect_success setup '
for i in 1 2 3; do echo line $i; done >file1 &&
cat file1 >file2 &&
git add file1 file2 &&
test_tick &&
git commit -m "Initial commit" &&
git tag initial &&
echo line 4 >>file1 &&
cat file1 >file2 &&
test_tick &&
git commit -m "add line 4 to file1" file1 &&
git tag second
'
# The next test will test the following:
#
# working index HEAD target working index HEAD
# ----------------------------------------------------
# file1: C C C D --merge D D D
# file2: C D D D --merge C D D
test_expect_success 'reset --merge is ok with changes in file it does not touch' '
git reset --merge HEAD^ &&
! grep 4 file1 &&
grep 4 file2 &&
test "$(git rev-parse HEAD)" = "$(git rev-parse initial)" &&
test -z "$(git diff --cached)"
'
test_expect_success 'reset --merge is ok when switching back' '
git reset --merge second &&
grep 4 file1 &&
grep 4 file2 &&
test "$(git rev-parse HEAD)" = "$(git rev-parse second)" &&
test -z "$(git diff --cached)"
'
# The next test will test the following:
#
# working index HEAD target working index HEAD
# ----------------------------------------------------
# file1: B B C D --merge D D D
# file2: C D D D --merge C D D
test_expect_success 'reset --merge discards changes added to index (1)' '
git reset --hard second &&
cat file1 >file2 &&
echo "line 5" >> file1 &&
git add file1 &&
git reset --merge HEAD^ &&
! grep 4 file1 &&
! grep 5 file1 &&
grep 4 file2 &&
test "$(git rev-parse HEAD)" = "$(git rev-parse initial)" &&
test -z "$(git diff --cached)"
'
test_expect_success 'reset --merge is ok again when switching back (1)' '
git reset --hard initial &&
echo "line 5" >> file2 &&
git add file2 &&
git reset --merge second &&
! grep 4 file2 &&
! grep 5 file1 &&
grep 4 file1 &&
test "$(git rev-parse HEAD)" = "$(git rev-parse second)" &&
test -z "$(git diff --cached)"
'
# The next test will test the following:
#
# working index HEAD target working index HEAD
# ----------------------------------------------------
# file1: C C C D --merge D D D
# file2: C C D D --merge D D D
test_expect_success 'reset --merge discards changes added to index (2)' '
git reset --hard second &&
echo "line 4" >> file2 &&
git add file2 &&
git reset --merge HEAD^ &&
! grep 4 file2 &&
test "$(git rev-parse HEAD)" = "$(git rev-parse initial)" &&
test -z "$(git diff)" &&
test -z "$(git diff --cached)"
'
test_expect_success 'reset --merge is ok again when switching back (2)' '
git reset --hard initial &&
git reset --merge second &&
! grep 4 file2 &&
grep 4 file1 &&
test "$(git rev-parse HEAD)" = "$(git rev-parse second)" &&
test -z "$(git diff --cached)"
'
# The next test will test the following:
#
# working index HEAD target working index HEAD
# ----------------------------------------------------
# file1: A B B C --merge (disallowed)
test_expect_success 'reset --merge fails with changes in file it touches' '
git reset --hard second &&
echo "line 5" >> file1 &&
test_tick &&
git commit -m "add line 5" file1 &&
sed -e "s/line 1/changed line 1/" <file1 >file3 &&
mv file3 file1 &&
test_must_fail git reset --merge HEAD^ 2>err.log &&
grep file1 err.log | grep "not uptodate"
'
test_expect_success 'setup 3 different branches' '
git reset --hard second &&
git branch branch1 &&
git branch branch2 &&
git branch branch3 &&
git checkout branch1 &&
echo "line 5 in branch1" >> file1 &&
test_tick &&
git commit -a -m "change in branch1" &&
git checkout branch2 &&
echo "line 5 in branch2" >> file1 &&
test_tick &&
git commit -a -m "change in branch2" &&
git tag third &&
git checkout branch3 &&
echo a new file >file3 &&
rm -f file1 &&
git add file3 &&
test_tick &&
git commit -a -m "change in branch3"
'
# The next test will test the following:
#
# working index HEAD target working index HEAD
# ----------------------------------------------------
# file1: X U B C --merge C C C
test_expect_success '"reset --merge HEAD^" is ok with pending merge' '
git checkout third &&
test_must_fail git merge branch1 &&
git reset --merge HEAD^ &&
test "$(git rev-parse HEAD)" = "$(git rev-parse second)" &&
test -z "$(git diff --cached)" &&
test -z "$(git diff)"
'
# The next test will test the following:
#
# working index HEAD target working index HEAD
# ----------------------------------------------------
# file1: X U B B --merge B B B
test_expect_success '"reset --merge HEAD" is ok with pending merge' '
git reset --hard third &&
test_must_fail git merge branch1 &&
git reset --merge HEAD &&
test "$(git rev-parse HEAD)" = "$(git rev-parse third)" &&
test -z "$(git diff --cached)" &&
test -z "$(git diff)"
'
test_expect_success '--merge with added/deleted' '
git reset --hard third &&
rm -f file2 &&
test_must_fail git merge branch3 &&
! test -f file2 &&
test -f file3 &&
git diff --exit-code file3 &&
git diff --exit-code branch3 file3 &&
git reset --merge HEAD &&
! test -f file3 &&
! test -f file2 &&
git diff --exit-code --cached
'
test_done

113
t/t7111-reset-table.sh Executable file
View File

@ -0,0 +1,113 @@
#!/bin/sh
#
# Copyright (c) 2010 Christian Couder
#
test_description='Tests to check that "reset" options follow a known table'
. ./test-lib.sh
test_expect_success 'creating initial commits' '
test_commit E file1 &&
test_commit D file1 &&
test_commit C file1
'
while read W1 I1 H1 T opt W2 I2 H2
do
test_expect_success "check: $W1 $I1 $H1 $T --$opt $W2 $I2 $H2" '
git reset --hard C &&
if test "$I1" != "$H1"
then
echo "$I1" >file1 &&
git add file1
fi &&
if test "$W1" != "$I1"
then
echo "$W1" >file1
fi &&
if test "$W2" != "XXXXX"
then
git reset --$opt $T &&
test "$(cat file1)" = "$W2" &&
git checkout-index -f -- file1 &&
test "$(cat file1)" = "$I2" &&
git checkout -f HEAD -- file1 &&
test "$(cat file1)" = "$H2"
else
test_must_fail git reset --$opt $T
fi
'
done <<\EOF
A B C D soft A B D
A B C D mixed A D D
A B C D hard D D D
A B C D merge XXXXX
A B C C soft A B C
A B C C mixed A C C
A B C C hard C C C
A B C C merge XXXXX
B B C D soft B B D
B B C D mixed B D D
B B C D hard D D D
B B C D merge D D D
B B C C soft B B C
B B C C mixed B C C
B B C C hard C C C
B B C C merge C C C
B C C D soft B C D
B C C D mixed B D D
B C C D hard D D D
B C C D merge XXXXX
B C C C soft B C C
B C C C mixed B C C
B C C C hard C C C
B C C C merge B C C
EOF
test_expect_success 'setting up branches to test with unmerged entries' '
git reset --hard C &&
git branch branch1 &&
git branch branch2 &&
git checkout branch1 &&
test_commit B1 file1 &&
git checkout branch2 &&
test_commit B2 file1
'
while read W1 I1 H1 T opt W2 I2 H2
do
test_expect_success "check: $W1 $I1 $H1 $T --$opt $W2 $I2 $H2" '
git reset --hard B2 &&
test_must_fail git merge branch1 &&
cat file1 >X_file1 &&
if test "$W2" != "XXXXX"
then
git reset --$opt $T &&
if test "$W2" = "X"
then
test_cmp file1 X_file1
else
test "$(cat file1)" = "$W2"
fi &&
git checkout-index -f -- file1 &&
test "$(cat file1)" = "$I2" &&
git checkout -f HEAD -- file1 &&
test "$(cat file1)" = "$H2"
else
test_must_fail git reset --$opt $T
fi
'
done <<\EOF
X U C D soft XXXXX
X U C D mixed X D D
X U C D hard D D D
X U C D merge D D D
X U C C soft XXXXX
X U C C mixed X C C
X U C C hard C C C
X U C C merge C C C
EOF
test_done

View File

@ -550,6 +550,8 @@ static int same(struct cache_entry *a, struct cache_entry *b)
return 0;
if (!a && !b)
return 1;
if ((a->ce_flags | b->ce_flags) & CE_CONFLICTED)
return 0;
return a->ce_mode == b->ce_mode &&
!hashcmp(a->sha1, b->sha1);
}
@ -809,7 +811,11 @@ static int merged_entry(struct cache_entry *merge, struct cache_entry *old,
{
int update = CE_UPDATE;
if (old) {
if (!old) {
if (verify_absent(merge, "overwritten", o))
return -1;
invalidate_ce_path(merge, o);
} else if (!(old->ce_flags & CE_CONFLICTED)) {
/*
* See if we can re-use the old CE directly?
* That way we get the uptodate stat info.
@ -827,11 +833,12 @@ static int merged_entry(struct cache_entry *merge, struct cache_entry *old,
update |= CE_SKIP_WORKTREE;
invalidate_ce_path(old, o);
}
}
else {
if (verify_absent(merge, "overwritten", o))
return -1;
invalidate_ce_path(merge, o);
} else {
/*
* Previously unmerged entry left as an existence
* marker by read_index_unmerged();
*/
invalidate_ce_path(old, o);
}
add_entry(o, merge, update, CE_STAGEMASK);
@ -847,7 +854,7 @@ static int deleted_entry(struct cache_entry *ce, struct cache_entry *old,
return -1;
return 0;
}
if (verify_uptodate(old, o))
if (!(old->ce_flags & CE_CONFLICTED) && verify_uptodate(old, o))
return -1;
add_entry(o, ce, CE_REMOVE, 0);
invalidate_ce_path(ce, o);