checkout: introduce --detach synonym for "git checkout foo^{commit}"

For example, one might use this when making a temporary merge to
test that two topics work well together.

Patch by Junio, with tests from Jeff King.

[jn: with some extra checks for bogus commandline usage]

Suggested-by: Jeff King <peff@peff.net>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
Signed-off-by: Jonathan Nieder <jrnieder@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
This commit is contained in:
Junio C Hamano 2011-02-08 04:32:49 -06:00
parent 09ebad6fae
commit 32669671c7
3 changed files with 126 additions and 7 deletions

View File

@ -9,6 +9,7 @@ SYNOPSIS
-------- --------
[verse] [verse]
'git checkout' [-q] [-f] [-m] [<branch>] 'git checkout' [-q] [-f] [-m] [<branch>]
'git checkout' [-q] [-f] [-m] [--detach] [<commit>]
'git checkout' [-q] [-f] [-m] [[-b|-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>...]
@ -22,9 +23,10 @@ branch.
'git checkout' [<branch>]:: 'git checkout' [<branch>]::
'git checkout' -b|-B <new_branch> [<start point>]:: 'git checkout' -b|-B <new_branch> [<start point>]::
'git checkout' [--detach] [<commit>]::
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 or commit.
+ +
If `-b` is given, a new branch is created as if linkgit:git-branch[1] If `-b` is given, a new branch is created as if linkgit:git-branch[1]
were called and then checked out; in this case you can were called and then checked out; in this case you can
@ -115,6 +117,13 @@ explicitly give a name with '-b' in such a case.
Create the new branch's reflog; see linkgit:git-branch[1] for Create the new branch's reflog; see linkgit:git-branch[1] for
details. details.
--detach::
Rather than checking out a branch to work on it, check out a
commit for inspection and discardable experiments.
This is the default behavior of "git checkout <commit>" when
<commit> is not a branch name. See the "DETACHED HEAD" section
below for details.
--orphan:: --orphan::
Create a new 'orphan' branch, named <new_branch>, started from Create a new 'orphan' branch, named <new_branch>, started from
<start_point> and switch to it. The first commit made on this <start_point> and switch to it. The first commit made on this
@ -204,7 +213,7 @@ leave out at most one of `A` and `B`, in which case it defaults to `HEAD`.
Detached HEAD DETACHED HEAD
------------- -------------
It is sometimes useful to be able to 'checkout' a commit that is It is sometimes useful to be able to 'checkout' a commit that is

View File

@ -30,6 +30,7 @@ struct checkout_opts {
int quiet; int quiet;
int merge; int merge;
int force; int force;
int force_detach;
int writeout_stage; int writeout_stage;
int writeout_error; int writeout_error;
@ -563,7 +564,7 @@ static void update_refs_for_switch(struct checkout_opts *opts,
if (!file_exists(ref_file) && file_exists(log_file)) if (!file_exists(ref_file) && file_exists(log_file))
remove_path(log_file); remove_path(log_file);
} }
} else if (strcmp(new->name, "HEAD")) { } else if (opts->force_detach || strcmp(new->name, "HEAD")) {
update_ref(msg.buf, "HEAD", new->commit->object.sha1, NULL, update_ref(msg.buf, "HEAD", new->commit->object.sha1, NULL,
REF_NODEREF, DIE_ON_ERR); REF_NODEREF, DIE_ON_ERR);
if (!opts->quiet) { if (!opts->quiet) {
@ -574,7 +575,8 @@ static void update_refs_for_switch(struct checkout_opts *opts,
} }
remove_branch_state(); remove_branch_state();
strbuf_release(&msg); strbuf_release(&msg);
if (!opts->quiet && (new->path || !strcmp(new->name, "HEAD"))) if (!opts->quiet &&
(new->path || (!opts->force_detach && !strcmp(new->name, "HEAD"))))
report_tracking(new); report_tracking(new);
} }
@ -677,6 +679,7 @@ static const char *unique_tracking_name(const char *name)
static int parse_branchname_arg(int argc, const char **argv, static int parse_branchname_arg(int argc, const char **argv,
int dwim_new_local_branch_ok, int dwim_new_local_branch_ok,
int force_detach,
struct branch_info *new, struct branch_info *new,
struct tree **source_tree, struct tree **source_tree,
unsigned char rev[20], unsigned char rev[20],
@ -753,7 +756,8 @@ static int parse_branchname_arg(int argc, const char **argv,
new->name = arg; new->name = arg;
setup_branch_path(new); setup_branch_path(new);
if (check_ref_format(new->path) == CHECK_REF_FORMAT_OK && if (!force_detach &&
check_ref_format(new->path) == CHECK_REF_FORMAT_OK &&
resolve_ref(new->path, branch_rev, 1, NULL)) resolve_ref(new->path, branch_rev, 1, NULL))
hashcpy(rev, branch_rev); hashcpy(rev, branch_rev);
else else
@ -804,6 +808,7 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
OPT_STRING('B', NULL, &opts.new_branch_force, "branch", OPT_STRING('B', NULL, &opts.new_branch_force, "branch",
"create/reset and checkout a branch"), "create/reset and checkout a branch"),
OPT_BOOLEAN('l', NULL, &opts.new_branch_log, "create reflog for new branch"), OPT_BOOLEAN('l', NULL, &opts.new_branch_log, "create reflog for new branch"),
OPT_BOOLEAN(0, "detach", &opts.force_detach, "detach the HEAD at named commit"),
OPT_SET_INT('t', "track", &opts.track, "set upstream info for new branch", OPT_SET_INT('t', "track", &opts.track, "set upstream info for new branch",
BRANCH_TRACK_EXPLICIT), BRANCH_TRACK_EXPLICIT),
OPT_STRING(0, "orphan", &opts.new_orphan_branch, "new branch", "new unparented branch"), OPT_STRING(0, "orphan", &opts.new_orphan_branch, "new branch", "new unparented branch"),
@ -842,9 +847,15 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
opts.new_branch = 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
|| opts.force_detach))
die ("--patch is incompatible with all other options"); die ("--patch is incompatible with all other options");
if (opts.force_detach && (opts.new_branch || opts.new_orphan_branch))
die("--detach cannot be used with -b/-B/--orphan");
if (opts.force_detach && 0 < opts.track)
die("--detach cannot be used with -t");
/* --track without -b should DWIM */ /* --track without -b should DWIM */
if (0 < opts.track && !opts.new_branch) { if (0 < opts.track && !opts.new_branch) {
const char *argv0 = argv[0]; const char *argv0 = argv[0];
@ -895,7 +906,8 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
dwim_new_local_branch && dwim_new_local_branch &&
opts.track == BRANCH_TRACK_UNSPECIFIED && opts.track == BRANCH_TRACK_UNSPECIFIED &&
!opts.new_branch; !opts.new_branch;
int n = parse_branchname_arg(argc, argv, dwim_ok, int n = parse_branchname_arg(argc, argv,
dwim_ok, opts.force_detach,
&new, &source_tree, rev, &opts.new_branch); &new, &source_tree, rev, &opts.new_branch);
argv += n; argv += n;
argc -= n; argc -= n;
@ -922,6 +934,9 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
} }
} }
if (opts.force_detach)
die("git checkout: --detach does not take a path argument");
if (1 < !!opts.writeout_stage + !!opts.force + !!opts.merge) if (1 < !!opts.writeout_stage + !!opts.force + !!opts.merge)
die("git checkout: --ours/--theirs, --force and --merge are incompatible when\nchecking out of the index."); die("git checkout: --ours/--theirs, --force and --merge are incompatible when\nchecking out of the index.");

