Merge branch 'tc/checkout-B'

* tc/checkout-B:
  builtin/checkout: handle -B from detached HEAD correctly
  builtin/checkout: learn -B
  builtin/checkout: reword hint for -b
  add tests for checkout -b
This commit is contained in:
Junio C Hamano 2010-08-18 11:42:47 -07:00
commit 07cd900112
4 changed files with 218 additions and 9 deletions

View File

@ -9,7 +9,7 @@ SYNOPSIS
-------- --------
[verse] [verse]
'git checkout' [-q] [-f] [-m] [<branch>] 'git checkout' [-q] [-f] [-m] [<branch>]
'git checkout' [-q] [-f] [-m] [[-b|--orphan] <new_branch>] [<start_point>] 'git checkout' [-q] [-f] [-m] [[-b|-B|--orphan] <new_branch>] [<start_point>]
'git checkout' [-f|--ours|--theirs|-m|--conflict=<style>] [<tree-ish>] [--] <paths>... 'git checkout' [-f|--ours|--theirs|-m|--conflict=<style>] [<tree-ish>] [--] <paths>...
'git checkout' --patch [<tree-ish>] [--] [<paths>...] 'git checkout' --patch [<tree-ish>] [--] [<paths>...]
@ -21,7 +21,7 @@ also update `HEAD` to set the specified branch as the current
branch. branch.
'git checkout' [<branch>]:: 'git checkout' [<branch>]::
'git checkout' -b <new branch> [<start point>]:: 'git checkout' -b|-B <new_branch> [<start point>]::
This form switches branches by updating the index, working This form switches branches by updating the index, working
tree, and HEAD to reflect the specified branch. tree, and HEAD to reflect the specified branch.
@ -31,6 +31,17 @@ were called and then checked out; in this case you can
use the `--track` or `--no-track` options, which will be passed to use the `--track` or `--no-track` options, which will be passed to
'git branch'. As a convenience, `--track` without `-b` implies branch 'git branch'. As a convenience, `--track` without `-b` implies branch
creation; see the description of `--track` below. creation; see the description of `--track` below.
+
If `-B` is given, <new_branch> is created if it doesn't exist; otherwise, it
is reset. This is the transactional equivalent of
+
------------
$ git branch -f <branch> [<start point>]
$ git checkout <branch>
------------
+
that is to say, the branch is not reset/created unless "git checkout" is
successful.
'git checkout' [--patch] [<tree-ish>] [--] <pathspec>...:: 'git checkout' [--patch] [<tree-ish>] [--] <pathspec>...::
@ -75,6 +86,12 @@ entries; instead, unmerged entries are ignored.
Create a new branch named <new_branch> and start it at Create a new branch named <new_branch> and start it at
<start_point>; see linkgit:git-branch[1] for details. <start_point>; see linkgit:git-branch[1] for details.
-B::
Creates the branch <new_branch> and start it at <start_point>;
if it already exists, then reset it to <start_point>. This is
equivalent to running "git branch" with "-f"; see
linkgit:git-branch[1] for details.
-t:: -t::
--track:: --track::
When creating a new branch, set up "upstream" configuration. See When creating a new branch, set up "upstream" configuration. See

View File

@ -159,7 +159,7 @@ void create_branch(const char *head,
dont_change_ref = 1; dont_change_ref = 1;
else if (!force) else if (!force)
die("A branch named '%s' already exists.", name); die("A branch named '%s' already exists.", name);
else if (!is_bare_repository() && !strcmp(head, name)) else if (!is_bare_repository() && head && !strcmp(head, name))
die("Cannot force update the current branch."); die("Cannot force update the current branch.");
forcing = 1; forcing = 1;
} }

View File

