Adamarla
Adamarla

Reputation: 812

GIT checkout unambiguous ref

I have a branch and a tag both named test. If I have an unambiguous ref like heads/test or refs/heads/test, how can I checkout without going into detached state?

git checkout heads/test

I want to save the current HEAD state and later return to it:

# Exit if dirty

# Save HEAD
ref=$(git symbolic-ref -q --short HEAD)
if [ -z "$ref" ] ; then # Detached
    ref=$(git rev-parse HEAD)
fi
echo $ref

# Do Stuff that changes HEAD
git checkout master

# Restore HEAD
git checkout $ref

::Edited for clarity::

Upvotes: 1

Views: 1187

Answers (3)

torek
torek

Reputation: 489748

Edit: Adamarla notes that git symbolic-ref --short -q HEAD misbehaves in this ambiguous-reference situation. For instance:

$ mkdir t
$ cd t
$ git init
Initialized empty Git repository in ...
$ echo test symref > README
$ git add README 
$ git commit -m initial
[master (root-commit) ee3448a] initial
 1 file changed, 1 insertion(+)
 create mode 100644 README
$ git branch test
$ git tag test
$ git checkout test
warning: refname 'test' is ambiguous.
Switched to branch 'test'
$ git symbolic-ref -q --short HEAD
heads/test

so my original answer below has an important error. I'd use a slight variant on the OP's alternative:

if headRef=$(git symbolic-ref -q HEAD); then
    headRef=${headRef#refs/heads/}
else
    headRef=$(git rev-parse HEAD)
fi

Note that:

  • git symbolic-ref errors (returns a failed status, as well as not printing the symbolic reference) when HEAD is not a symbolic reference:

    if ! ref=$(git symbolic-ref -q --short HEAD); then
        ... HEAD is detached ...
    

    (In other words, you do not need a separate zero-length string test.)

  • HEAD can only be a symbolic reference to a branch name, not a tag name.

  • Most Git commands behave as described in the gitrevisions documentation, in that the bare name test will resolve to the commit pointed-to by refs/tags/test rather than refs/heads/test. The git checkout command, however, behaves differently: git checkout test will prefer the branch.

Thus, if the git symbolic-ref succeeds, your short reference is suitable to pass to git checkout. If it fails, you do need to run git rev-parse HEAD to get the hash ID to pass to the later git checkout.

Upvotes: 3

Adamarla
Adamarla

Reputation: 812

I was hoping for a plumbing checkout command that would handle branch checkout from a qualified ref, but have come up with this workaround:

# Save Checkout
refBranch=$(git symbolic-ref -q HEAD)
refDetach=$(git rev-parse HEAD)
echo "refBranch [$refBranch]"
echo "refDetach [$refDetach]"

# Do Stuff
git checkout master --quiet
echo "Now on [$(git symbolic-ref -q --short HEAD)]"

# Restore Checkout
if [[ ${refBranch:0:11} == "refs/heads/" ]] ; then
    echo "Checkout Branch [${refBranch:11}]"
    git checkout ${refBranch:11} --quiet
else # Detached
    echo "Checkout Detach [$refDetach]"
    git checkout $refDetach --quiet
fi

Or if we are guaranteed to always get the refs/heads/ prefix:

# Save Checkout
if headRef=$(git symbolic-ref -q HEAD); then
    headRef=${headRef:11}
else
    headRef=$(git rev-parse HEAD)
fi
echo "headRef [${headRef}]"

# Do Stuff
git checkout master --quiet
echo "Now on [$(git symbolic-ref -q --short HEAD)]"

# Restore Checkout
git checkout $headRef --quiet

Upvotes: 1

Mark Adelsberger
Mark Adelsberger

Reputation: 45819

From the docs:

Branch to checkout; if it refers to a branch (i.e., a name that, when prepended with "refs/heads/", is a valid ref), then that branch is checked out. Otherwise, if it refers to a valid commit, your HEAD becomes "detached" and you are no longer on any branch (see below for details).

So: If there exist both a branch and a tag with the name test, then git checkout test will checkout the branch without detaching. It is true that git gives a warning, but it still will consistently behave this way.

But on the other hand, if you give it anything other than a "branch name" as defined above - which includes explicitly qualifying the branch, as in heads/test - then it detaches.

So it seems if you have heads/test and want to check out without detaching, the (porcelain) procedure is to strip the heads/ off and just checkout test.

If you really don't want to do that... well, I don't know a "safe" alternative that's any better.

You could use git symbolic-ref

git symbolic-ref -m "Checkout test" HEAD refs/heads/test
git reset HEAD -- .
git checkout -- .

but notice you have to fully qualify the ref in this case - so you still can't just use heads/test. Also if you don't manually provide a "reason" in a -m argument, you'll muck up the reflog. And you have to then update the index and work tree. You could write a script or alias to do all that, I guess.

Or you could directly manipulate the .git/HEAD file, but if you mess it up git will no longer recognize the repo until you fix it.

Also, you said:

Checking out tags/test works fine.

Not sure what this means, since (a) your question was how to check out heads/test without entering detached head, but (b) checking out a tag always enters detached head. And:

I am getting the ref from:

git symbolic-ref -q --short HEAD

Under what circumstances would this return tags/test? Maybe I misunderstood because you put that on the same line where you were talking about checkout out the tag... do you mean that git symbolic-ref -q --short HEAD is returning heads/test, and you're storing that and later trying to check out to it?

In that case, see above; but also in that case, you could leave out the --short and at least that would be one step closer to making the symbolic-ref approach work...

Upvotes: 2

Related Questions