git: reliably switching to a detached HEAD and the

2019-06-23 18:32发布

问题:

So here's the scenario. I've got a script that runs some tests. I need to make another script that accepts as a parameter a git commit name and then does the following:

  1. Saves the current commit state - branch name or unnamed commit.
  2. Switches to a detached HEAD at the specified commit
  3. Runs the test script against that commit
  4. Switches back so HEAD is the same as it was before this business

I need to make sure this script is robust so that it's never destructive no matter the state of the repository. It should work when it's run from a detached HEAD or from a regular branch, and preferably it should work even when there are uncommitted or unstaged changes around.

I feel like this should be an easy question to answer, since running a test script against a previous commit seems like a really common task to want to automate. But I can't seem to find any simple series of commands to do it.

(Similar to what pushd / cd / popd do for current working directory).

回答1:

If it's in a script, for only this one use case, you don't need to do anything super-fancy, just store where HEAD was before, and check it out again after:

# If HEAD is a sym-ref, the first assignment will work
# otherwise, it's detached, so get the SHA1 with rev-parse
if ! head=$(git symbolic-ref HEAD 2>&1); then
    head=$(git rev-parse HEAD)
fi
# trim a refs/heads/ prefix; no-op otherwise
head=${head#refs/heads/}


# now go on and do your stuff, test, whatever you like

# then return to where you were
# This will ERASE ANY LOCAL CHANGES.
git checkout -f $head

This has the advantage of working no matter what you do in the middle - in particular you could do a lot of git operations there - perhaps a test merge, or cherry-pick a commit for testing (maybe testing that commit, maybe it contains some build configuration settings purely for testing). Since those operations create commits, they'd cause the HEAD@{1} approach to fail (you'd want HEAD@{2} instead). Even better, if your testing actually involves creating temporary branches, this will still work, while the @{-1} approach would not.

(Plus, as far as I can tell, HEAD@{1} always checks out the commit referred to by HEAD at that point, not the branch that was then pointing to that commit. That kind of makes sense, as the branch could've conceivably changed since then.)



回答2:

try (not tested)

git checkout HEAD@{1}

to "switches back so HEAD" as it was before your checkout xxx.

See also "HEAD and ORIG_HEAD in Git"


All the other revision specification are here: rev_parse, "SPECIFYING REVISIONS" section.
For instance, to get back to the previous branch, you can try @{-1}.

Just tested it (the "previous HEAD" option):

Simple git repo with 3 files added in three commits (a, then b, then c):

C:\git\tests\p3>git log --oneline
6e5b961 c
66c68e3 b
77e9a40 a

I checkout the first commit (DETACHED HEAD)

C:\git\tests\p3>git checkout 77e9a40
Note: moving to '77e9a40' which isn't a local branch
If you want to create a new branch from this checkout, you may do so
(now or later) by using -b with the checkout command again. Example:
  git checkout -b <new_branch_name>
HEAD is now at 77e9a40... a

C:git\tests\p3>dir
08/12/2010  12:27 PM                 4 a.txt

I try to get back to the previous HEAD before making the DETACHED HEAD:

C:\git\tests\p3>git checkout HEAD@{1}
Previous HEAD position was 77e9a40... a
HEAD is now at 6e5b961... c

It works!

You get back the right commit, but not the right branch (i.e. you are still in a detached mode)

C:\git\tests\p3>git branch
* (no branch)
  master

In this configuration, trying to get back to the previous branch wouldn't work

C:\git\tests\p3>git checkout HEAD@{-1}
error: pathspec 'HEAD@{-1}' did not match any file(s) known to git.

So the only real solution to get back to the HEAD (not to a commit in a detached mode) is to memorize it first

git symbolic-ref HEA

See Jefromi's answer.