nowox nowox - 8 days ago 6
Git Question

How to create a feature branch and reset to origin/master in a single operation?

I often start my journey with a small fix on the master branch that eventually appear more complex than initially thought.

So I easily fall in this situation:

* Damned I have more stuff to check (HEAD -> master)
* Forgot to fix this as well...
* Oops, fixed that too
* Fixed this
* (origin/master)


And at that point somebody ask me to quickly do a small change on the master branch. So I need to create a
feature
/
bugfix
branch and revert
master
to its
origin
:

$ git branch --magic bug/foobar
$ git log
* Damned I have more stuff to check (HEAD -> bug/foobar)
* Forgot to fix this as well...
* Oops, fixed that too
* Fixed this
* (master, origin/master)


Currently I solve my issue with this:

$ git branch bug/foobar
$ git reset --hard origin/master
$ git checkout bug/foobar


I can obviously create an alias for it (not tested yet):

swap-to="!git branch $@ && \
git reset --hard \
\$(git config --get branch.\$(git rev-parse --abbrev-ref HEAD).remote)/\
\$(git rev-parse --abbrev-ref HEAD) && \
git checkout $@"


Is there a quicker/smarter way to do this?

Answer

Restating the problem

Here, you are starting out on master, but we could pick any starting (local) branch B that has some upstream U set. (In your particular case the upstream is origin/master.) You started on a task that you thought was quick and easy, and now you have realized that it is not so quick and easy after all, so you would like to "spin it off" to its own branch.

We may observe, then, that what we want is to save the name of the current branch, then change Git's notion of "the current branch" to a newly created branch, pointing to the current commit, then adjust the saved branch to point to the saved branch's upstream.

Background

Branch names are simply labels pointing to some commit. Git is fairly sensible here: git branch lets you create a new label pointing to any existing commit, or move anything except the current branch to any existing commit. The default is to create the new branch pointing to the current commit.

The tricky part is that the current branch cannot be moved this way, and the reason for that is that the current branch must match the index and work-tree except for whatever active changes you are making right now. There is only one user-facing ("porcelain") command for changing Git's notion of "the current branch", which is git checkout.

Fortunately, git checkout also has the -b (create new branch) flag, which operates much like git branch: it defaults to creating a new branch at the current commit. Furthermore, it avoids touching the index and work-tree as much as possible—just like any other git checkout—so if we create a new branch at the current commit, it never has to touch the index or work-tree at all. The result is that it always succeeds, and leaves us on the newly created branch. (It can create a new branch pointing to a particular commit, but that can also fail—but we don't need that feature here, so we get to use the mode in which it doesn't fail. Well, "doesn't fail" as long as the new branch name really is new, at least.)

Hence, the solution

The solution still needs to be a few lines of script, but we can write this as a small script named git-spinoff and put that in our $PATH (I would use $HOME/scripts/git-spinoff for this). We could even do this as a shell alias function, but I find scripts to be better in general (easier to understand, debug, etc.).

To make this script reliable, let's actually check our required condition: that we're on some branch (so HEAD is not "detached") and that this branch has an upstream set. Then we can create our new branch and re-point—using git branch -f, i.e., without using git reset—the other branch:

#! /bin/sh
#
# git-spinoff: spin the current branch off to a new
# branch.  When this succeeds, we are on the new branch.

die() {
    echo "fatal: $@" 1>&2
    exit 1
}

# check arguments
case $# in
1) newbranch="$1";;
*) echo "usage: git spinoff <newbranch>" 1>&2; exit 1;;
esac

# make sure we are on a branch that has an upstream
branch=$(git symbolic-ref -q --short HEAD) ||
    die "existing branch is detached; there's nothing to restore"
upstream=$(git rev-parse -q --verify @{u}) ||
    die "existing branch $branch has no upstream: there's nowhere to restore-to"

# now create and check out out the new branch, or quit if we can't
git checkout -b "$newbranch" || exit $?

# last, re-adjust the previous branch (which is no longer the current
# branch since we are on the new one we created) to point to its own
# upstream (if this fails, ignore the failure!)
git branch -f "$branch" "$upstream"

That last command can actually be improved, as there is a "plumbing command" that does what we want. To use it, we must retain the full (refs/heads/ style) name of the original branch, and choose a message. This particular message might be improve-able, hence is just a sample:

fullbranch=$(git symbolic-ref -q HEAD) || die ...
branch=${fullbranch#refs/heads/}
... same as before ...
git update-ref -m \
    "git spinoff: re-point $branch to ${branch}@{upstream}" \
    $fullbranch $upstream

With this git-spinoff script in our path, we can now run git spinoff.

Edit: this is now tested, and is included in https://github.com/chris3torek/scripts (as https://github.com/chris3torek/scripts/blob/master/git-spinoff).

Comments