Home > OS >  How do I save the current HEAD so I can check it back out in the same way later?
How do I save the current HEAD so I can check it back out in the same way later?

Time:11-12

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 returns HEAD if detached, which is no good for getting back what was detached.
  • git rev-parse --symbolic-full-name HEAD: returns refs/heads/<branch> which gets interpreted by git checkout as an instruction to detach rather than check out the branch. Also, on orphan branches (i.e. branches with no commits yet) fails with fatal: ambiguous argument 'HEAD': unknown revision or path not in the working tree.
  • git symbolic-ref HEAD: Same problem with refs/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 with refs/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} plus git 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
}
  • Related