Browse Source

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
maint
Junio C Hamano 17 years ago
parent
commit
29a1f99b4b
  1. 26
      Documentation/git-checkout.txt
  2. 105
      builtin-checkout.c
  3. 70
      t/t7201-co.sh

26
Documentation/git-checkout.txt

@ -9,7 +9,7 @@ SYNOPSIS
-------- --------
[verse] [verse]
'git checkout' [-q] [-f] [[--track | --no-track] -b <new_branch> [-l]] [-m] [<branch>] '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 DESCRIPTION
----------- -----------
@ -23,14 +23,19 @@ options, which will be passed to `git branch`.


When <paths> are given, this command does *not* switch When <paths> are given, this command does *not* switch
branches. It updates the named paths in the working tree from branches. It updates the named paths in the working tree from
the index file (i.e. it runs `git checkout-index -f -u`), or the index file, or from a named commit. In
from a named commit. In this case, the `-b` options is meaningless and giving
this case, the `-f` and `-b` options are meaningless and giving
either of them results in an error. <tree-ish> argument can be 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) used to specify a specific tree-ish (i.e. commit, tag or tree)
to update the index for the given paths before updating the to update the index for the given paths before updating the
working tree. 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 OPTIONS
------- -------
@ -38,8 +43,17 @@ OPTIONS
Quiet, suppress feedback messages. Quiet, suppress feedback messages.


-f:: -f::
Proceed even if the index or the working tree differs When switching branches, proceed even if the index or the
from HEAD. This is used to throw away local changes. 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:: -b::
Create a new branch named <new_branch> and start it at Create a new branch named <new_branch> and start it at

105
builtin-checkout.c

@ -20,6 +20,18 @@ static const char * const checkout_usage[] = {
NULL, 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, static int post_checkout_hook(struct commit *old, struct commit *new,
int changed) int changed)
{ {
@ -76,7 +88,43 @@ static int read_tree_some(struct tree *tree, const char **pathspec)
return 0; 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; int pos;
struct checkout state; struct checkout state;
@ -85,7 +133,7 @@ static int checkout_paths(struct tree *source_tree, const char **pathspec)
int flag; int flag;
struct commit *head; struct commit *head;
int errs = 0; int errs = 0;

int stage = opts->writeout_stage;
int newfd; int newfd;
struct lock_file *lock_file = xcalloc(1, sizeof(struct lock_file)); 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)) if (report_path_error(ps_matched, pathspec, 0))
return 1; 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 */ /* Now we are committed to check them out */
memset(&state, 0, sizeof(state)); memset(&state, 0, sizeof(state));
state.force = 1; 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++) { for (pos = 0; pos < active_nr; pos++) {
struct cache_entry *ce = active_cache[pos]; struct cache_entry *ce = active_cache[pos];
if (pathspec_match(pathspec, NULL, ce->name, 0)) { 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); 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) static int reset_tree(struct tree *tree, struct checkout_opts *o, int worktree)
{ {
struct unpack_trees_options opts; 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_BOOLEAN('l', NULL, &opts.new_branch_log, "log for new branch"),
OPT_SET_INT('t', "track", &opts.track, "track", OPT_SET_INT('t', "track", &opts.track, "track",
BRANCH_TRACK_EXPLICIT), 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('f', NULL, &opts.force, "force"),
OPT_BOOLEAN('m', NULL, &opts.merge, "merge"), OPT_BOOLEAN('m', NULL, &opts.merge, "merge"),
OPT_END(), OPT_END(),
@ -527,20 +594,22 @@ no_reference:
die("invalid path specification"); die("invalid path specification");


/* Checkout paths */ /* Checkout paths */
if (opts.new_branch || opts.force || opts.merge) { if (opts.new_branch || opts.merge) {
if (argc == 1) { 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 { } 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) { if (new.name && !new.commit) {
die("Cannot switch branch to a non-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); return switch_branches(&opts, &new);
} }

70
t/t7201-co.sh

@ -337,4 +337,74 @@ test_expect_success \
test refs/heads/delete-me = "$(git symbolic-ref HEAD)" && test refs/heads/delete-me = "$(git symbolic-ref HEAD)" &&
test_must_fail git checkout --track -b track' 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 test_done

Loading…
Cancel
Save