Merge branch 'cc/reset-keep'

* cc/reset-keep:
  Documentation: improve description of "git reset --keep"
  reset: disallow using --keep when there are unmerged entries
  reset: disallow "reset --keep" outside a work tree
  Documentation: reset: describe new "--keep" option
  reset: add test cases for "--keep" option
  reset: add option "--keep" to "git reset"
This commit is contained in:
Junio C Hamano 2010-03-20 11:29:35 -07:00
commit 49559cad6c
5 changed files with 229 additions and 20 deletions

View file

@ -8,7 +8,7 @@ git-reset - Reset current HEAD to the specified state
SYNOPSIS
--------
[verse]
'git reset' [--mixed | --soft | --hard | --merge] [-q] [<commit>]
'git reset' [--mixed | --soft | --hard | --merge | --keep] [-q] [<commit>]
'git reset' [-q] [<commit>] [--] <paths>...
'git reset' --patch [<commit>] [--] [<paths>...]
@ -52,6 +52,14 @@ OPTIONS
and updates the files that are different between the named commit
and the current commit in the working tree.
--keep::
Reset the index to the given commit, keeping local changes in
the working tree since the current commit, while updating
working tree files without local changes to what appears in
the given commit. If a file that is different between the
current commit and the given commit has local changes, reset
is aborted.
-p::
--patch::
Interactively select hunks in the difference between the index
@ -93,6 +101,7 @@ in the index and in state D in HEAD.
--mixed A D D
--hard D D D
--merge (disallowed)
--keep (disallowed)
working index HEAD target working index HEAD
----------------------------------------------------
@ -100,6 +109,7 @@ in the index and in state D in HEAD.
--mixed A C C
--hard C C C
--merge (disallowed)
--keep A C C
working index HEAD target working index HEAD
----------------------------------------------------
@ -107,6 +117,7 @@ in the index and in state D in HEAD.
--mixed B D D
--hard D D D
--merge D D D
--keep (disallowed)
working index HEAD target working index HEAD
----------------------------------------------------
@ -114,6 +125,7 @@ in the index and in state D in HEAD.
--mixed B C C
--hard C C C
--merge C C C
--keep B C C
working index HEAD target working index HEAD
----------------------------------------------------
@ -121,6 +133,7 @@ in the index and in state D in HEAD.
--mixed B D D
--hard D D D
--merge (disallowed)
--keep (disallowed)
working index HEAD target working index HEAD
----------------------------------------------------
@ -128,6 +141,7 @@ in the index and in state D in HEAD.
--mixed B C C
--hard C C C
--merge B C C
--keep 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
@ -138,6 +152,15 @@ 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.
"reset --keep" is meant to be used when removing some of the last
commits in the current branch while keeping changes in the working
tree. If there could be conflicts between the changes in the commit we
want to remove and the changes in the working tree we want to keep,
the reset is disallowed. That's why it is disallowed if there are both
changes between the working tree and HEAD, and between HEAD and the
target. To be safe, it is also disallowed when there are unmerged
entries.
The following tables show what happens when there are unmerged
entries:
@ -147,6 +170,7 @@ entries:
--mixed X B B
--hard B B B
--merge B B B
--keep (disallowed)
working index HEAD target working index HEAD
----------------------------------------------------
@ -154,6 +178,7 @@ entries:
--mixed X A A
--hard A A A
--merge A A A
--keep (disallowed)
X means any state and U means an unmerged index.
@ -325,6 +350,32 @@ $ git add frotz.c <3>
<2> This commits all other changes in the index.
<3> Adds the file to the index again.
Keep changes in working tree while discarding some previous commits::
+
Suppose you are working on something and you commit it, and then you
continue working a bit more, but now you think that what you have in
your working tree should be in another branch that has nothing to do
with what you commited previously. You can start a new branch and
reset it while keeping the changes in your work tree.
+
------------
$ git tag start
$ git checkout -b branch1
$ edit
$ git commit ... <1>
$ edit
$ git checkout -b branch2 <2>
$ git reset --keep start <3>
------------
+
<1> This commits your first edits in branch1.
<2> In the ideal world, you could have realized that the earlier
commit did not belong to the new topic when you created and switched
to branch2 (i.e. "git checkout -b branch2 start"), but nobody is
perfect.
<3> But you can use "reset --keep" to remove the unwanted commit after
you switched to "branch2".
Author
------
Written by Junio C Hamano <gitster@pobox.com> and Linus Torvalds <torvalds@osdl.org>

View file

