In your own answer you mentioned that:
git checkout -b "${new_branch}" "origin/dev"
means that the new branch will track origin/dev ...
This is correct, although it uses this rather badly overloaded word "track". In the last few years, it seems to me that the Git documentation has been moving (slowly) away from this word, which is probably a good idea (although it persists in the --track
and --no-track
options!).
The more-appropriate / better / more-modern term is that the new branch will have origin/dev
set as its upstream. Each branch name can have one upstream setting. This upstream is simply the name of a branch (e.g., master
) or of a remote-tracking name (e.g., origin/master
). The presence of this setting, along with its actual value, affects how git status
reports status, how git merge
and git rebase
behave when used with no additional arguments, and how git pull
and git push
behave with no additional arguments.1
Alternatively, a branch can have no upstream. If a branch has no upstream, git status
does not report a comparison of the branch to its nonexistent upstream, git merge
and git rebase
demand more arguments, and so on. Note that the upstream setting, or lack thereof, is independent of the commit hash to which the branch-name points.
(See also Make an existing Git branch track a remote branch?)
What I did to fix it, was using --no-track, like so:
git branch --no-track "${new_branch}" "remotes/origin/dev"
git checkout "${new_branch}"
This will do the job, but you can also do it with:
git checkout --no-track -b "${new_branch}" origin/dev
1This is not meant to be a comprehensive list. In particular git branch -vv
also looks at the upstream setting, and git for-each-ref
and git rev-parse
have the ability to extract the upstream setting of a branch. Moreover, parts of Git don't bother to verify whether the name to which the upstream is set, if it is set at all, is valid, but other parts of Git do; so there's quite a mishmash of possibilities here.
Background (with a lot of detail)
The precise default actions for both git branch
and git checkout
is a little bit complicated. Git is trying to be helpful, but the result is just plain messy.
I think it helps to remember that a branch name acts as a pointer to one particular commit. Git calls this the tip commit of the branch. You may choose any existing commit in the entire repository, and attach a branch name there. For instance, given a commit chain like this one:
...--E--F--G
\
H--I--J <-- master (HEAD)
(with an inexplicable-as-yet kink in the drawing), we can go find the actual hash ID of commit G
and attach a new branch name there now. Let's say G
's actual hash ID starts with 491ab94
, so we run:
git branch marker 491ab94
The result looks like this:
...--E--F--G <-- marker
\
H--I--J <-- master (HEAD)
Now there are two branches where there used to be just one. The new branch, named marker
, identifies commit G
. The existing master
is unchanged: it continues to identify commit J
.
Creating a new branch requires picking a commit
Whenever you create a new branch name, you must answer a question for Git: Which existing commit should this branch name identify? Here, we picked G
by its hash ID. Since a hash ID is not a name, this ID cannot be set as the upstream for the new branch.
If you omit the hash ID in git branch
, Git defaults to using HEAD
:
git branch m2
Since HEAD
is currently attached to master
, this makes m2
point to the same commit as master
:
...--E--F--G <-- marker
\
H--I--J <-- master (HEAD), m2
In this case, the upstream of m2
is left unset by default.
You can also create new branch names with git checkout
. The key difference between using git branch
and git checkout
is that git checkout
also attaches HEAD
to the new branch, moving to (checking out) a different commit than the previously-current commit if need be. For instance:
git checkout -b m2
(instead of git branch m2
) does not have to move at all, and does not move, but does re-attach HEAD
, producing:
...--E--F--G <-- marker
\
H--I--J <-- master, m2 (HEAD)
Commit J
is still the commit that is checked-out, but now HEAD
is attached to the name m2
. As before, m2
has no upstream.
Sometimes, picking a specific commit sets an upstream
As we just saw, if you let Git default to HEAD
, Git does not set an upstream for the new branch. Also, if you pick a specific commit by its hash ID, Git does not set an upstream. But sometimes, if you pick a specific commit by name, Git does set an upstream.
So: when does Git set an upstream? Well, consider the kinds of names we have for commits. Some of them are our own branch names, like master
and m2
and marker
above. Some of them are tag names like v1.2
. Some are remote-tracking names like origin/master
or origin/develop
. Which ones make the most sense as an upstream name?
If you said "the remote-tracking names", congratulations: you and Git think alike! If not, well, perhaps that's why you're always at war with Git. :-) Anyway, using a remote-tracking name as your starting-point tells Git: Not only do I want you to create this new branch, I'd also like you to set this remote-tracking name as the branch's upstream.
You can do the same thing explicitly using --track
, and in this case, you can have Git set the upstream to one of your own branches. For instance, to set the upstream of develop
to master
while creating develop
, you can use:
git branch --track develop master
or:
git checkout --track -b develop master
If you dislike --track
behavior (i.e., upstream-setting) at all times, you can either always add --no-track
to your command, or configure Git not to automatically set remote-tracking names as upstreams, using:
git config branch.autoSetupMerge false
If you really like --track
behavior a whole lot, and want it to happen even when using local branch names as starting points, you can configure Git to do this too:
git config branch.autoSetupMerge always
The --track
and --no-track
options, if you use them, override the configured default of always
or false
or true
. If you have not configured branch.autoSetupMerge
, Git pretends you have it set to true
, which means what we just outlined above: Default to --track
if the name is a remote-tracking name.
All of the above is purely meant to be convenient
You can always change, or remove, the upstream of any branch at any time, using git branch --set-upstream-to
or git branch --unset-upstream
. So any fiddling you do with --track
or --no-track
or branch.autoSetupMerge
is meant to be convenient. Set this to whatever you personally find the most convenient.
One last twist: orphan branches
Above, I said that creating a new branch requires picking a starting commit. This is true, but is almost a lie as well. There's a corner case that occurs in every new, totally-empty repository. Consider:
$ mkdir newrepo
$ cd newrepo
$ git init
Initialized empty Git repository in ...
At this point, you have no commits, and git branch
shows that you have no branches either. And yet, git status
tells you that you are on branch master
. How can this be?
$ git status
On branch master
No commits yet
nothing to commit (create/copy files and use "git add" to track)
The answer is that you are on a branch name that does not exist. Although this sounds self-contradictory, it's really just a special case in Git. The next commit you make will be a root commit: a commit with no parent. The act of creating this commit will produce a commit hash ID. That commit hash ID will create the branch.
Hence, this branch that you are on (Git has saved the name in HEAD
) does not exist, and you cannot set its upstream:
$ git branch --set-upstream-to=origin/master
fatal: branch 'master' does not exist
You must first create the branch, by committing. Once the branch exists, then you can set its upstream.
This is true for a new, empty repository because of the fact that there are no commits yet. But it's also true for any branch name you create with git checkout --orphan
, which does this same trick: it writes the new branch name into HEAD
, but does not actually create the branch.
What this boils down to is that the branch creation occurs by making a commit. So for this particular corner case—an orphan branch that does not really exist yet, or the master
branch in a new empty repository—you "choose" the commit to which the branch will point, not by looking at some existing commit, but by creating a new commit and, in the process, saying: This new commit is my choice for where the branch name shall point. The new commit is a root commit (has no parent) and the branch now exists, and only now can Git set its upstream.
Thus, for orphan branches, you cannot set the upstream until the branch actually exists.