{"thread":{"id":"26433","subject":"[1.8.0] git checkout refs/heads/foo checks out branch foo","startedAt":"2011-02-07T11:01:51Z","lastAt":"2011-02-08T10:34:34Z","messageCount":18,"participants":["Martin von Zweigbergk","Junio C Hamano","Jeff King","Heiko Voigt","Sverre Rabbelier","Jonathan Nieder"],"isPatch":false,"patchVersion":null,"patchTotal":null},"messages":[{"id":"160597","messageId":"alpine.DEB.1.10.1102062234010.3788@debian","threadId":"26433","inReplyTo":null,"subject":"[1.8.0] git checkout refs/heads/foo checks out branch foo","fromName":"Martin von Zweigbergk","fromEmail":"martin.von.zweigbergk@gmail.com","sentAt":"2011-02-07T11:01:51Z","receivedAt":"2011-02-07T11:01:51Z","isPatch":false,"sender":{"key":"martinvonz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/891642?v=4"},"body":"Proposal:\n\n'git checkout refs/heads/foo' (or 'git checkout heads/foo' for that\nmatter) does not check out the branch, but instead detaches HEAD at\nfoo. This is quite counter-intuitive (at least to me) and the same\nfunctionality can be achieved by using e.g. foo~0. Change the behavior\nso that the branch is actually checked out. This also applies to\ne.g. 'git rebase master refs/heads/topic', which currently rebases a\ndetached HEAD. There are probably other examples as well that I'm not\naware of.\n\n\nRisks:\n\nExisting scripts may depend on the current behavior. It seems unlikely\nthat many users depend on it. Most likely, they use foo~0 or foo^0\ninstead.\n\n\nMigration plan:\n\nMake 'git checkout refs/head/foo' emit a warning in the next 1.7.x\nexplaining that its semantics will change in 1.8.0. Then change the\nbehavior in 1.8.0 and remove the warning.\n"},{"id":"160634","messageId":"7vwrlbtvm0.fsf@alter.siamese.dyndns.org","threadId":"26433","inReplyTo":"alpine.DEB.1.10.1102062234010.3788@debian","subject":"Re: [1.8.0] git checkout refs/heads/foo checks out branch foo","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2011-02-07T20:53:11Z","receivedAt":"2011-02-07T20:53:11Z","isPatch":false,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Martin von Zweigbergk <martin.von.zweigbergk@gmail.com> writes:\n\n> Proposal:\n>\n> 'git checkout refs/heads/foo' (or 'git checkout heads/foo' for that\n> matter) does not check out the branch, but instead detaches HEAD at\n> foo.\n\nYes, and it is deliberately so as it is a guaranteed-sure way for scripts\nto work around potential ref ambiguity when talking about the branch\ncalled 'foo' to spell it out with leading refs/heads/.\n\nAs long as you do not break scripts that guard against breakage you are\nproposing by saying \"refs/heads/foo^0\", we are Ok with the proposal, I\nthink.\n\n> Migration plan:\n>\n> Make 'git checkout refs/head/foo' emit a warning in the next 1.7.x\n> explaining that its semantics will change in 1.8.0. Then change the\n> behavior in 1.8.0 and remove the warning.\n\nReasonable.\n\nThanks.\n"},{"id":"160638","messageId":"20110207205934.GD13461@sigill.intra.peff.net","threadId":"26433","inReplyTo":"alpine.DEB.1.10.1102062234010.3788@debian","subject":"Re: [1.8.0] git checkout refs/heads/foo checks out branch foo","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2011-02-07T20:59:34Z","receivedAt":"2011-02-07T20:59:34Z","isPatch":false,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Mon, Feb 07, 2011 at 06:01:51AM -0500, Martin von Zweigbergk wrote:\n\n> 'git checkout refs/heads/foo' (or 'git checkout heads/foo' for that\n> matter) does not check out the branch, but instead detaches HEAD at\n> foo. This is quite counter-intuitive (at least to me) and the same\n> functionality can be achieved by using e.g. foo~0. Change the behavior\n> so that the branch is actually checked out. This also applies to\n> e.g. 'git rebase master refs/heads/topic', which currently rebases a\n> detached HEAD. There are probably other examples as well that I'm not\n> aware of.\n\nI have seen that behavior claimed as intentional, but I agree it is\nunintuitive. In most other places referring to a ref by a short name or\na fully qualified name is equivalent (except with respect to\ndisambiguating short names, of course).\n\n> Existing scripts may depend on the current behavior. It seems unlikely\n> that many users depend on it. Most likely, they use foo~0 or foo^0\n> instead.\n\nAs cool and clever as the foo^0 behavior is once you understand it, I\nthink it is a horribly confusing thing for non-experts. As part of this\nproposal, should we perhaps offer \"git checkout --detach\" as the\neasy-on-the-eyes way of intentionally detaching?\n\n-Peff\n"},{"id":"160641","messageId":"20110207211127.GG63976@book.hvoigt.net","threadId":"26433","inReplyTo":"alpine.DEB.1.10.1102062234010.3788@debian","subject":"Re: [1.8.0] git checkout refs/heads/foo checks out branch foo","fromName":"Heiko Voigt","fromEmail":"hvoigt@hvoigt.net","sentAt":"2011-02-07T21:11:27Z","receivedAt":"2011-02-07T21:11:27Z","isPatch":false,"sender":{"key":"hvoigt@hvoigt.net","avatar":"https://avatars.githubusercontent.com/u/184958?v=4"},"body":"Hallo Martin,\n\nOn Mon, Feb 07, 2011 at 06:01:51AM -0500, Martin von Zweigbergk wrote:\n> Proposal:\n> \n> 'git checkout refs/heads/foo' (or 'git checkout heads/foo' for that\n> matter) does not check out the branch, but instead detaches HEAD at\n> foo. This is quite counter-intuitive (at least to me) and the same\n> functionality can be achieved by using e.g. foo~0. Change the behavior\n> so that the branch is actually checked out. This also applies to\n> e.g. 'git rebase master refs/heads/topic', which currently rebases a\n> detached HEAD. There are probably other examples as well that I'm not\n> aware of.\n\nJust to clarify: You are not proposing that 'git checkout origin/master'\nwould also not checkout to a detached head, right? Because that is a\nfeature I am using frequently to test branches that have been pushed by\nanother developer to a remote server. If that would create a new local\nbranch that would be confusing.\n\nCheers Heiko\n"},{"id":"160646","messageId":"AANLkTinmqTi4cYbR6PtSxt6itCvFQDuT_sE1tjx45a3h@mail.gmail.com","threadId":"26433","inReplyTo":"20110207205934.GD13461@sigill.intra.peff.net","subject":"Re: [1.8.0] git checkout refs/heads/foo checks out branch foo","fromName":"Sverre Rabbelier","fromEmail":"srabbelier@gmail.com","sentAt":"2011-02-07T21:36:36Z","receivedAt":"2011-02-07T21:36:36Z","isPatch":false,"sender":{"key":"srabbelier@gmail.com","avatar":"https://avatars.githubusercontent.com/u/3098?v=4"},"body":"Heya,\n\nOn Mon, Feb 7, 2011 at 21:59, Jeff King <peff@peff.net> wrote:\n> As cool and clever as the foo^0 behavior is once you understand it, I\n> think it is a horribly confusing thing for non-experts. As part of this\n> proposal, should we perhaps offer \"git checkout --detach\" as the\n> easy-on-the-eyes way of intentionally detaching?\n\nNow _that_ is an excellent usability improvement, assuming we want to\nencourage detaching HEAD... do we?\n\n-- \nCheers,\n\nSverre Rabbelier\n"},{"id":"160651","messageId":"20110207220030.GA19357@elie","threadId":"26433","inReplyTo":"AANLkTinmqTi4cYbR6PtSxt6itCvFQDuT_sE1tjx45a3h@mail.gmail.com","subject":"Re: [1.8.0] git checkout refs/heads/foo checks out branch foo","fromName":"Jonathan Nieder","fromEmail":"jrnieder@gmail.com","sentAt":"2011-02-07T22:00:30Z","receivedAt":"2011-02-07T22:00:30Z","isPatch":false,"sender":{"key":"jrnieder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/281595?v=4"},"body":"Sverre Rabbelier wrote:\n\n> Now _that_ is an excellent usability improvement, assuming we want to\n> encourage detaching HEAD... do we?\n\nYes.\n\n-- 8<\nSubject: commit: document --detach synonym for \"git checkout foo^{commit}\"\n\nFor example, one might use this when making a temporary merge to test\nthat two topics work well together.\n\nThis patch just documents the option.  It is not meant for application\nwithout an implementation and tests for the option.\n\nSuggested-by: Jeff King <peff@peff.net>\nSigned-off-by: Jonathan Nieder <jrnieder@gmail.com>\n---\n Documentation/git-checkout.txt |   13 +++++++++++--\n 1 files changed, 11 insertions(+), 2 deletions(-)\n\ndiff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt\nindex 880763d..698ae6c 100644\n--- a/Documentation/git-checkout.txt\n+++ b/Documentation/git-checkout.txt\n@@ -9,6 +9,7 @@ SYNOPSIS\n --------\n [verse]\n 'git checkout' [-q] [-f] [-m] [<branch>]\n+'git checkout' [-q] [-f] [-n] [--detach] [<commit>]\n 'git checkout' [-q] [-f] [-m] [[-b|-B|--orphan] <new_branch>] [<start_point>]\n 'git checkout' [-f|--ours|--theirs|-m|--conflict=<style>] [<tree-ish>] [--] <paths>...\n 'git checkout' --patch [<tree-ish>] [--] [<paths>...]\n@@ -22,9 +23,10 @@ branch.\n \n 'git checkout' [<branch>]::\n 'git checkout' -b|-B <new_branch> [<start point>]::\n+'git checkout' [--detach] [<commit>]::\n \n \tThis form switches branches by updating the index, working\n-\ttree, and HEAD to reflect the specified branch.\n+\ttree, and HEAD to reflect the specified branch or commit.\n +\n If `-b` is given, a new branch is created as if linkgit:git-branch[1]\n were called and then checked out; in this case you can\n@@ -115,6 +117,13 @@ explicitly give a name with '-b' in such a case.\n \tCreate the new branch's reflog; see linkgit:git-branch[1] for\n \tdetails.\n \n+--detach::\n+\tRather than checking out a branch to work on it, check out a\n+\tcommit for inspection and discardable experiments.\n+\tThis is the default behavior of \"git checkout <commit>\" when\n+\t<commit> is not a branch name.  See the \"DETACHED HEAD\" section\n+\tbelow for details.\n+\n --orphan::\n \tCreate a new 'orphan' branch, named <new_branch>, started from\n \t<start_point> and switch to it.  The first commit made on this\n@@ -204,7 +213,7 @@ leave out at most one of `A` and `B`, in which case it defaults to `HEAD`.\n \n \n \n-Detached HEAD\n+DETACHED HEAD\n -------------\n \n It is sometimes useful to be able to 'checkout' a commit that is\n-- \n1.7.4\n"},{"id":"160656","messageId":"7vaai7s9g4.fsf@alter.siamese.dyndns.org","threadId":"26433","inReplyTo":"20110207220030.GA19357@elie","subject":"Re: [1.8.0] git checkout refs/heads/foo checks out branch foo","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2011-02-07T23:37:15Z","receivedAt":"2011-02-07T23:37:15Z","isPatch":false,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jonathan Nieder <jrnieder@gmail.com> writes:\n\n> Sverre Rabbelier wrote:\n>\n>> Now _that_ is an excellent usability improvement, assuming we want to\n>> encourage detaching HEAD... do we?\n>\n> Yes.\n>\n> -- 8<\n> Subject: commit: document --detach synonym for \"git checkout foo^{commit}\"\n>\n> For example, one might use this when making a temporary merge to test\n> that two topics work well together.\n>\n> This patch just documents the option.  It is not meant for application\n> without an implementation and tests for the option.\n\nOn top of v1.7.3.5-1-g0cb6ad3 (uk/checkout-ambiguous-ref)...\n\n builtin/checkout.c |   12 +++++++++---\n 1 files changed, 9 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex 953abdd..141f6a3 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -685,6 +685,7 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)\n \tchar *conflict_style = NULL;\n \tint patch_mode = 0;\n \tint dwim_new_local_branch = 1;\n+\tint force_detach = 0;\n \tstruct option options[] = {\n \t\tOPT__QUIET(&opts.quiet),\n \t\tOPT_STRING('b', NULL, &opts.new_branch, \"branch\",\n@@ -692,6 +693,7 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)\n \t\tOPT_STRING('B', NULL, &opts.new_branch_force, \"branch\",\n \t\t\t   \"create/reset and checkout a branch\"),\n \t\tOPT_BOOLEAN('l', NULL, &opts.new_branch_log, \"create reflog for new branch\"),\n+\t\tOPT_BOOLEAN(0, \"detach\", &force_detach, \"detach the HEAD at named commit\"),\n \t\tOPT_SET_INT('t', \"track\",  &opts.track, \"set upstream info for new branch\",\n \t\t\tBRANCH_TRACK_EXPLICIT),\n \t\tOPT_STRING(0, \"orphan\", &opts.new_orphan_branch, \"new branch\", \"new unparented branch\"),\n@@ -726,6 +728,9 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)\n \tif (opts.new_branch && opts.new_branch_force)\n \t\tdie(\"-B cannot be used with -b\");\n \n+\tif ((opts.new_branch || opts.new_orphan_branch) && force_detach)\n+\t\tdie(\"--detach cannot be used with -b/-B/--orphan\");\n+\n \t/* copy -B over to -b, so that we can just check the latter */\n \tif (opts.new_branch_force)\n \t\topts.new_branch = opts.new_branch_force;\n@@ -834,7 +839,8 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)\n \t\tnew.name = arg;\n \t\tsetup_branch_path(&new);\n \n-\t\tif (check_ref_format(new.path) == CHECK_REF_FORMAT_OK &&\n+\t\tif (!force_detach &&\n+\t\t    check_ref_format(new.path) == CHECK_REF_FORMAT_OK &&\n \t\t    resolve_ref(new.path, branch_rev, 1, NULL))\n \t\t\thashcpy(rev, branch_rev);\n \t\telse\n"},{"id":"160657","messageId":"20110207234526.GA28336@sigill.intra.peff.net","threadId":"26433","inReplyTo":"7vaai7s9g4.fsf@alter.siamese.dyndns.org","subject":"Re: [1.8.0] git checkout refs/heads/foo checks out branch foo","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2011-02-07T23:45:26Z","receivedAt":"2011-02-07T23:45:26Z","isPatch":false,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Mon, Feb 07, 2011 at 03:37:15PM -0800, Junio C Hamano wrote:\n\n> > Subject: commit: document --detach synonym for \"git checkout foo^{commit}\"\n> >\n> > For example, one might use this when making a temporary merge to test\n> > that two topics work well together.\n> >\n> > This patch just documents the option.  It is not meant for application\n> > without an implementation and tests for the option.\n> \n> On top of v1.7.3.5-1-g0cb6ad3 (uk/checkout-ambiguous-ref)...\n\nWell, I started on tests and your email came just as I was about to\nactually implement. So here are the tests. We didn't seem to have any\nexplicit checkout-detached tests before, so I tried to cover existing\nmethods in addition to the new option (which means Martin will need to\ntweak one of the tests below when implementing his proposal).\n\nJonathan, do you want to roll all of these up into a single patch?\n\n---\n t/t2020-checkout-detach.sh |   62 ++++++++++++++++++++++++++++++++++++++++++++\n 1 files changed, 62 insertions(+), 0 deletions(-)\n create mode 100755 t/t2020-checkout-detach.sh\n\ndiff --git a/t/t2020-checkout-detach.sh b/t/t2020-checkout-detach.sh\nnew file mode 100755\nindex 0000000..886e186\n--- /dev/null\n+++ b/t/t2020-checkout-detach.sh\n@@ -0,0 +1,62 @@\n+#!/bin/sh\n+\n+test_description='checkout into detached HEAD state'\n+. ./test-lib.sh\n+\n+check_detached() {\n+\t! git symbolic-ref -q HEAD >/dev/null\n+}\n+\n+check_not_detached() {\n+\t! check_detached\n+}\n+\n+reset() {\n+\tgit checkout master &&\n+\tcheck_not_detached\n+}\n+\n+test_expect_success 'setup' '\n+\ttest_commit one &&\n+\ttest_commit two &&\n+\tgit branch branch &&\n+\tgit tag tag\n+'\n+\n+test_expect_success 'checkout branch does not detach' '\n+\treset &&\n+\tgit checkout branch &&\n+\tcheck_not_detached\n+'\n+\n+test_expect_success 'checkout tag detaches' '\n+\treset &&\n+\tgit checkout tag &&\n+\tcheck_detached\n+'\n+\n+test_expect_success 'checkout branch by full name detaches' '\n+\treset &&\n+\tgit checkout refs/heads/branch &&\n+\tcheck_detached\n+'\n+\n+test_expect_success 'checkout non-ref detaches' '\n+\treset &&\n+\tgit checkout branch^ &&\n+\tcheck_detached\n+'\n+\n+test_expect_success 'checkout ref^0 detaches' '\n+\treset &&\n+\tgit checkout branch^0 &&\n+\tcheck_detached\n+'\n+\n+test_expect_success 'checkout --detach detaches' '\n+\treset &&\n+\tgit checkout --detach branch &&\n+\tcheck_detached\n+'\n+\n+test_done\n-- \n1.7.4.rc2.27.gd0787\n"},{"id":"160658","messageId":"20110208000151.GA19944@elie","threadId":"26433","inReplyTo":"20110207234526.GA28336@sigill.intra.peff.net","subject":"Re: [1.8.0] git checkout refs/heads/foo checks out branch foo","fromName":"Jonathan Nieder","fromEmail":"jrnieder@gmail.com","sentAt":"2011-02-08T00:01:52Z","receivedAt":"2011-02-08T00:01:52Z","isPatch":false,"sender":{"key":"jrnieder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/281595?v=4"},"body":"Jeff King wrote:\n\n> Jonathan, do you want to roll all of these up into a single patch?\n\nSounds good.\n\n> +++ b/t/t2020-checkout-detach.sh\n[...]\n> +check_detached() {\n> +\t! git symbolic-ref -q HEAD >/dev/null\n> +}\n> +\n> +check_not_detached() {\n> +\t! check_detached\n> +}\n\nTo be pedantic, I'll put\n\n check_detached () {\n\ttest_must_fail git symbolic-ref -q HEAD >/dev/null\n }\n\n check_not_detached () {\n\tgit symbolic-ref -q HEAD >/dev/null\n }\n\nand add some more paranoid tests:\n\n test_expect_success 'checkout --detach without branch name detaches' '\n        reset &&\n\tgit checkout --detach &&\n\tcheck_detached\n '\n\n test_expect_success 'checkout --detach catches error in usage' '\n\treset &&\n\tgit checkout master &&\n\ttest_must_fail git checkout --detach tag nonsense &&\n\tcheck_not_detached\n '\n\n test_expect_success 'checkout --detach moves HEAD' '\n\treset &&\n\tgit checkout one &&\n\tgit checkout --detach two &&\n\tgit diff --exit-code HEAD &&\n\tgit diff --exit-code two\n '\n"},{"id":"160659","messageId":"alpine.DEB.1.10.1102071918430.3788@debian","threadId":"26433","inReplyTo":"20110207211127.GG63976@book.hvoigt.net","subject":"Re: [1.8.0] git checkout refs/heads/foo checks out branch foo","fromName":"Martin von Zweigbergk","fromEmail":"martin.von.zweigbergk@gmail.com","sentAt":"2011-02-08T00:22:21Z","receivedAt":"2011-02-08T00:22:21Z","isPatch":false,"sender":{"key":"martinvonz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/891642?v=4"},"body":"On Mon, 7 Feb 2011, Heiko Voigt wrote:\n\n> Hallo Martin,\n> \n> On Mon, Feb 07, 2011 at 06:01:51AM -0500, Martin von Zweigbergk wrote:\n> > Proposal:\n> > \n> > 'git checkout refs/heads/foo' (or 'git checkout heads/foo' for that\n> > matter) does not check out the branch, but instead detaches HEAD at\n> > foo. This is quite counter-intuitive (at least to me) and the same\n> > functionality can be achieved by using e.g. foo~0. Change the behavior\n> > so that the branch is actually checked out. This also applies to\n> > e.g. 'git rebase master refs/heads/topic', which currently rebases a\n> > detached HEAD. There are probably other examples as well that I'm not\n> > aware of.\n> \n> Just to clarify: You are not proposing that 'git checkout origin/master'\n> would also not checkout to a detached head, right? Because that is a\n> feature I am using frequently to test branches that have been pushed by\n> another developer to a remote server. If that would create a new local\n> branch that would be confusing.\n\nNope, I'm not proposing that. I wouldn't want that either.\n\n/Martin\n"},{"id":"160660","messageId":"20110208002810.GA27181@sigill.intra.peff.net","threadId":"26433","inReplyTo":"20110208000151.GA19944@elie","subject":"Re: [1.8.0] git checkout refs/heads/foo checks out branch foo","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2011-02-08T00:28:10Z","receivedAt":"2011-02-08T00:28:10Z","isPatch":false,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Mon, Feb 07, 2011 at 06:01:52PM -0600, Jonathan Nieder wrote:\n\n> > Jonathan, do you want to roll all of these up into a single patch?\n> \n> Sounds good.\n\nThanks.\n\n> To be pedantic, I'll put\n> [...]\n\nAll look good to me.\n\n-Peff\n"},{"id":"160661","messageId":"alpine.DEB.1.10.1102071925440.3788@debian","threadId":"26433","inReplyTo":"20110207234526.GA28336@sigill.intra.peff.net","subject":"Re: [1.8.0] git checkout refs/heads/foo checks out branch foo","fromName":"Martin von Zweigbergk","fromEmail":"martin.von.zweigbergk@gmail.com","sentAt":"2011-02-08T00:31:12Z","receivedAt":"2011-02-08T00:31:12Z","isPatch":false,"sender":{"key":"martinvonz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/891642?v=4"},"body":"On Mon, 7 Feb 2011, Jeff King wrote:\n\n> On Mon, Feb 07, 2011 at 03:37:15PM -0800, Junio C Hamano wrote:\n> \n> > > Subject: commit: document --detach synonym for \"git checkout foo^{commit}\"\n> > >\n> > > For example, one might use this when making a temporary merge to test\n> > > that two topics work well together.\n> > >\n> > > This patch just documents the option.  It is not meant for application\n> > > without an implementation and tests for the option.\n> > \n> > On top of v1.7.3.5-1-g0cb6ad3 (uk/checkout-ambiguous-ref)...\n> \n> Well, I started on tests and your email came just as I was about to\n> actually implement. So here are the tests. We didn't seem to have any\n> explicit checkout-detached tests before, so I tried to cover existing\n> methods in addition to the new option (which means Martin will need to\n> tweak one of the tests below when implementing his proposal).\n\nOops, I didn't know writing a proposal meant signing up to do it as\nwell ;-). But seriously, this seems like a relatively small change, so\nit will hopefully be a good reason for me to push myself to get\nstarted with the C code as well.\n"},{"id":"160664","messageId":"20110208005238.GB24340@elie","threadId":"26433","inReplyTo":"20110207234526.GA28336@sigill.intra.peff.net","subject":"[PATCH/WIP] checkout: introduce --detach synonym for \"git checkout foo^{commit}\"","fromName":"Jonathan Nieder","fromEmail":"jrnieder@gmail.com","sentAt":"2011-02-08T00:52:53Z","receivedAt":"2011-02-08T00:52:53Z","isPatch":true,"sender":{"key":"jrnieder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/281595?v=4"},"body":"From: Junio C Hamano <gitster@pobox.com>\n\nFor example, one might use this when making a temporary merge to\ntest that two topics work well together.\n\nPatch by Junio, tests from Jeff King.\n\nSuggested-by: Jeff King <peff@peff.net>\nSigned-off-by: Jonathan Nieder <jrnieder@gmail.com>\n---\nJeff King wrote:\n\n> Jonathan, do you want to roll all of these up into a single patch?\n\nOkay, here it is.  Two of the new tests fail. :)\n\n Documentation/git-checkout.txt |   13 +++++-\n builtin/checkout.c             |    8 +++-\n t/t2020-checkout-detach.sh     |   89 ++++++++++++++++++++++++++++++++++++++++\n 3 files changed, 107 insertions(+), 3 deletions(-)\n create mode 100755 t/t2020-checkout-detach.sh\n\ndiff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt\nindex 22d3611..d162117 100644\n--- a/Documentation/git-checkout.txt\n+++ b/Documentation/git-checkout.txt\n@@ -9,6 +9,7 @@ SYNOPSIS\n --------\n [verse]\n 'git checkout' [-q] [-f] [-m] [<branch>]\n+'git checkout' [-q] [-f] [-m] [--detach] [<commit>]\n 'git checkout' [-q] [-f] [-m] [[-b|-B|--orphan] <new_branch>] [<start_point>]\n 'git checkout' [-f|--ours|--theirs|-m|--conflict=<style>] [<tree-ish>] [--] <paths>...\n 'git checkout' --patch [<tree-ish>] [--] [<paths>...]\n@@ -22,9 +23,10 @@ branch.\n \n 'git checkout' [<branch>]::\n 'git checkout' -b|-B <new_branch> [<start point>]::\n+'git checkout' [--detach] [<commit>]::\n \n \tThis form switches branches by updating the index, working\n-\ttree, and HEAD to reflect the specified branch.\n+\ttree, and HEAD to reflect the specified branch or commit.\n +\n If `-b` is given, a new branch is created as if linkgit:git-branch[1]\n were called and then checked out; in this case you can\n@@ -115,6 +117,13 @@ explicitly give a name with '-b' in such a case.\n \tCreate the new branch's reflog; see linkgit:git-branch[1] for\n \tdetails.\n \n+--detach::\n+\tRather than checking out a branch to work on it, check out a\n+\tcommit for inspection and discardable experiments.\n+\tThis is the default behavior of \"git checkout <commit>\" when\n+\t<commit> is not a branch name.  See the \"DETACHED HEAD\" section\n+\tbelow for details.\n+\n --orphan::\n \tCreate a new 'orphan' branch, named <new_branch>, started from\n \t<start_point> and switch to it.  The first commit made on this\n@@ -204,7 +213,7 @@ leave out at most one of `A` and `B`, in which case it defaults to `HEAD`.\n \n \n \n-Detached HEAD\n+DETACHED HEAD\n -------------\n \n It is sometimes useful to be able to 'checkout' a commit that is\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex 953abdd..526abb9 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -685,6 +685,7 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)\n \tchar *conflict_style = NULL;\n \tint patch_mode = 0;\n \tint dwim_new_local_branch = 1;\n+\tint force_detach = 0;\n \tstruct option options[] = {\n \t\tOPT__QUIET(&opts.quiet),\n \t\tOPT_STRING('b', NULL, &opts.new_branch, \"branch\",\n@@ -692,6 +693,7 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)\n \t\tOPT_STRING('B', NULL, &opts.new_branch_force, \"branch\",\n \t\t\t   \"create/reset and checkout a branch\"),\n \t\tOPT_BOOLEAN('l', NULL, &opts.new_branch_log, \"create reflog for new branch\"),\n+\t\tOPT_BOOLEAN(0, \"detach\", &force_detach, \"detach the HEAD at named commit\"),\n \t\tOPT_SET_INT('t', \"track\",  &opts.track, \"set upstream info for new branch\",\n \t\t\tBRANCH_TRACK_EXPLICIT),\n \t\tOPT_STRING(0, \"orphan\", &opts.new_orphan_branch, \"new branch\", \"new unparented branch\"),\n@@ -726,6 +728,9 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)\n \tif (opts.new_branch && opts.new_branch_force)\n \t\tdie(\"-B cannot be used with -b\");\n \n+\tif ((opts.new_branch || opts.new_orphan_branch) && force_detach)\n+\t\tdie(\"--detach cannot be used with -b/-B/--orphan\");\n+\n \t/* copy -B over to -b, so that we can just check the latter */\n \tif (opts.new_branch_force)\n \t\topts.new_branch = opts.new_branch_force;\n@@ -834,7 +839,8 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)\n \t\tnew.name = arg;\n \t\tsetup_branch_path(&new);\n \n-\t\tif (check_ref_format(new.path) == CHECK_REF_FORMAT_OK &&\n+\t\tif (!force_detach &&\n+\t\t    check_ref_format(new.path) == CHECK_REF_FORMAT_OK &&\n \t\t    resolve_ref(new.path, branch_rev, 1, NULL))\n \t\t\thashcpy(rev, branch_rev);\n \t\telse\ndiff --git a/t/t2020-checkout-detach.sh b/t/t2020-checkout-detach.sh\nnew file mode 100755\nindex 0000000..e57f253\n--- /dev/null\n+++ b/t/t2020-checkout-detach.sh\n@@ -0,0 +1,89 @@\n+#!/bin/sh\n+\n+test_description='checkout into detached HEAD state'\n+. ./test-lib.sh\n+\n+check_detached () {\n+\ttest_must_fail git symbolic-ref -q HEAD >/dev/null\n+}\n+\n+check_not_detached () {\n+\tgit symbolic-ref -q HEAD >/dev/null\n+}\n+\n+reset () {\n+\tgit checkout master &&\n+\tcheck_not_detached\n+}\n+\n+test_expect_success 'setup' '\n+\ttest_commit one &&\n+\ttest_commit two &&\n+\tgit branch branch &&\n+\tgit tag tag\n+'\n+\n+test_expect_success 'checkout branch does not detach' '\n+\treset &&\n+\tgit checkout branch &&\n+\tcheck_not_detached\n+'\n+\n+test_expect_success 'checkout tag detaches' '\n+\treset &&\n+\tgit checkout tag &&\n+\tcheck_detached\n+'\n+\n+test_expect_success 'checkout branch by full name detaches' '\n+\treset &&\n+\tgit checkout refs/heads/branch &&\n+\tcheck_detached\n+'\n+\n+test_expect_success 'checkout non-ref detaches' '\n+\treset &&\n+\tgit checkout branch^ &&\n+\tcheck_detached\n+'\n+\n+test_expect_success 'checkout ref^0 detaches' '\n+\treset &&\n+\tgit checkout branch^0 &&\n+\tcheck_detached\n+'\n+\n+test_expect_success 'checkout --detach detaches' '\n+\treset &&\n+\tgit checkout --detach branch &&\n+\tcheck_detached\n+'\n+\n+test_expect_failure 'checkout --detach without branch name' '\n+\treset &&\n+\tgit checkout --detach &&\n+\tcheck_detached\n+'\n+\n+test_expect_failure 'checkout --detach errors out for extra argument' '\n+\treset &&\n+\tgit checkout master &&\n+\ttest_expect_code 129 git checkout --detach tag nonsense &&\n+\tcheck_not_detached\n+'\n+\n+test_expect_success 'checkout --detached and -b are incompatible' '\n+\tcheck_not_detached &&\n+\ttest_must_fail git checkout --detach -b newbranch tag &&\n+\tcheck_not_detached\n+'\n+\n+test_expect_success 'checkout --detach moves HEAD' '\n+\treset &&\n+\tgit checkout one &&\n+\tgit checkout --detach two &&\n+\tgit diff --exit-code HEAD &&\n+\tgit diff --exit-code two\n+'\n+\n+test_done\n-- \n1.7.4\n"},{"id":"160666","messageId":"20110208005555.GC24340@elie","threadId":"26433","inReplyTo":"20110208005238.GB24340@elie","subject":"Re: [PATCH/WIP] checkout: introduce --detach synonym for \"git checkout foo^{commit}\"","fromName":"Jonathan Nieder","fromEmail":"jrnieder@gmail.com","sentAt":"2011-02-08T00:55:55Z","receivedAt":"2011-02-08T00:55:55Z","isPatch":true,"sender":{"key":"jrnieder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/281595?v=4"},"body":"Jonathan Nieder wrote:\n\n> Jeff King wrote:\n>\n>> Jonathan, do you want to roll all of these up into a single patch?\n>\n> Okay, here it is.\n\nAh, should have mentioned: this applies on top of a merge of\nen/and-cascade-tests~25 (aka v1.7.4-rc0~65^2~19, test-lib: make\ntest_expect_code a test command, 2010-10-03) and\nuk/checkout-ambiguous-ref.\n"},{"id":"160689","messageId":"20110208102605.GA29660@elie","threadId":"26433","inReplyTo":"20110208005238.GB24340@elie","subject":"[PATCH v2 0/3] checkout: introduce --detach synonym for \"git checkout foo^{commit}\"","fromName":"Jonathan Nieder","fromEmail":"jrnieder@gmail.com","sentAt":"2011-02-08T10:26:05Z","receivedAt":"2011-02-08T10:26:05Z","isPatch":true,"sender":{"key":"jrnieder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/281595?v=4"},"body":"Jonathan Nieder wrote:\n\n> Two of the new tests fail. :)\n\n... and here's a reroll fixing them.  Applies on top of the old\nuk/checkout-ambiguous-ref (0cb6ad3, 2011-01-11).\n\nPatch 1 is an irrelevant cleanup, for while I have your eyes on this\ncode. :)\n\nPatch 2 introduces the --detach option, taking care to error out in\nspecial cases.\n\nPatch 3 is another cleanup, to make the effect of patch 2 more clear.\n\nThanks again for your help.\nJonathan Nieder (2):\n  checkout: split off a function to peel away branchname arg\n  checkout: rearrange update_refs_for_switch for clarity\n\nJunio C Hamano (1):\n  checkout: introduce --detach synonym for \"git checkout foo^{commit}\"\n\n Documentation/git-checkout.txt |   13 ++-\n builtin/checkout.c             |  265 +++++++++++++++++++++++----------------\n t/t2020-checkout-detach.sh     |   95 ++++++++++++++\n 3 files changed, 262 insertions(+), 111 deletions(-)\n create mode 100755 t/t2020-checkout-detach.sh\n"},{"id":"160690","messageId":"20110208102909.GB29660@elie","threadId":"26433","inReplyTo":"20110208102605.GA29660@elie","subject":"[PATCH 1/3] checkout: split off a function to peel away branchname arg","fromName":"Jonathan Nieder","fromEmail":"jrnieder@gmail.com","sentAt":"2011-02-08T10:29:09Z","receivedAt":"2011-02-08T10:29:09Z","isPatch":true,"sender":{"key":"jrnieder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/281595?v=4"},"body":"The code to parse and consume the tree name and \"--\" in commands such\nas \"git checkout @{-1} -- '*.c'\" is intimidatingly long.  Split it out\ninto a separate function and make it easier to skip on first reading\nby making the data it uses and produces more explicit.\n\nNo functional change intended.\n\nSigned-off-by: Jonathan Nieder <jrnieder@gmail.com>\n---\nJust some code movement.\n\n builtin/checkout.c |  229 ++++++++++++++++++++++++++++++----------------------\n 1 files changed, 131 insertions(+), 98 deletions(-)\n\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex 953abdd..0e7a6a3 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -675,11 +675,123 @@ static const char *unique_tracking_name(const char *name)\n \treturn NULL;\n }\n \n+static int parse_branchname_arg(int argc, const char **argv,\n+\t\t\t\tint dwim_new_local_branch_ok,\n+\t\t\t\tstruct branch_info *new,\n+\t\t\t\tstruct tree **source_tree,\n+\t\t\t\tunsigned char rev[20],\n+\t\t\t\tconst char **new_branch)\n+{\n+\tint argcount = 0;\n+\tunsigned char branch_rev[20];\n+\tconst char *arg;\n+\tint has_dash_dash;\n+\n+\t/*\n+\t * case 1: git checkout <ref> -- [<paths>]\n+\t *\n+\t *   <ref> must be a valid tree, everything after the '--' must be\n+\t *   a path.\n+\t *\n+\t * case 2: git checkout -- [<paths>]\n+\t *\n+\t *   everything after the '--' must be paths.\n+\t *\n+\t * case 3: git checkout <something> [<paths>]\n+\t *\n+\t *   With no paths, if <something> is a commit, that is to\n+\t *   switch to the branch or detach HEAD at it.  As a special case,\n+\t *   if <something> is A...B (missing A or B means HEAD but you can\n+\t *   omit at most one side), and if there is a unique merge base\n+\t *   between A and B, A...B names that merge base.\n+\t *\n+\t *   With no paths, if <something> is _not_ a commit, no -t nor -b\n+\t *   was given, and there is a tracking branch whose name is\n+\t *   <something> in one and only one remote, then this is a short-hand\n+\t *   to fork local <something> from that remote tracking branch.\n+\t *\n+\t *   Otherwise <something> shall not be ambiguous.\n+\t *   - If it's *only* a reference, treat it like case (1).\n+\t *   - If it's only a path, treat it like case (2).\n+\t *   - else: fail.\n+\t *\n+\t */\n+\tif (!argc)\n+\t\treturn 0;\n+\n+\tif (!strcmp(argv[0], \"--\"))\t/* case (2) */\n+\t\treturn 1;\n+\n+\targ = argv[0];\n+\thas_dash_dash = (argc > 1) && !strcmp(argv[1], \"--\");\n+\n+\tif (!strcmp(arg, \"-\"))\n+\t\targ = \"@{-1}\";\n+\n+\tif (get_sha1_mb(arg, rev)) {\n+\t\tif (has_dash_dash)          /* case (1) */\n+\t\t\tdie(\"invalid reference: %s\", arg);\n+\t\tif (dwim_new_local_branch_ok &&\n+\t\t    !check_filename(NULL, arg) &&\n+\t\t    argc == 1) {\n+\t\t\tconst char *remote = unique_tracking_name(arg);\n+\t\t\tif (!remote || get_sha1(remote, rev))\n+\t\t\t\treturn argcount;\n+\t\t\t*new_branch = arg;\n+\t\t\targ = remote;\n+\t\t\t/* DWIMmed to create local branch */\n+\t\t} else {\n+\t\t\treturn argcount;\n+\t\t}\n+\t}\n+\n+\t/* we can't end up being in (2) anymore, eat the argument */\n+\targcount++;\n+\targv++;\n+\targc--;\n+\n+\tnew->name = arg;\n+\tsetup_branch_path(new);\n+\n+\tif (check_ref_format(new->path) == CHECK_REF_FORMAT_OK &&\n+\t    resolve_ref(new->path, branch_rev, 1, NULL))\n+\t\thashcpy(rev, branch_rev);\n+\telse\n+\t\tnew->path = NULL; /* not an existing branch */\n+\n+\tnew->commit = lookup_commit_reference_gently(rev, 1);\n+\tif (!new->commit) {\n+\t\t/* not a commit */\n+\t\t*source_tree = parse_tree_indirect(rev);\n+\t} else {\n+\t\tparse_commit(new->commit);\n+\t\t*source_tree = new->commit->tree;\n+\t}\n+\n+\tif (!*source_tree)                   /* case (1): want a tree */\n+\t\tdie(\"reference is not a tree: %s\", arg);\n+\tif (!has_dash_dash) {/* case (3 -> 1) */\n+\t\t/*\n+\t\t * Do not complain the most common case\n+\t\t *\tgit checkout branch\n+\t\t * even if there happen to be a file called 'branch';\n+\t\t * it would be extremely annoying.\n+\t\t */\n+\t\tif (argc)\n+\t\t\tverify_non_filename(NULL, arg);\n+\t} else {\n+\t\targcount++;\n+\t\targv++;\n+\t\targc--;\n+\t}\n+\n+\treturn argcount;\n+}\n+\n int cmd_checkout(int argc, const char **argv, const char *prefix)\n {\n \tstruct checkout_opts opts;\n-\tunsigned char rev[20], branch_rev[20];\n-\tconst char *arg;\n+\tunsigned char rev[20];\n \tstruct branch_info new;\n \tstruct tree *source_tree = NULL;\n \tchar *conflict_style = NULL;\n@@ -709,7 +821,6 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)\n \t\t  PARSE_OPT_NOARG | PARSE_OPT_HIDDEN },\n \t\tOPT_END(),\n \t};\n-\tint has_dash_dash;\n \n \tmemset(&opts, 0, sizeof(opts));\n \tmemset(&new, 0, sizeof(new));\n@@ -766,108 +877,30 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)\n \t\tdie(\"git checkout: -f and -m are incompatible\");\n \n \t/*\n-\t * case 1: git checkout <ref> -- [<paths>]\n-\t *\n-\t *   <ref> must be a valid tree, everything after the '--' must be\n-\t *   a path.\n-\t *\n-\t * case 2: git checkout -- [<paths>]\n-\t *\n-\t *   everything after the '--' must be paths.\n-\t *\n-\t * case 3: git checkout <something> [<paths>]\n-\t *\n-\t *   With no paths, if <something> is a commit, that is to\n-\t *   switch to the branch or detach HEAD at it.  As a special case,\n-\t *   if <something> is A...B (missing A or B means HEAD but you can\n-\t *   omit at most one side), and if there is a unique merge base\n-\t *   between A and B, A...B names that merge base.\n-\t *\n-\t *   With no paths, if <something> is _not_ a commit, no -t nor -b\n-\t *   was given, and there is a tracking branch whose name is\n-\t *   <something> in one and only one remote, then this is a short-hand\n-\t *   to fork local <something> from that remote tracking branch.\n-\t *\n-\t *   Otherwise <something> shall not be ambiguous.\n-\t *   - If it's *only* a reference, treat it like case (1).\n-\t *   - If it's only a path, treat it like case (2).\n-\t *   - else: fail.\n-\t *\n+\t * Extract branch name from command line arguments, so\n+\t * all that is left is pathspecs.\n+\t *\n+\t * Handle\n+\t *\n+\t *  1) git checkout <tree> -- [<paths>]\n+\t *  2) git checkout -- [<paths>]\n+\t *  3) git checkout <something> [<paths>]\n+\t *\n+\t * including \"last branch\" syntax and DWIM-ery for names of\n+\t * remote branches, erroring out for invalid or ambiguous cases.\n \t */\n \tif (argc) {\n-\t\tif (!strcmp(argv[0], \"--\")) {       /* case (2) */\n-\t\t\targv++;\n-\t\t\targc--;\n-\t\t\tgoto no_reference;\n-\t\t}\n-\n-\t\targ = argv[0];\n-\t\thas_dash_dash = (argc > 1) && !strcmp(argv[1], \"--\");\n-\n-\t\tif (!strcmp(arg, \"-\"))\n-\t\t\targ = \"@{-1}\";\n-\n-\t\tif (get_sha1_mb(arg, rev)) {\n-\t\t\tif (has_dash_dash)          /* case (1) */\n-\t\t\t\tdie(\"invalid reference: %s\", arg);\n-\t\t\tif (!patch_mode &&\n-\t\t\t    dwim_new_local_branch &&\n-\t\t\t    opts.track == BRANCH_TRACK_UNSPECIFIED &&\n-\t\t\t    !opts.new_branch &&\n-\t\t\t    !check_filename(NULL, arg) &&\n-\t\t\t    argc == 1) {\n-\t\t\t\tconst char *remote = unique_tracking_name(arg);\n-\t\t\t\tif (!remote || get_sha1(remote, rev))\n-\t\t\t\t\tgoto no_reference;\n-\t\t\t\topts.new_branch = arg;\n-\t\t\t\targ = remote;\n-\t\t\t\t/* DWIMmed to create local branch */\n-\t\t\t}\n-\t\t\telse\n-\t\t\t\tgoto no_reference;\n-\t\t}\n-\n-\t\t/* we can't end up being in (2) anymore, eat the argument */\n-\t\targv++;\n-\t\targc--;\n-\n-\t\tnew.name = arg;\n-\t\tsetup_branch_path(&new);\n-\n-\t\tif (check_ref_format(new.path) == CHECK_REF_FORMAT_OK &&\n-\t\t    resolve_ref(new.path, branch_rev, 1, NULL))\n-\t\t\thashcpy(rev, branch_rev);\n-\t\telse\n-\t\t\tnew.path = NULL; /* not an existing branch */\n-\n-\t\tif (!(new.commit = lookup_commit_reference_gently(rev, 1))) {\n-\t\t\t/* not a commit */\n-\t\t\tsource_tree = parse_tree_indirect(rev);\n-\t\t} else {\n-\t\t\tparse_commit(new.commit);\n-\t\t\tsource_tree = new.commit->tree;\n-\t\t}\n-\n-\t\tif (!source_tree)                   /* case (1): want a tree */\n-\t\t\tdie(\"reference is not a tree: %s\", arg);\n-\t\tif (!has_dash_dash) {/* case (3 -> 1) */\n-\t\t\t/*\n-\t\t\t * Do not complain the most common case\n-\t\t\t *\tgit checkout branch\n-\t\t\t * even if there happen to be a file called 'branch';\n-\t\t\t * it would be extremely annoying.\n-\t\t\t */\n-\t\t\tif (argc)\n-\t\t\t\tverify_non_filename(NULL, arg);\n-\t\t}\n-\t\telse {\n-\t\t\targv++;\n-\t\t\targc--;\n-\t\t}\n+\t\tint dwim_ok =\n+\t\t\t!patch_mode &&\n+\t\t\tdwim_new_local_branch &&\n+\t\t\topts.track == BRANCH_TRACK_UNSPECIFIED &&\n+\t\t\t!opts.new_branch;\n+\t\tint n = parse_branchname_arg(argc, argv, dwim_ok,\n+\t\t\t\t&new, &source_tree, rev, &opts.new_branch);\n+\t\targv += n;\n+\t\targc -= n;\n \t}\n \n-no_reference:\n-\n \tif (opts.track == BRANCH_TRACK_UNSPECIFIED)\n \t\topts.track = git_branch_track;\n \n-- \n1.7.4\n"},{"id":"160691","messageId":"20110208103249.GC29660@elie","threadId":"26433","inReplyTo":"20110208102605.GA29660@elie","subject":"[PATCH 2/3] checkout: introduce --detach synonym for \"git checkout foo^{commit}\"","fromName":"Jonathan Nieder","fromEmail":"jrnieder@gmail.com","sentAt":"2011-02-08T10:32:49Z","receivedAt":"2011-02-08T10:32:49Z","isPatch":true,"sender":{"key":"jrnieder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/281595?v=4"},"body":"From: Junio C Hamano <gitster@pobox.com>\n\nFor example, one might use this when making a temporary merge to\ntest that two topics work well together.\n\nPatch by Junio, with tests from Jeff King.\n\n[jn: with some extra checks for bogus commandline usage]\n\nSuggested-by: Jeff King <peff@peff.net>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\nSigned-off-by: Jonathan Nieder <jrnieder@gmail.com>\n---\n Documentation/git-checkout.txt |   13 +++++-\n builtin/checkout.c             |   25 ++++++++--\n t/t2020-checkout-detach.sh     |   95 ++++++++++++++++++++++++++++++++++++++++\n 3 files changed, 126 insertions(+), 7 deletions(-)\n create mode 100755 t/t2020-checkout-detach.sh\n\ndiff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt\nindex 22d3611..d162117 100644\n--- a/Documentation/git-checkout.txt\n+++ b/Documentation/git-checkout.txt\n@@ -9,6 +9,7 @@ SYNOPSIS\n --------\n [verse]\n 'git checkout' [-q] [-f] [-m] [<branch>]\n+'git checkout' [-q] [-f] [-m] [--detach] [<commit>]\n 'git checkout' [-q] [-f] [-m] [[-b|-B|--orphan] <new_branch>] [<start_point>]\n 'git checkout' [-f|--ours|--theirs|-m|--conflict=<style>] [<tree-ish>] [--] <paths>...\n 'git checkout' --patch [<tree-ish>] [--] [<paths>...]\n@@ -22,9 +23,10 @@ branch.\n \n 'git checkout' [<branch>]::\n 'git checkout' -b|-B <new_branch> [<start point>]::\n+'git checkout' [--detach] [<commit>]::\n \n \tThis form switches branches by updating the index, working\n-\ttree, and HEAD to reflect the specified branch.\n+\ttree, and HEAD to reflect the specified branch or commit.\n +\n If `-b` is given, a new branch is created as if linkgit:git-branch[1]\n were called and then checked out; in this case you can\n@@ -115,6 +117,13 @@ explicitly give a name with '-b' in such a case.\n \tCreate the new branch's reflog; see linkgit:git-branch[1] for\n \tdetails.\n \n+--detach::\n+\tRather than checking out a branch to work on it, check out a\n+\tcommit for inspection and discardable experiments.\n+\tThis is the default behavior of \"git checkout <commit>\" when\n+\t<commit> is not a branch name.  See the \"DETACHED HEAD\" section\n+\tbelow for details.\n+\n --orphan::\n \tCreate a new 'orphan' branch, named <new_branch>, started from\n \t<start_point> and switch to it.  The first commit made on this\n@@ -204,7 +213,7 @@ leave out at most one of `A` and `B`, in which case it defaults to `HEAD`.\n \n \n \n-Detached HEAD\n+DETACHED HEAD\n -------------\n \n It is sometimes useful to be able to 'checkout' a commit that is\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex 0e7a6a3..51ec977 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -30,6 +30,7 @@ struct checkout_opts {\n \tint quiet;\n \tint merge;\n \tint force;\n+\tint force_detach;\n \tint writeout_stage;\n \tint writeout_error;\n \n@@ -563,7 +564,7 @@ static void update_refs_for_switch(struct checkout_opts *opts,\n \t\t\tif (!file_exists(ref_file) && file_exists(log_file))\n \t\t\t\tremove_path(log_file);\n \t\t}\n-\t} else if (strcmp(new->name, \"HEAD\")) {\n+\t} else if (opts->force_detach || strcmp(new->name, \"HEAD\")) {\n \t\tupdate_ref(msg.buf, \"HEAD\", new->commit->object.sha1, NULL,\n \t\t\t   REF_NODEREF, DIE_ON_ERR);\n \t\tif (!opts->quiet) {\n@@ -574,7 +575,8 @@ static void update_refs_for_switch(struct checkout_opts *opts,\n \t}\n \tremove_branch_state();\n \tstrbuf_release(&msg);\n-\tif (!opts->quiet && (new->path || !strcmp(new->name, \"HEAD\")))\n+\tif (!opts->quiet &&\n+\t    (new->path || (!opts->force_detach && !strcmp(new->name, \"HEAD\"))))\n \t\treport_tracking(new);\n }\n \n@@ -677,6 +679,7 @@ static const char *unique_tracking_name(const char *name)\n \n static int parse_branchname_arg(int argc, const char **argv,\n \t\t\t\tint dwim_new_local_branch_ok,\n+\t\t\t\tint force_detach,\n \t\t\t\tstruct branch_info *new,\n \t\t\t\tstruct tree **source_tree,\n \t\t\t\tunsigned char rev[20],\n@@ -753,7 +756,8 @@ static int parse_branchname_arg(int argc, const char **argv,\n \tnew->name = arg;\n \tsetup_branch_path(new);\n \n-\tif (check_ref_format(new->path) == CHECK_REF_FORMAT_OK &&\n+\tif (!force_detach &&\n+\t    check_ref_format(new->path) == CHECK_REF_FORMAT_OK &&\n \t    resolve_ref(new->path, branch_rev, 1, NULL))\n \t\thashcpy(rev, branch_rev);\n \telse\n@@ -804,6 +808,7 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)\n \t\tOPT_STRING('B', NULL, &opts.new_branch_force, \"branch\",\n \t\t\t   \"create/reset and checkout a branch\"),\n \t\tOPT_BOOLEAN('l', NULL, &opts.new_branch_log, \"create reflog for new branch\"),\n+\t\tOPT_BOOLEAN(0, \"detach\", &opts.force_detach, \"detach the HEAD at named commit\"),\n \t\tOPT_SET_INT('t', \"track\",  &opts.track, \"set upstream info for new branch\",\n \t\t\tBRANCH_TRACK_EXPLICIT),\n \t\tOPT_STRING(0, \"orphan\", &opts.new_orphan_branch, \"new branch\", \"new unparented branch\"),\n@@ -842,9 +847,15 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)\n \t\topts.new_branch = opts.new_branch_force;\n \n \tif (patch_mode && (opts.track > 0 || opts.new_branch\n-\t\t\t   || opts.new_branch_log || opts.merge || opts.force))\n+\t\t\t   || opts.new_branch_log || opts.merge || opts.force\n+\t\t\t   || opts.force_detach))\n \t\tdie (\"--patch is incompatible with all other options\");\n \n+\tif (opts.force_detach && (opts.new_branch || opts.new_orphan_branch))\n+\t\tdie(\"--detach cannot be used with -b/-B/--orphan\");\n+\tif (opts.force_detach && 0 < opts.track)\n+\t\tdie(\"--detach cannot be used with -t\");\n+\n \t/* --track without -b should DWIM */\n \tif (0 < opts.track && !opts.new_branch) {\n \t\tconst char *argv0 = argv[0];\n@@ -895,7 +906,8 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)\n \t\t\tdwim_new_local_branch &&\n \t\t\topts.track == BRANCH_TRACK_UNSPECIFIED &&\n \t\t\t!opts.new_branch;\n-\t\tint n = parse_branchname_arg(argc, argv, dwim_ok,\n+\t\tint n = parse_branchname_arg(argc, argv,\n+\t\t\t\tdwim_ok, opts.force_detach,\n \t\t\t\t&new, &source_tree, rev, &opts.new_branch);\n \t\targv += n;\n \t\targc -= n;\n@@ -922,6 +934,9 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)\n \t\t\t}\n \t\t}\n \n+\t\tif (opts.force_detach)\n+\t\t\tdie(\"git checkout: --detach does not take a path argument\");\n+\n \t\tif (1 < !!opts.writeout_stage + !!opts.force + !!opts.merge)\n \t\t\tdie(\"git checkout: --ours/--theirs, --force and --merge are incompatible when\\nchecking out of the index.\");\n \ndiff --git a/t/t2020-checkout-detach.sh b/t/t2020-checkout-detach.sh\nnew file mode 100755\nindex 0000000..0042145\n--- /dev/null\n+++ b/t/t2020-checkout-detach.sh\n@@ -0,0 +1,95 @@\n+#!/bin/sh\n+\n+test_description='checkout into detached HEAD state'\n+. ./test-lib.sh\n+\n+check_detached () {\n+\ttest_must_fail git symbolic-ref -q HEAD >/dev/null\n+}\n+\n+check_not_detached () {\n+\tgit symbolic-ref -q HEAD >/dev/null\n+}\n+\n+reset () {\n+\tgit checkout master &&\n+\tcheck_not_detached\n+}\n+\n+test_expect_success 'setup' '\n+\ttest_commit one &&\n+\ttest_commit two &&\n+\tgit branch branch &&\n+\tgit tag tag\n+'\n+\n+test_expect_success 'checkout branch does not detach' '\n+\treset &&\n+\tgit checkout branch &&\n+\tcheck_not_detached\n+'\n+\n+test_expect_success 'checkout tag detaches' '\n+\treset &&\n+\tgit checkout tag &&\n+\tcheck_detached\n+'\n+\n+test_expect_success 'checkout branch by full name detaches' '\n+\treset &&\n+\tgit checkout refs/heads/branch &&\n+\tcheck_detached\n+'\n+\n+test_expect_success 'checkout non-ref detaches' '\n+\treset &&\n+\tgit checkout branch^ &&\n+\tcheck_detached\n+'\n+\n+test_expect_success 'checkout ref^0 detaches' '\n+\treset &&\n+\tgit checkout branch^0 &&\n+\tcheck_detached\n+'\n+\n+test_expect_success 'checkout --detach detaches' '\n+\treset &&\n+\tgit checkout --detach branch &&\n+\tcheck_detached\n+'\n+\n+test_expect_success 'checkout --detach without branch name' '\n+\treset &&\n+\tgit checkout --detach &&\n+\tcheck_detached\n+'\n+\n+test_expect_success 'checkout --detach errors out for non-commit' '\n+\treset &&\n+\ttest_must_fail git checkout --detach one^{tree} &&\n+\tcheck_not_detached\n+'\n+\n+test_expect_success 'checkout --detach errors out for extra argument' '\n+\treset &&\n+\tgit checkout master &&\n+\ttest_must_fail git checkout --detach tag one.t &&\n+\tcheck_not_detached\n+'\n+\n+test_expect_success 'checkout --detached and -b are incompatible' '\n+\treset &&\n+\ttest_must_fail git checkout --detach -b newbranch tag &&\n+\tcheck_not_detached\n+'\n+\n+test_expect_success 'checkout --detach moves HEAD' '\n+\treset &&\n+\tgit checkout one &&\n+\tgit checkout --detach two &&\n+\tgit diff --exit-code HEAD &&\n+\tgit diff --exit-code two\n+'\n+\n+test_done\n-- \n1.7.4\n"},{"id":"160692","messageId":"20110208103434.GD29660@elie","threadId":"26433","inReplyTo":"20110208102605.GA29660@elie","subject":"[PATCH 3/3] checkout: rearrange update_refs_for_switch for clarity","fromName":"Jonathan Nieder","fromEmail":"jrnieder@gmail.com","sentAt":"2011-02-08T10:34:34Z","receivedAt":"2011-02-08T10:34:34Z","isPatch":true,"sender":{"key":"jrnieder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/281595?v=4"},"body":"Take care of simple, exceptional cases before the meat of the \"check\nout by branch name\" code begins.  After this change, the function\nvaguely follows the following pseudocode:\n\n\tif (-B or -b)\n\t\tcreate branch;\n\tif (plain \"git checkout\" or \"git checkout HEAD\")\n\t\t;\n\telse if (--detach or checking out by non-branch commit name)\n\t\tdetach HEAD;\n\telse if (checking out by branch name)\n\t\tattach HEAD;\n\nOne nice side benefit is to make it possible to remove handling of\nthe --detach option from outside switch_branches.\n\nSigned-off-by: Jonathan Nieder <jrnieder@gmail.com>\n---\nThat's the end of the series.  Thoughts welcome, as always.\n\n'night,\nJonathan\n\n builtin/checkout.c |   27 +++++++++++++--------------\n 1 files changed, 13 insertions(+), 14 deletions(-)\n\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex 51ec977..179d047 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -542,7 +542,17 @@ static void update_refs_for_switch(struct checkout_opts *opts,\n \tstrbuf_addf(&msg, \"checkout: moving from %s to %s\",\n \t\t    old_desc ? old_desc : \"(invalid)\", new->name);\n \n-\tif (new->path) {\n+\tif (!strcmp(new->name, \"HEAD\") && !new->path && !opts->force_detach) {\n+\t\t/* Nothing to do. */\n+\t} else if (opts->force_detach || !new->path) {\t/* No longer on any branch. */\n+\t\tupdate_ref(msg.buf, \"HEAD\", new->commit->object.sha1, NULL,\n+\t\t\t   REF_NODEREF, DIE_ON_ERR);\n+\t\tif (!opts->quiet) {\n+\t\t\tif (old->path && advice_detached_head)\n+\t\t\t\tdetach_advice(old->path, new->name);\n+\t\t\tdescribe_detached_head(\"HEAD is now at\", new->commit);\n+\t\t}\n+\t} else if (new->path) {\t/* Switch branches. */\n \t\tcreate_symref(\"HEAD\", new->path, msg.buf);\n \t\tif (!opts->quiet) {\n \t\t\tif (old->path && !strcmp(new->path, old->path))\n@@ -564,14 +574,6 @@ static void update_refs_for_switch(struct checkout_opts *opts,\n \t\t\tif (!file_exists(ref_file) && file_exists(log_file))\n \t\t\t\tremove_path(log_file);\n \t\t}\n-\t} else if (opts->force_detach || strcmp(new->name, \"HEAD\")) {\n-\t\tupdate_ref(msg.buf, \"HEAD\", new->commit->object.sha1, NULL,\n-\t\t\t   REF_NODEREF, DIE_ON_ERR);\n-\t\tif (!opts->quiet) {\n-\t\t\tif (old->path && advice_detached_head)\n-\t\t\t\tdetach_advice(old->path, new->name);\n-\t\t\tdescribe_detached_head(\"HEAD is now at\", new->commit);\n-\t\t}\n \t}\n \tremove_branch_state();\n \tstrbuf_release(&msg);\n@@ -679,7 +681,6 @@ static const char *unique_tracking_name(const char *name)\n \n static int parse_branchname_arg(int argc, const char **argv,\n \t\t\t\tint dwim_new_local_branch_ok,\n-\t\t\t\tint force_detach,\n \t\t\t\tstruct branch_info *new,\n \t\t\t\tstruct tree **source_tree,\n \t\t\t\tunsigned char rev[20],\n@@ -756,8 +757,7 @@ static int parse_branchname_arg(int argc, const char **argv,\n \tnew->name = arg;\n \tsetup_branch_path(new);\n \n-\tif (!force_detach &&\n-\t    check_ref_format(new->path) == CHECK_REF_FORMAT_OK &&\n+\tif (check_ref_format(new->path) == CHECK_REF_FORMAT_OK &&\n \t    resolve_ref(new->path, branch_rev, 1, NULL))\n \t\thashcpy(rev, branch_rev);\n \telse\n@@ -906,8 +906,7 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)\n \t\t\tdwim_new_local_branch &&\n \t\t\topts.track == BRANCH_TRACK_UNSPECIFIED &&\n \t\t\t!opts.new_branch;\n-\t\tint n = parse_branchname_arg(argc, argv,\n-\t\t\t\tdwim_ok, opts.force_detach,\n+\t\tint n = parse_branchname_arg(argc, argv, dwim_ok,\n \t\t\t\t&new, &source_tree, rev, &opts.new_branch);\n \t\targv += n;\n \t\targc -= n;\n-- \n1.7.4\n"}]}