author | Junio C Hamano <gitster@pobox.com> | |
Sat, 20 Mar 2010 18:29:35 +0000 (11:29 -0700) | ||
committer | Junio C Hamano <gitster@pobox.com> | |
Sat, 20 Mar 2010 18:29:35 +0000 (11:29 -0700) |
* 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"
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"
index 168db08627e009c8d760bae36a4344d433923632..645f0c17485d35e6696be950fc61f5fdcf3dd14e 100644 (file)
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>...]
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
--mixed A D D
--hard D D D
--merge (disallowed)
+ --keep (disallowed)
working index HEAD target working index HEAD
----------------------------------------------------
--mixed A C C
--hard C C C
--merge (disallowed)
+ --keep A C C
working index HEAD target working index HEAD
----------------------------------------------------
--mixed B D D
--hard D D D
--merge D D D
+ --keep (disallowed)
working index HEAD target working index HEAD
----------------------------------------------------
--mixed B C C
--hard C C C
--merge C C C
+ --keep B C C
working index HEAD target working index HEAD
----------------------------------------------------
--mixed B D D
--hard D D D
--merge (disallowed)
+ --keep (disallowed)
working index HEAD target working index 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
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:
--mixed X B B
--hard B B B
--merge B B B
+ --keep (disallowed)
working index HEAD target working index HEAD
----------------------------------------------------
--mixed X A A
--hard A A A
--merge A A A
+ --keep (disallowed)
X means any state and U means an unmerged index.
<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>
diff --git a/builtin/reset.c b/builtin/reset.c
index 0f5022eed24f980f6fedee49f8602fefa6fe85e4..2c3a69adc674bb3f539d29b2d29116e89f887066 100644 (file)
--- a/builtin/reset.c
+++ b/builtin/reset.c
#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)
{
if (!quiet)
opts.verbose_update = 1;
switch (reset_type) {
+ case KEEP:
case MERGE:
opts.update = 1;
break;
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))
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;
"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()
};
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())
/* 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. */
diff --git a/t/t7103-reset-bare.sh b/t/t7103-reset-bare.sh
index afb55b3a463f79be83c0e3cc4a8aff8a0c6676be..1eef93c2b292c8ec649f12c826587365e5a0d0e6 100755 (executable)
--- a/t/t7103-reset-bare.sh
+++ b/t/t7103-reset-bare.sh
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)
'
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"
'
diff --git a/t/t7110-reset-merge.sh b/t/t7110-reset-merge.sh
index 8704d0019655d591785c0cf0eadb7d846c6b4469..70cdd8e618c648f7ee6550997d68c40d912c8db9 100755 (executable)
--- a/t/t7110-reset-merge.sh
+++ b/t/t7110-reset-merge.sh
# 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
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
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
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
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 &&
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
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 &&
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
diff --git a/t/t7111-reset-table.sh b/t/t7111-reset-table.sh
index de896c948d17bb55967fd04449cbb45c248fd6ab..ce421ad5ac4b3a17d0e347a7d86a386e2b14547b 100755 (executable)
--- a/t/t7111-reset-table.sh
+++ b/t/t7111-reset-table.sh
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' '
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