@ -22,13 +22,15 @@
#include "cache-tree.h"
static const char * const git_reset_usage[] = {
"git reset [--mixed | --soft | --hard | --merge] [-q] [<commit>]",
"git reset [--mixed | --soft | --hard | --merge | --keep] [-q] [<commit>]",
"git reset [--mixed] <commit> [--] <paths>...",
NULL
};
enum reset_type { MIXED, SOFT, HARD, MERGE, NONE };
static const char *reset_type_names[] = { "mixed", "soft", "hard", "merge", NULL };
enum reset_type { MIXED, SOFT, HARD, MERGE, KEEP, NONE };
static const char *reset_type_names[] = {
"mixed", "soft", "hard", "merge", "keep", NULL
};
static char *args_to_str(const char **argv)
{
@ -71,6 +73,7 @@ static int reset_index_file(const unsigned char *sha1, int reset_type, int quiet
if (!quiet)
opts.verbose_update = 1;
switch (reset_type) {
case KEEP:
case MERGE:
opts.update = 1;
break;
@ -85,6 +88,16 @@ static int reset_index_file(const unsigned char *sha1, int reset_type, int quiet
read_cache_unmerged();
if (reset_type == KEEP) {
unsigned char head_sha1[20];
if (get_sha1("HEAD", head_sha1))
return error("You do not have a valid HEAD.");
if (!fill_tree_descriptor(desc, head_sha1))
return error("Failed to find tree of HEAD.");
nr++;
opts.fn = twoway_merge;
}
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))
@ -211,6 +224,14 @@ static void prepend_reflog_action(const char *action, char *buf, size_t size)
warning("Reflog action message too long: %.*s...", 50, buf);
}
static void die_if_unmerged_cache(int reset_type)
{
if (is_merge() || read_cache() < 0 || unmerged_cache())
die("Cannot do a %s reset in the middle of a merge.",
reset_type_names[reset_type]);
}
int cmd_reset(int argc, const char **argv, const char *prefix)
{
int i = 0, reset_type = NONE, update_ref_status = 0, quiet = 0;
@ -229,6 +250,8 @@ int cmd_reset(int argc, const char **argv, const char *prefix)
"reset HEAD, index and working tree", HARD),
OPT_SET_INT(0, "merge", &reset_type,
"reset HEAD, index and working tree", MERGE),
OPT_SET_INT(0, "keep", &reset_type,
"reset HEAD but keep local changes", KEEP),
OPT_BOOLEAN('p', "patch", &patch_mode, "select hunks interactively"),
OPT_END()
};
@ -304,7 +327,7 @@ int cmd_reset(int argc, const char **argv, const char *prefix)
if (reset_type == NONE)
reset_type = MIXED; /* by default */
if (reset_type == HARD || reset_type == MERGE)
if (reset_type != SOFT && reset_type != MIXED)
setup_work_tree();
if (reset_type == MIXED && is_bare_repository())
@ -314,12 +337,18 @@ int cmd_reset(int argc, const char **argv, const char *prefix)
/* 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. */
if (reset_type == SOFT) {
if (is_merge() || read_cache() < 0 || unmerged_cache())
die("Cannot do a soft reset in the middle of a merge.");
if (reset_type == SOFT)
die_if_unmerged_cache(reset_type);
else {
int err;
if (reset_type == KEEP)
die_if_unmerged_cache(reset_type);
err = reset_index_file(sha1, reset_type, quiet);
if (reset_type == KEEP)
err = err || reset_index_file(sha1, MIXED, quiet);
if (err)
die("Could not reset index file to revision '%s'.", rev);
}
else if (reset_index_file(sha1, reset_type, quiet))
die("Could not reset index file to revision '%s'.", rev);
/* Any resets update HEAD to the head being switched to,
* saving the previous head in ORIG_HEAD before. */

View file

@ -11,21 +11,26 @@ test_expect_success 'setup non-bare' '
git commit -a -m two
'
test_expect_success 'hard reset requires a worktree' '
test_expect_success '"hard" reset requires a worktree' '
(cd .git &&
test_must_fail git reset --hard)
'
test_expect_success 'merge reset requires a worktree' '
test_expect_success '"merge" reset requires a worktree' '
(cd .git &&
test_must_fail git reset --merge)
'
test_expect_success 'mixed reset is ok' '
test_expect_success '"keep" reset requires a worktree' '
(cd .git &&
test_must_fail git reset --keep)
'
test_expect_success '"mixed" reset is ok' '
(cd .git && git reset)
'
test_expect_success 'soft reset is ok' '
test_expect_success '"soft" reset is ok' '
(cd .git && git reset --soft)
'
@ -40,19 +45,23 @@ test_expect_success 'setup bare' '
cd bare.git
'
test_expect_success 'hard reset is not allowed in bare' '
test_expect_success '"hard" reset is not allowed in bare' '
test_must_fail git reset --hard HEAD^
'
test_expect_success 'merge reset is not allowed in bare' '
test_expect_success '"merge" reset is not allowed in bare' '
test_must_fail git reset --merge HEAD^
'
test_expect_success 'mixed reset is not allowed in bare' '
test_expect_success '"keep" reset is not allowed in bare' '
test_must_fail git reset --keep HEAD^
'
test_expect_success '"mixed" reset is not allowed in bare' '
test_must_fail git reset --mixed HEAD^
'
test_expect_success 'soft reset is allowed in bare' '
test_expect_success '"soft" reset is allowed in bare' '
git reset --soft HEAD^ &&
test "`git show --pretty=format:%s | head -n 1`" = "one"
'

View file

@ -3,7 +3,7 @@
# Copyright (c) 2009 Christian Couder
#
test_description='Tests for "git reset --merge"'
test_description='Tests for "git reset" with "--merge" and "--keep" options'
. ./test-lib.sh
@ -43,6 +43,30 @@ test_expect_success 'reset --merge is ok when switching back' '
test -z "$(git diff --cached)"
'
# The next test will test the following:
#
# working index HEAD target working index HEAD
# ----------------------------------------------------
# file1: C C C D --keep D D D
# file2: C D D D --keep C D D
test_expect_success 'reset --keep is ok with changes in file it does not touch' '
git reset --hard second &&
cat file1 >file2 &&
git reset --keep 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 --keep is ok when switching back' '
git reset --keep 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
@ -74,6 +98,18 @@ test_expect_success 'reset --merge is ok again when switching back (1)' '
test -z "$(git diff --cached)"
'
# The next test will test the following:
#
# working index HEAD target working index HEAD
# ----------------------------------------------------
# file1: B B C D --keep (disallowed)
test_expect_success 'reset --keep fails with changes in index in files it touches' '
git reset --hard second &&
echo "line 5" >> file1 &&
git add file1 &&
test_must_fail git reset --keep HEAD^
'
# The next test will test the following:
#
# working index HEAD target working index HEAD
@ -100,6 +136,30 @@ test_expect_success 'reset --merge is ok again when switching back (2)' '
test -z "$(git diff --cached)"
'
# The next test will test the following:
#
# working index HEAD target working index HEAD
# ----------------------------------------------------
# file1: C C C D --keep D D D
# file2: C C D D --keep C D D
test_expect_success 'reset --keep keeps changes it does not touch' '
git reset --hard second &&
echo "line 4" >> file2 &&
git add file2 &&
git reset --keep HEAD^ &&
grep 4 file2 &&
test "$(git rev-parse HEAD)" = "$(git rev-parse initial)" &&
test -z "$(git diff --cached)"
'
test_expect_success 'reset --keep keeps changes when switching back' '
git reset --keep 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
@ -116,6 +176,22 @@ test_expect_success 'reset --merge fails with changes in file it touches' '
grep file1 err.log | grep "not uptodate"
'
# The next test will test the following:
#
# working index HEAD target working index HEAD
# ----------------------------------------------------
# file1: A B B C --keep (disallowed)
test_expect_success 'reset --keep 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 --keep 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 &&
@ -152,6 +228,18 @@ test_expect_success '"reset --merge HEAD^" is ok with pending merge' '
test -z "$(git diff)"
'
# The next test will test the following:
#
# working index HEAD target working index HEAD
# ----------------------------------------------------
# file1: X U B C --keep (disallowed)
test_expect_success '"reset --keep HEAD^" fails with pending merge' '
git reset --hard third &&
test_must_fail git merge branch1 &&
test_must_fail git reset --keep HEAD^ 2>err.log &&
grep "middle of a merge" err.log
'
# The next test will test the following:
#
# working index HEAD target working index HEAD
@ -166,7 +254,19 @@ test_expect_success '"reset --merge HEAD" is ok with pending merge' '
test -z "$(git diff)"
'
test_expect_success '--merge with added/deleted' '
# The next test will test the following:
#
# working index HEAD target working index HEAD
# ----------------------------------------------------
# file1: X U B B --keep (disallowed)
test_expect_success '"reset --keep HEAD" fails with pending merge' '
git reset --hard third &&
test_must_fail git merge branch1 &&
test_must_fail git reset --keep HEAD 2>err.log &&
grep "middle of a merge" err.log
'
test_expect_success '--merge is ok with added/deleted merge' '
git reset --hard third &&
rm -f file2 &&
test_must_fail git merge branch3 &&
@ -180,4 +280,16 @@ test_expect_success '--merge with added/deleted' '
git diff --exit-code --cached
'
test_expect_success '--keep fails with added/deleted merge' '
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 &&
test_must_fail git reset --keep HEAD 2>err.log &&
grep "middle of a merge" err.log
'
test_done

View file

@ -44,26 +44,32 @@ 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 D keep 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
A B C C keep A C C
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 D keep XXXXX
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 B C C keep B 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 D keep 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
B C C C keep B C C
EOF
test_expect_success 'setting up branches to test with unmerged entries' '
@ -104,10 +110,12 @@ X U B C soft XXXXX
X U B C mixed X C C
X U B C hard C C C
X U B C merge C C C
X U B C keep XXXXX
X U B B soft XXXXX
X U B B mixed X B B
X U B B hard B B B
X U B B merge B B B
X U B B keep XXXXX
EOF
test_done