@ -32,7 +32,11 @@ struct checkout_opts {
int writeout_stage; int writeout_stage;
int writeout_error; int writeout_error;
/* not set by parse_options */
int branch_exists;
const char *new_branch; const char *new_branch;
const char *new_branch_force;
const char *new_orphan_branch; const char *new_orphan_branch;
int new_branch_log; int new_branch_log;
enum branch_track track; enum branch_track track;
@ -511,7 +515,8 @@ static void update_refs_for_switch(struct checkout_opts *opts,
} }
} }
else else
create_branch(old->name, opts->new_branch, new->name, 0, create_branch(old->name, opts->new_branch, new->name,
opts->new_branch_force ? 1 : 0,
opts->new_branch_log, opts->track); opts->new_branch_log, opts->track);
new->name = opts->new_branch; new->name = opts->new_branch;
setup_branch_path(new); setup_branch_path(new);
@ -531,7 +536,7 @@ static void update_refs_for_switch(struct checkout_opts *opts,
new->name); new->name);
else else
fprintf(stderr, "Switched to%s branch '%s'\n", fprintf(stderr, "Switched to%s branch '%s'\n",
opts->new_branch ? " a new" : "", opts->branch_exists ? " and reset" : " a new",
new->name); new->name);
} }
if (old->path && old->name) { if (old->path && old->name) {
@ -657,7 +662,10 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
int dwim_new_local_branch = 1; int dwim_new_local_branch = 1;
struct option options[] = { struct option options[] = {
OPT__QUIET(&opts.quiet), OPT__QUIET(&opts.quiet),
OPT_STRING('b', NULL, &opts.new_branch, "new branch", "branch"), OPT_STRING('b', NULL, &opts.new_branch, "branch",
"create and checkout a new branch"),
OPT_STRING('B', NULL, &opts.new_branch_force, "branch",
"create/reset and checkout a branch"),
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),
@ -688,6 +696,14 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
argc = parse_options(argc, argv, prefix, options, checkout_usage, argc = parse_options(argc, argv, prefix, options, checkout_usage,
PARSE_OPT_KEEP_DASHDASH); PARSE_OPT_KEEP_DASHDASH);
/* we can assume from now on new_branch = !new_branch_force */
if (opts.new_branch && opts.new_branch_force)
die("-B cannot be used with -b");
/* copy -B over to -b, so that we can just check the latter */
if (opts.new_branch_force)
opts.new_branch = opts.new_branch_force;
if (patch_mode && (opts.track > 0 || opts.new_branch if (patch_mode && (opts.track > 0 || opts.new_branch
|| opts.new_branch_log || opts.merge || opts.force)) || opts.new_branch_log || opts.merge || opts.force))
die ("--patch is incompatible with all other options"); die ("--patch is incompatible with all other options");
@ -709,7 +725,7 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
if (opts.new_orphan_branch) { if (opts.new_orphan_branch) {
if (opts.new_branch) if (opts.new_branch)
die("--orphan and -b are mutually exclusive"); die("--orphan and -b|-B are mutually exclusive");
if (opts.track > 0) if (opts.track > 0)
die("--orphan cannot be used with -t"); die("--orphan cannot be used with -t");
opts.new_branch = opts.new_orphan_branch; opts.new_branch = opts.new_orphan_branch;
@ -858,8 +874,12 @@ no_reference:
if (strbuf_check_branch_ref(&buf, opts.new_branch)) if (strbuf_check_branch_ref(&buf, opts.new_branch))
die("git checkout: we do not like '%s' as a branch name.", die("git checkout: we do not like '%s' as a branch name.",
opts.new_branch); opts.new_branch);
if (!get_sha1(buf.buf, rev)) if (!get_sha1(buf.buf, rev)) {
die("git checkout: branch %s already exists", opts.new_branch); opts.branch_exists = 1;
if (!opts.new_branch_force)
die("git checkout: branch %s already exists",
opts.new_branch);
}
strbuf_release(&buf); strbuf_release(&buf);
} }

172
t/t2018-checkout-branch.sh Executable file
View File

@ -0,0 +1,172 @@
#!/bin/sh
test_description='checkout '
. ./test-lib.sh
# Arguments: <branch> <sha> [<checkout options>]
#
# Runs "git checkout" to switch to <branch>, testing that
#
# 1) we are on the specified branch, <branch>;
# 2) HEAD is <sha>; if <sha> is not specified, the old HEAD is used.
#
# If <checkout options> is not specified, "git checkout" is run with -b.
do_checkout() {
exp_branch=$1 &&
exp_ref="refs/heads/$exp_branch" &&
# if <sha> is not specified, use HEAD.
exp_sha=${2:-$(git rev-parse --verify HEAD)} &&
# default options for git checkout: -b
if [ -z "$3" ]; then
opts="-b"
else
opts="$3"
fi
git checkout $opts $exp_branch $exp_sha &&
test $exp_ref = $(git rev-parse --symbolic-full-name HEAD) &&
test $exp_sha = $(git rev-parse --verify HEAD)
}
test_dirty_unmergeable() {
! git diff --exit-code >/dev/null
}
setup_dirty_unmergeable() {
echo >>file1 change2
}
test_dirty_mergeable() {
! git diff --cached --exit-code >/dev/null
}
setup_dirty_mergeable() {
echo >file2 file2 &&
git add file2
}
test_expect_success 'setup' '
test_commit initial file1 &&
HEAD1=$(git rev-parse --verify HEAD) &&
test_commit change1 file1 &&
HEAD2=$(git rev-parse --verify HEAD) &&
git branch -m branch1
'
test_expect_success 'checkout -b to a new branch, set to HEAD' '
do_checkout branch2
'
test_expect_success 'checkout -b to a new branch, set to an explicit ref' '
git checkout branch1 &&
git branch -D branch2 &&
do_checkout branch2 $HEAD1
'
test_expect_success 'checkout -b to a new branch with unmergeable changes fails' '
git checkout branch1 &&
# clean up from previous test
git branch -D branch2 &&
setup_dirty_unmergeable &&
test_must_fail do_checkout branch2 $HEAD1 &&
test_dirty_unmergeable
'
test_expect_success 'checkout -f -b to a new branch with unmergeable changes discards changes' '
# still dirty and on branch1
do_checkout branch2 $HEAD1 "-f -b" &&
test_must_fail test_dirty_unmergeable
'
test_expect_success 'checkout -b to a new branch preserves mergeable changes' '
git checkout branch1 &&
# clean up from previous test
git branch -D branch2 &&
setup_dirty_mergeable &&
do_checkout branch2 $HEAD1 &&
test_dirty_mergeable
'
test_expect_success 'checkout -f -b to a new branch with mergeable changes discards changes' '
# clean up from previous test
git reset --hard &&
git checkout branch1 &&
# clean up from previous test
git branch -D branch2 &&
setup_dirty_mergeable &&
do_checkout branch2 $HEAD1 "-f -b" &&
test_must_fail test_dirty_mergeable
'
test_expect_success 'checkout -b to an existing branch fails' '
git reset --hard HEAD &&
test_must_fail do_checkout branch2 $HEAD2
'
test_expect_success 'checkout -B to an existing branch resets branch to HEAD' '
git checkout branch1 &&
do_checkout branch2 "" -B
'
test_expect_success 'checkout -B to an existing branch from detached HEAD resets branch to HEAD' '
git checkout $(git rev-parse --verify HEAD) &&
do_checkout branch2 "" -B
'
test_expect_success 'checkout -B to an existing branch with an explicit ref resets branch to that ref' '
git checkout branch1 &&
do_checkout branch2 $HEAD1 -B
'
test_expect_success 'checkout -B to an existing branch with unmergeable changes fails' '
git checkout branch1 &&
setup_dirty_unmergeable &&
test_must_fail do_checkout branch2 $HEAD1 -B &&
test_dirty_unmergeable
'
test_expect_success 'checkout -f -B to an existing branch with unmergeable changes discards changes' '
# still dirty and on branch1
do_checkout branch2 $HEAD1 "-f -B" &&
test_must_fail test_dirty_unmergeable
'
test_expect_success 'checkout -B to an existing branch preserves mergeable changes' '
git checkout branch1 &&
setup_dirty_mergeable &&
do_checkout branch2 $HEAD1 -B &&
test_dirty_mergeable
'
test_expect_success 'checkout -f -B to an existing branch with mergeable changes discards changes' '
# clean up from previous test
git reset --hard &&
git checkout branch1 &&
setup_dirty_mergeable &&
do_checkout branch2 $HEAD1 "-f -B" &&
test_must_fail test_dirty_mergeable
'
test_done