I want to do:
current_state="$(git something-or-another)"
and then, later:
git checkout "$current_state"
and get back to whatever situation I had originally, whether it was a branch, detached head, or whatever. (I don't need to worry about uncommitted changes; the working directory will be clean during both halves of the operation.) I'm also open to other commands besides checkout
to achieve this, but I would strongly prefer something that can be saved in a string, or failing that, a commit or ref.
Things I've tried so far:
git rev-parse --abbrev-ref HEAD
: works when a branch is checked out, but returnsHEAD
if detached, which is no good for getting back what was detached.git rev-parse --symbolic-full-name HEAD
: returnsrefs/heads/<branch>
which gets interpreted bygit checkout
as an instruction to detach rather than check out the branch. Also, on orphan branches (i.e. branches with no commits yet) fails withfatal: ambiguous argument 'HEAD': unknown revision or path not in the working tree.
git symbolic-ref HEAD
: Same problem withrefs/heads/
; also, fails with an error if head is detached.git symbolic-ref -q HEAD || git rev-parse HEAD
: Works when detached, but still has same problem as above withrefs/heads/
causing detachment.(git symbolic-ref -q HEAD || git rev-parse HEAD) | sed 's%^refs/heads/%%'
: this is the closest I've come, and it seems to work, but it feels incredibly hacky.cp "$(git rev-parse --git-dir)"/HEAD{,.bak}
plusgit checkout -- .
: this also seems to work, but if anything seems even hackier than the previous option.
Am I missing something? How can I easily string-ify and restore HEAD, in all of its possible states?
CodePudding user response:
To get the current branch name, use git symbolic-ref --short HEAD
. If this produces an error, you are in detached HEAD mode; remember that fact and run git rev-parse HEAD
to get the hash ID.
To return to where you were:
if $was_on_branch; then
git switch $branch_name
else
git switch --detach $hash
fi
where $hash
is the saved hash ID, $branch_name
is the saved branch name, and $was_on_branch
is the status flag. (Use git checkout
with the same arguments if you have an older Git that lacks git switch
.)
There is one mode this does not cover: if you're on an unborn branch, the symbolic ref lookup will succeed but the branch does not yet exist so the attempt to switch back will fail. This particular case probably should not be handled in general, but if you want to handle it, note that this is the only case where git symbolic-ref
succeeds, yet git rev-parse HEAD
fails.
In general, if you're considering doing this, you might also consider using git worktree add
instead.
CodePudding user response:
With the help of @torek's excellent answer explaining all of the different cases, I've produced the following bash functions which can serialize the state of HEAD to a string and then apply it later.
I tend to agree with them that this is probably something you should avoid doing if possible, but if you can't then this should get the job done:
# Serialize HEAD into a string that can be stored, passed around, etc.
# and later used with the below function to get back that state.
git_serialize_head() {
if branch="$(git symbolic-ref --quiet --short HEAD)"; then
if git rev-parse --quiet --verify HEAD > /dev/null; then
# On a branch
echo "branch $branch"
else
# On an orphaned/unborn branch (have name, but no commits)
echo "orphan $branch"
fi
else
# Detached head
echo "detach $(git rev-parse HEAD)"
fi
}
# Take a string from the function above and apply it to the git repository.
git_checkout_serialized_head() {
type="${1%% *}" # Remove everything from first space to end of string
value="${1#* }" # Remove everything from start of string to first space
if [[ "$type" == "branch" ]]; then
git checkout "$value"
elif [[ "$type" == "orphan" ]]; then
# Note - though the branch was unborn when we serialized it, it may not
# be any longer. Since `checkout --orphan` will complain if the branch
# already exists, in this scenario check out that branch instead.
#
# We still need to know if it was an orphan branch at serialization time,
# though, because if it *wasn't* that means it was deleted in between
# serialization and restoration, and the user probably doesn't want it to
# come back as a new root in their repo. (The unqualified `git checkout`
# in the type==branch case above will produce an error for us in this
# situation.)
if git rev-parse --quiet --verify "$value" > /dev/null; then
# Not an orphan any more, check out normally
git checkout "$value"
else
# Still an orphan, check out as such
git checkout --orphan "$value"
fi
elif [[ "$type" == "detach" ]]; then
git checkout --detach "$value"
else
echo "Unknown checkout type in serialized HEAD: $type" >&2
exit 1
fi
}