But is there a generic way to create the branch baz from foo and have it track the same remote as foo regardless of what that remote happens to be?
Yes, but it's a little complicated. Git being what it is, there are about twelve dozen1 different ways to do this, but we'll just pick one here.
First, let's look at all the important parts of this:
There are your own local branch names, foo and baz in your example.
There are remote-tracking names such as origin/bar (full name refs/remotes/origin/bar).
There's the commit hash ID to which each branch name or remote-tracking name points.
And, there's an optional upstream setting for any (local) branch name. Typically the upstream of some local branch is a remote-tracking name (Git calls this a "remote-tracking branch") using the same base name but prefixed with a remote name. For instance, master typically has origin/master set as its upstream. However, as in your example, the upstream of a branch need not match: the upstream of foo can be origin/bar.
(It's also possible to set a local branch as the upstream of another local branch; this behaves the way you would expect. Internally this is implemented by setting the remote half of the two-part upstream setting to ., which means your own repository. But we get to ignore these details here.)
As fphillipe mentioned, we have git branch --set-upstream-to, which can set the upstream of any branch, at any time. If there is already some upstream set, this replaces it. If there was no upstream set, now there is.
We also have two different ways to create a new branch name:
git branch name [start-point] creates the new (local) branch named name. The initial hash ID stored in this name is given by start-point; if you omit start-point, the initial hash ID is whatever git rev-parse HEAD produces.
git checkout -b name [start-point] creates the new (local) branch named name and then does a git checkout of that name, all in one. It's essentially equivalent to running git branch followed by git checkout.
1"Ew, gross" 😀
The goal
In this case, you want to create a local branch named baz, set its hash to match that of foo, and set its upstream to foo's upstream, which is origin/bar. That is, after creating baz you would like:
git rev-parse baz
to produce the same output as:
git rev-parse foo
—that means the same commit is the tip commit of each branch—but you want:
git rev-parse --symbolic-full-name baz@{upstream}
to produce refs/remotes/origin/bar.
Both of the creation commands can, but don't always, also set the upstream of the newly created branch. You can use the --track argument to force them to set it, and you can use the --no-track argument to force them not to set it. The --track flag takes the remote-tracking name to use, so if you first figure out that the upstream of foo is origin/bar, you can write:
git checkout -b baz origin/bar
(or the same with git branch if you don't want to also check out the new branch). But this has two bugs:
- It requires that you manually find the upstream of
foo. You want to automate this. - Perhaps more important, it creates
baz pointing to the same commit as origin/bar. If foo points to a different commit, that violates one of the desires.
Hence the job must be done in several parts. First, you will create the branch (and maybe switch to it if you like), using the local name foo to set the starting-point. Both git branch and git checkout will not set the upstream of the new branch in this case, at least by default, but you can be totally explicit by using --no-track, or you can turn foo into a raw hash ID. Usually all of this is unnecessary,2 but if you wanted to go for raw hash ID, here's a shell fragment to do it:
name=baz start=foo hash=$(git rev-parse ${start}^{commit}) || exit
If git rev-parse fails to parse foo to a raw hash ID, or foo does not identify a commit, git rev-parse will produce an error message to stderr and exit nonzero, and the || exit will have your shell fragment quit at this point.
Then:
git branch $name $hash || exit
will try to create the branch name $name, pointing to the desired hash.
We also need to find the upstream name, for which git rev-parse is again the command to use:
upstream=$(git rev-parse --symbolic-full-name ${start}@{upstream}) || exit
As before, we have our shell fragment quit if git rev-parse has failed, letting git rev-parse print the appropriate error. Now that we have the upstream, we can set it using git branch --set-upstream-to:
git branch --set-upstream-to $name $upstream
2At the command line, you just run a command and observe: if it did what you want, fine, if not, you correct as you go. From a script, however, it's often hard to see what did or might happen. It tends to be best to use something that has as few side-effects as possible. So we break down each Git command into direct plumbing commands when possible. I don't carry this to its extreme, in this example, as that gets too annoying, but I will show the git rev-parse operations.
The code
Putting the pieces together in the right order, plus a little bit of argument checking and use of -e to avoid all the || exits, gets us a full (but completely untested) shell script:
#! /bin/sh -e usage() { echo "usage: $0 branch-name start-point" } case "$#" in 2) ;; *) usage 1>&2; exit 1;; esac name="$1" start="$2" hash=$(git rev-parse ${start}^{commit}) upstream=$(git rev-parse --symbolic-full-name ${start}@{upstream}) git branch $name $hash git branch --set-upstream-to $name $upstream
After testing this, write it as an executable shell script somewhere in your $PATH, calling it (e.g.) git-branch-with-upstream, and then:
git branch-with-upstream baz foo
will invoke the script, which will create baz pointing to the same commit as foo and having foo's upstream as baz's upstream.
You now know how to write new Git commands!