95
t/t2020-checkout-detach.sh Executable file
View File

@ -0,0 +1,95 @@
#!/bin/sh
test_description='checkout into detached HEAD state'
. ./test-lib.sh
check_detached () {
test_must_fail git symbolic-ref -q HEAD >/dev/null
}
check_not_detached () {
git symbolic-ref -q HEAD >/dev/null
}
reset () {
git checkout master &&
check_not_detached
}
test_expect_success 'setup' '
test_commit one &&
test_commit two &&
git branch branch &&
git tag tag
'
test_expect_success 'checkout branch does not detach' '
reset &&
git checkout branch &&
check_not_detached
'
test_expect_success 'checkout tag detaches' '
reset &&
git checkout tag &&
check_detached
'
test_expect_success 'checkout branch by full name detaches' '
reset &&
git checkout refs/heads/branch &&
check_detached
'
test_expect_success 'checkout non-ref detaches' '
reset &&
git checkout branch^ &&
check_detached
'
test_expect_success 'checkout ref^0 detaches' '
reset &&
git checkout branch^0 &&
check_detached
'
test_expect_success 'checkout --detach detaches' '
reset &&
git checkout --detach branch &&
check_detached
'
test_expect_success 'checkout --detach without branch name' '
reset &&
git checkout --detach &&
check_detached
'
test_expect_success 'checkout --detach errors out for non-commit' '
reset &&
test_must_fail git checkout --detach one^{tree} &&
check_not_detached
'
test_expect_success 'checkout --detach errors out for extra argument' '
reset &&
git checkout master &&
test_must_fail git checkout --detach tag one.t &&
check_not_detached
'
test_expect_success 'checkout --detached and -b are incompatible' '
reset &&
test_must_fail git checkout --detach -b newbranch tag &&
check_not_detached
'
test_expect_success 'checkout --detach moves HEAD' '
reset &&
git checkout one &&
git checkout --detach two &&
git diff --exit-code HEAD &&
git diff --exit-code two
'
test_done