Merge branch 'jc/maint-checkout-fix' into 'jc/better-conflict-resolution'

* jc/maint-checkout-fix:
  checkout --ours/--theirs: allow checking out one side of a conflicting merge
  checkout -f: allow ignoring unmerged paths when checking out of the index
  checkout: do not check out unmerged higher stages randomly
This commit is contained in:
Junio C Hamano 2008-08-30 19:44:17 -07:00
commit 29a1f99b4b
3 changed files with 177 additions and 24 deletions

View File

@ -9,7 +9,7 @@ SYNOPSIS
--------
[verse]
'git checkout' [-q] [-f] [[--track | --no-track] -b <new_branch> [-l]] [-m] [<branch>]
'git checkout' [<tree-ish>] [--] <paths>...
'git checkout' [-f|--ours|--theirs] [<tree-ish>] [--] <paths>...
DESCRIPTION
-----------
@ -23,14 +23,19 @@ options, which will be passed to `git branch`.
When <paths> 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. <tree-ish> 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. The contents from a
specific side of the merge can be checked out of the index by
using --ours or --theirs.
OPTIONS
-------
@ -38,8 +43,17 @@ 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.
--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 <new_branch> and start it at

View File

@ -20,6 +20,18 @@ static const char * const checkout_usage[] = {
NULL,
};
struct checkout_opts {
int quiet;
int merge;
int force;
int writeout_stage;
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)
{
@ -76,7 +88,43 @@ static int read_tree_some(struct tree *tree, const char **pathspec)
return 0;
}
static int checkout_paths(struct tree *source_tree, const char **pathspec)
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 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)
{
int pos;
struct checkout state;
@ -85,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));
@ -107,6 +155,26 @@ 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;
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);
}
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 +182,13 @@ 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;
}
if (stage)
errs |= checkout_stage(stage, ce, pos, &state);
pos = skip_same_name(ce, pos) - 1;
}
}
@ -151,17 +225,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;
@ -426,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(),
@ -527,20 +594,22 @@ 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) {
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);
}

View File

@ -337,4 +337,74 @@ 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_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_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