From 8fdcf3125465f70c0cad5be5ab192d46e46307c7 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Fri, 29 Aug 2008 13:40:36 -0700 Subject: [PATCH 1/3] checkout: do not check out unmerged higher stages randomly During a conflicted merge when you have unmerged stages for a path F in the index, if you said: $ git checkout F we rewrote F as many times as we have stages for it, and the last one (typically "theirs") was left in the work tree, without resolving the conflict. This fixes it by noticing that a specified pathspec pattern matches an unmerged path, and by erroring out. Signed-off-by: Junio C Hamano --- builtin-checkout.c | 29 ++++++++++++++++++++++++++++- t/t7201-co.sh | 22 ++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/builtin-checkout.c b/builtin-checkout.c index 411cc513c6..8544010994 100644 --- a/builtin-checkout.c +++ b/builtin-checkout.c @@ -76,6 +76,15 @@ static int read_tree_some(struct tree *tree, const char **pathspec) return 0; } +static int skip_same_name(struct cache_entry *ce, int pos) +{ + while (++pos < active_nr && + !strcmp(active_cache[pos]->name, ce->name)) + ; /* skip */ + return pos; +} + + static int checkout_paths(struct tree *source_tree, const char **pathspec) { int pos; @@ -107,6 +116,20 @@ static int checkout_paths(struct tree *source_tree, const char **pathspec) if (report_path_error(ps_matched, pathspec, 0)) return 1; + /* Any unmerged paths? */ + for (pos = 0; pos < active_nr; pos++) { + struct cache_entry *ce = active_cache[pos]; + if (pathspec_match(pathspec, NULL, ce->name, 0)) { + if (!ce_stage(ce)) + continue; + errs = 1; + error("path '%s' is unmerged", ce->name); + pos = skip_same_name(ce, pos) - 1; + } + } + if (errs) + return 1; + /* Now we are committed to check them out */ memset(&state, 0, sizeof(state)); state.force = 1; @@ -114,7 +137,11 @@ static int checkout_paths(struct tree *source_tree, const char **pathspec) for (pos = 0; pos < active_nr; pos++) { struct cache_entry *ce = active_cache[pos]; if (pathspec_match(pathspec, NULL, ce->name, 0)) { - errs |= checkout_entry(ce, &state, NULL); + if (!ce_stage(ce)) { + errs |= checkout_entry(ce, &state, NULL); + continue; + } + pos = skip_same_name(ce, pos) - 1; } } diff --git a/t/t7201-co.sh b/t/t7201-co.sh index 9ad5d635a2..83a366f1e7 100755 --- a/t/t7201-co.sh +++ b/t/t7201-co.sh @@ -337,4 +337,26 @@ test_expect_success \ test refs/heads/delete-me = "$(git symbolic-ref HEAD)" && test_must_fail git checkout --track -b track' +test_expect_success 'checkout an unmerged path should fail' ' + rm -f .git/index && + O=$(echo original | git hash-object -w --stdin) && + A=$(echo ourside | git hash-object -w --stdin) && + B=$(echo theirside | git hash-object -w --stdin) && + ( + echo "100644 $A 0 fild" && + echo "100644 $O 1 file" && + echo "100644 $A 2 file" && + echo "100644 $B 3 file" && + echo "100644 $A 0 filf" + ) | git update-index --index-info && + echo "none of the above" >sample && + cat sample >fild && + cat sample >file && + cat sample >filf && + test_must_fail git checkout fild file filf && + test_cmp sample fild && + test_cmp sample filf && + test_cmp sample file +' + test_done From db9410990ee41f2b253763621c0023c782ec86e2 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Sat, 30 Aug 2008 07:46:55 -0700 Subject: [PATCH 2/3] checkout -f: allow ignoring unmerged paths when checking out of the index Earlier we made "git checkout $pathspec" to atomically refuse the operation of $pathspec matched any path with unmerged stages. This patch allows: $ git checkout -f a b c to ignore, instead of error out on, such unmerged paths. The fix to prevent checkout of an unmerged path from random stages is still there. Signed-off-by: Junio C Hamano --- Documentation/git-checkout.txt | 19 +++++++++++----- builtin-checkout.c | 41 +++++++++++++++++++--------------- t/t7201-co.sh | 23 +++++++++++++++++++ 3 files changed, 59 insertions(+), 24 deletions(-) diff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt index 5aa69c0e12..15fdb08ce0 100644 --- a/Documentation/git-checkout.txt +++ b/Documentation/git-checkout.txt @@ -9,7 +9,7 @@ SYNOPSIS -------- [verse] 'git checkout' [-q] [-f] [[--track | --no-track] -b [-l]] [-m] [] -'git checkout' [] [--] ... +'git checkout' [-f] [] [--] ... DESCRIPTION ----------- @@ -23,14 +23,17 @@ options, which will be passed to `git branch`. When are given, this command does *not* switch branches. It updates the named paths in the working tree from -the index file (i.e. it runs `git checkout-index -f -u`), or -from a named commit. In -this case, the `-f` and `-b` options are meaningless and giving +the index file, or from a named commit. In +this case, the `-b` options is meaningless and giving either of them results in an error. argument can be used to specify a specific tree-ish (i.e. commit, tag or tree) to update the index for the given paths before updating the working tree. +The index may contain unmerged entries after a failed merge. By +default, if you try to check out such an entry from the index, the +checkout operation will fail and nothing will be checked out. +Using -f will ignore these unmerged entries. OPTIONS ------- @@ -38,8 +41,12 @@ OPTIONS Quiet, suppress feedback messages. -f:: - Proceed even if the index or the working tree differs - from HEAD. This is used to throw away local changes. + When switching branches, proceed even if the index or the + working tree differs from HEAD. This is used to throw away + local changes. ++ +When checking out paths from the index, do not fail upon unmerged +entries; instead, unmerged entries are ignored. -b:: Create a new branch named and start it at diff --git a/builtin-checkout.c b/builtin-checkout.c index 8544010994..1303f3b5b3 100644 --- a/builtin-checkout.c +++ b/builtin-checkout.c @@ -20,6 +20,17 @@ static const char * const checkout_usage[] = { NULL, }; +struct checkout_opts { + int quiet; + int merge; + int force; + int writeout_error; + + const char *new_branch; + int new_branch_log; + enum branch_track track; +}; + static int post_checkout_hook(struct commit *old, struct commit *new, int changed) { @@ -85,7 +96,8 @@ static int skip_same_name(struct cache_entry *ce, int pos) } -static int checkout_paths(struct tree *source_tree, const char **pathspec) +static int checkout_paths(struct tree *source_tree, const char **pathspec, + struct checkout_opts *opts) { int pos; struct checkout state; @@ -122,8 +134,12 @@ static int checkout_paths(struct tree *source_tree, const char **pathspec) if (pathspec_match(pathspec, NULL, ce->name, 0)) { if (!ce_stage(ce)) continue; - errs = 1; - error("path '%s' is unmerged", ce->name); + if (opts->force) { + warning("path '%s' is unmerged", ce->name); + } else { + errs = 1; + error("path '%s' is unmerged", ce->name); + } pos = skip_same_name(ce, pos) - 1; } } @@ -178,17 +194,6 @@ static void describe_detached_head(char *msg, struct commit *commit) strbuf_release(&sb); } -struct checkout_opts { - int quiet; - int merge; - int force; - int writeout_error; - - char *new_branch; - int new_branch_log; - enum branch_track track; -}; - static int reset_tree(struct tree *tree, struct checkout_opts *o, int worktree) { struct unpack_trees_options opts; @@ -554,15 +559,15 @@ no_reference: die("invalid path specification"); /* Checkout paths */ - if (opts.new_branch || opts.force || opts.merge) { + if (opts.new_branch || opts.merge) { if (argc == 1) { - die("git checkout: updating paths is incompatible with switching branches/forcing\nDid you intend to checkout '%s' which can not be resolved as commit?", argv[0]); + die("git checkout: updating paths is incompatible with switching branches.\nDid you intend to checkout '%s' which can not be resolved as commit?", argv[0]); } else { - die("git checkout: updating paths is incompatible with switching branches/forcing"); + die("git checkout: updating paths is incompatible with switching branches."); } } - return checkout_paths(source_tree, pathspec); + return checkout_paths(source_tree, pathspec, &opts); } if (new.name && !new.commit) { diff --git a/t/t7201-co.sh b/t/t7201-co.sh index 83a366f1e7..88692f9877 100755 --- a/t/t7201-co.sh +++ b/t/t7201-co.sh @@ -359,4 +359,27 @@ test_expect_success 'checkout an unmerged path should fail' ' test_cmp sample file ' +test_expect_success 'checkout with an unmerged path can be ignored' ' + rm -f .git/index && + O=$(echo original | git hash-object -w --stdin) && + A=$(echo ourside | git hash-object -w --stdin) && + B=$(echo theirside | git hash-object -w --stdin) && + ( + echo "100644 $A 0 fild" && + echo "100644 $O 1 file" && + echo "100644 $A 2 file" && + echo "100644 $B 3 file" && + echo "100644 $A 0 filf" + ) | git update-index --index-info && + echo "none of the above" >sample && + echo ourside >expect && + cat sample >fild && + cat sample >file && + cat sample >filf && + git checkout -f fild file filf && + test_cmp expect fild && + test_cmp expect filf && + test_cmp sample file +' + test_done From 38901a48375952ab6c02f22bddfa19ac2bec2c36 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Sat, 30 Aug 2008 07:48:18 -0700 Subject: [PATCH 3/3] checkout --ours/--theirs: allow checking out one side of a conflicting merge This lets you to check out 'our' (or 'their') version of an unmerged path out of the index while resolving conflicts. Signed-off-by: Junio C Hamano --- Documentation/git-checkout.txt | 11 ++++++++-- builtin-checkout.c | 39 +++++++++++++++++++++++++++++++++- t/t7201-co.sh | 25 ++++++++++++++++++++++ 3 files changed, 72 insertions(+), 3 deletions(-) diff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt index 15fdb08ce0..a9ca2f5520 100644 --- a/Documentation/git-checkout.txt +++ b/Documentation/git-checkout.txt @@ -9,7 +9,7 @@ SYNOPSIS -------- [verse] 'git checkout' [-q] [-f] [[--track | --no-track] -b [-l]] [-m] [] -'git checkout' [-f] [] [--] ... +'git checkout' [-f|--ours|--theirs] [] [--] ... DESCRIPTION ----------- @@ -33,7 +33,9 @@ working tree. The index may contain unmerged entries after a failed merge. By default, if you try to check out such an entry from the index, the checkout operation will fail and nothing will be checked out. -Using -f will ignore these unmerged entries. +Using -f will ignore these unmerged entries. The contents from a +specific side of the merge can be checked out of the index by +using --ours or --theirs. OPTIONS ------- @@ -48,6 +50,11 @@ OPTIONS When checking out paths from the index, do not fail upon unmerged entries; instead, unmerged entries are ignored. +--ours:: +--theirs:: + When checking out paths from the index, check out stage #2 + ('ours') or #3 ('theirs') for unmerged paths. + -b:: Create a new branch named and start it at . The new branch name must pass all checks defined diff --git a/builtin-checkout.c b/builtin-checkout.c index 1303f3b5b3..16bfbb6605 100644 --- a/builtin-checkout.c +++ b/builtin-checkout.c @@ -24,6 +24,7 @@ struct checkout_opts { int quiet; int merge; int force; + int writeout_stage; int writeout_error; const char *new_branch; @@ -95,6 +96,32 @@ static int skip_same_name(struct cache_entry *ce, int pos) return pos; } +static int check_stage(int stage, struct cache_entry *ce, int pos) +{ + while (pos < active_nr && + !strcmp(active_cache[pos]->name, ce->name)) { + if (ce_stage(active_cache[pos]) == stage) + return 0; + pos++; + } + return error("path '%s' does not have %s version", + ce->name, + (stage == 2) ? "our" : "their"); +} + +static int checkout_stage(int stage, struct cache_entry *ce, int pos, + struct checkout *state) +{ + while (pos < active_nr && + !strcmp(active_cache[pos]->name, ce->name)) { + if (ce_stage(active_cache[pos]) == stage) + return checkout_entry(active_cache[pos], state, NULL); + pos++; + } + return error("path '%s' does not have %s version", + ce->name, + (stage == 2) ? "our" : "their"); +} static int checkout_paths(struct tree *source_tree, const char **pathspec, struct checkout_opts *opts) @@ -106,7 +133,7 @@ static int checkout_paths(struct tree *source_tree, const char **pathspec, int flag; struct commit *head; int errs = 0; - + int stage = opts->writeout_stage; int newfd; struct lock_file *lock_file = xcalloc(1, sizeof(struct lock_file)); @@ -136,6 +163,8 @@ static int checkout_paths(struct tree *source_tree, const char **pathspec, continue; if (opts->force) { warning("path '%s' is unmerged", ce->name); + } else if (stage) { + errs |= check_stage(stage, ce, pos); } else { errs = 1; error("path '%s' is unmerged", ce->name); @@ -157,6 +186,8 @@ static int checkout_paths(struct tree *source_tree, const char **pathspec, errs |= checkout_entry(ce, &state, NULL); continue; } + if (stage) + errs |= checkout_stage(stage, ce, pos, &state); pos = skip_same_name(ce, pos) - 1; } } @@ -458,6 +489,10 @@ int cmd_checkout(int argc, const char **argv, const char *prefix) OPT_BOOLEAN('l', NULL, &opts.new_branch_log, "log for new branch"), OPT_SET_INT('t', "track", &opts.track, "track", BRANCH_TRACK_EXPLICIT), + OPT_SET_INT('2', "ours", &opts.writeout_stage, "stage", + 2), + OPT_SET_INT('3', "theirs", &opts.writeout_stage, "stage", + 3), OPT_BOOLEAN('f', NULL, &opts.force, "force"), OPT_BOOLEAN('m', NULL, &opts.merge, "merge"), OPT_END(), @@ -573,6 +608,8 @@ no_reference: if (new.name && !new.commit) { die("Cannot switch branch to a non-commit."); } + if (opts.writeout_stage) + die("--ours/--theirs is incompatible with switching branches."); return switch_branches(&opts, &new); } diff --git a/t/t7201-co.sh b/t/t7201-co.sh index 88692f9877..c7ae14118a 100755 --- a/t/t7201-co.sh +++ b/t/t7201-co.sh @@ -382,4 +382,29 @@ test_expect_success 'checkout with an unmerged path can be ignored' ' test_cmp sample file ' +test_expect_success 'checkout unmerged stage' ' + rm -f .git/index && + O=$(echo original | git hash-object -w --stdin) && + A=$(echo ourside | git hash-object -w --stdin) && + B=$(echo theirside | git hash-object -w --stdin) && + ( + echo "100644 $A 0 fild" && + echo "100644 $O 1 file" && + echo "100644 $A 2 file" && + echo "100644 $B 3 file" && + echo "100644 $A 0 filf" + ) | git update-index --index-info && + echo "none of the above" >sample && + echo ourside >expect && + cat sample >fild && + cat sample >file && + cat sample >filf && + git checkout --ours . && + test_cmp expect fild && + test_cmp expect filf && + test_cmp expect file && + git checkout --theirs file && + test ztheirside = "z$(cat file)" +' + test_done