From 9e8eceab73e9f6a1e9349b1c14d4db0c035ac1ba Mon Sep 17 00:00:00 2001 From: Linus Torvalds Date: Mon, 1 Dec 2008 09:30:31 -0800 Subject: Add 'merge' mode to 'git reset' We have always had a nice way to reset a working tree to another state while carrying our changes around: "git read-tree -u -m". Yes, it fails if the target tree is different in the paths that are dirty in the working tree, but this is how we used to switch branches in "git checkout", and it worked fine. However, perhaps exactly _because_ we've supported this from very early on, another low-level command, namely "git reset", never did. But as time went on, 'git reset' remains as a very common command, while 'git read-tree' is now a very odd and low-level plumbing thing that nobody sane should ever use, because it only makes sense together with other operations like either switching branches or just rewriting HEAD. Which means that we have effectively lost the ability to do something very common: jump to another point in time without always dropping all our dirty state. So add this kind of mode to "git reset", and since it merges your changes to what you are resetting to, just call it that: "git reset --merge". I've wanted this for a long time, since I very commonly carry a dirty tree while working on things. My main 'Makefile' file quite often has the next version already modified, and sometimes I have local modifications that I don't want to commit, but I still do pulls and patch applications, and occasionally want to do "git reset" to undo them - while still keeping my local modifications. (Maybe we could eventually change it to something like "if we have a working tree, default to --merge, otherwise default to --mixed"). NOTE! This new mode is certainly not perfect. There's a few things to look out for: - if the index has unmerged entries, "--merge" will currently simply refuse to reset ("you need to resolve your current index first"). You'll need to use "--hard" or similar in this case. This is sad, because normally a unmerged index means that the working tree file should have matched the source tree, so the correct action is likely to make --merge reset such a path to the target (like --hard), regardless of dirty state in-tree or in-index. But that's not how read-tree has ever worked, so.. - "git checkout -m" actually knows how to do a three-way merge, rather than refuse to update the working tree. So we do know how to do that, and arguably that would be even nicer behavior. At the same time it's also arguably true that there is a chance of loss of state (ie you cannot get back to the original tree if the three-way merge ends up resolving cleanly to no diff at all), so the "refuse to do it" is in some respects the safer - but less user-friendly - option. In other words, I think 'git reset --merge' could become a bit more friendly, but this is already a big improvement. It allows you to undo a recent commit without having to throw your current work away. Yes, yes, with a dirty tree you could always do git stash git reset --hard git stash apply instead, but isn't "git reset --merge" a nice way to handle one particular simple case? Signed-off-by: Linus Torvalds -- Hmm? Maybe I'm the only one that does a lot of work with a dirty tree, and sure, I can do other things like the "git stash" thing, or using "git checkout" to actually create a new branch, and then playing games with branch renaming etc to make it work like this one. But I suspect others dislike how "git reset" works too. But see the suggested improvements above. builtin-reset.c | 26 ++++++++++++++++++-------- 1 files changed, 18 insertions(+), 8 deletions(-) --- builtin-reset.c | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/builtin-reset.c b/builtin-reset.c index 9514b77f8c..c0cb915c26 100644 --- a/builtin-reset.c +++ b/builtin-reset.c @@ -20,11 +20,14 @@ #include "parse-options.h" static const char * const git_reset_usage[] = { - "git reset [--mixed | --soft | --hard] [-q] []", + "git reset [--mixed | --soft | --hard | --merge] [-q] []", "git reset [--mixed] [--] ...", NULL }; +enum reset_type { MIXED, SOFT, HARD, MERGE, NONE }; +static const char *reset_type_names[] = { "mixed", "soft", "hard", "merge", NULL }; + static char *args_to_str(const char **argv) { char *buf = NULL; @@ -49,7 +52,7 @@ static inline int is_merge(void) return !access(git_path("MERGE_HEAD"), F_OK); } -static int reset_index_file(const unsigned char *sha1, int is_hard_reset, int quiet) +static int reset_index_file(const unsigned char *sha1, int reset_type, int quiet) { int i = 0; const char *args[6]; @@ -57,9 +60,17 @@ static int reset_index_file(const unsigned char *sha1, int is_hard_reset, int qu args[i++] = "read-tree"; if (!quiet) args[i++] = "-v"; - args[i++] = "--reset"; - if (is_hard_reset) + switch (reset_type) { + case MERGE: args[i++] = "-u"; + args[i++] = "-m"; + break; + case HARD: + args[i++] = "-u"; + /* fallthrough */ + default: + args[i++] = "--reset"; + } args[i++] = sha1_to_hex(sha1); args[i] = NULL; @@ -169,9 +180,6 @@ static void prepend_reflog_action(const char *action, char *buf, size_t size) warning("Reflog action message too long: %.*s...", 50, buf); } -enum reset_type { MIXED, SOFT, HARD, NONE }; -static const char *reset_type_names[] = { "mixed", "soft", "hard", NULL }; - int cmd_reset(int argc, const char **argv, const char *prefix) { int i = 0, reset_type = NONE, update_ref_status = 0, quiet = 0; @@ -186,6 +194,8 @@ int cmd_reset(int argc, const char **argv, const char *prefix) OPT_SET_INT(0, "soft", &reset_type, "reset only HEAD", SOFT), OPT_SET_INT(0, "hard", &reset_type, "reset HEAD, index and working tree", HARD), + OPT_SET_INT(0, "merge", &reset_type, + "reset HEAD, index and working tree", MERGE), OPT_BOOLEAN('q', NULL, &quiet, "disable showing new HEAD in hard reset and progress message"), OPT_END() @@ -266,7 +276,7 @@ int cmd_reset(int argc, const char **argv, const char *prefix) if (is_merge() || read_cache() < 0 || unmerged_cache()) die("Cannot do a soft reset in the middle of a merge."); } - else if (reset_index_file(sha1, (reset_type == HARD), quiet)) + 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, -- cgit v1.2.3 From 1b5b465fbde9515e04bd1e8524a208c92af3555e Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Wed, 3 Dec 2008 18:00:12 -0800 Subject: Document "git-reset --merge" The commit log message for the feature made it sound as if this is a saner version of --mixed, but the use case presented makes it clear that it is a better variant of --hard when your changes and somebody else's changes are mixed together. Perhaps we would want to rewrite the example that shows the use of --hard not to talk about recovering from a failed merge? Signed-off-by: Junio C Hamano --- Documentation/git-reset.txt | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/Documentation/git-reset.txt b/Documentation/git-reset.txt index 52aab5e680..c542b0caf7 100644 --- a/Documentation/git-reset.txt +++ b/Documentation/git-reset.txt @@ -8,7 +8,7 @@ git-reset - Reset current HEAD to the specified state SYNOPSIS -------- [verse] -'git reset' [--mixed | --soft | --hard] [-q] [] +'git reset' [--mixed | --soft | --hard | --merge] [-q] [] 'git reset' [-q] [] [--] ... DESCRIPTION @@ -45,6 +45,11 @@ OPTIONS switched to. Any changes to tracked files in the working tree since are lost. +--merge:: + Resets the index to match the tree recorded by the named commit, + and updates the files that are different between the named commit + and the current commit in the working tree. + -q:: Be quiet, only report errors. @@ -152,6 +157,28 @@ tip of the current branch in ORIG_HEAD, so resetting hard to it brings your index file and the working tree back to that state, and resets the tip of the branch to that commit. +Undo a merge or pull inside a dirty work tree:: ++ +------------ +$ git pull <1> +Auto-merging nitfol +Merge made by recursive. + nitfol | 20 +++++---- + ... +$ git reset --merge ORIG_HEAD <2> +------------ ++ +<1> Even if you may have local modifications in your +working tree, you can safely say "git pull" when you know +that the change in the other branch does not overlap with +them. +<2> After inspecting the result of the merge, you may find +that the change in the other branch is unsatisfactory. Running +"git reset --hard ORIG_HEAD" will let you go back to where you +were, but it will discard your local changes, which you do not +want. "git reset --merge" keeps your local changes. + + Interrupted workflow:: + Suppose you are interrupted by an urgent fix request while you -- cgit v1.2.3