0

I have this:

git branch # I am on a feature branch "X"  
git fetch origin dev;
git checkout -b "${new_branch}" "origin/dev"

the problem is that the last command checks out a new branch with X as the base, instead of "origin/dev" as the base. Why would it do that? I was under the impression that git checkout -b foo bar would checkout a new branch called foo using bar as the "base" (please correct my terminology). Why wouldn't that work?

I am on git version: 2.17.1

perhaps I should use this instead:

git checkout -b "${new_branch}" --track origin/dev

?

update: what is likely happening is that origin/dev is getting updated with changes from the local feature branches. so the first command I am using does use origin/dev as the base, it's only that origin/dev is seeing updates from the feature branches because tracking was set up...

Alexander Mills
  • 90,741
  • 139
  • 482
  • 817
  • What, precisely (i.e., in terms of `git config --list` output for instance, or `git rev-parse ${new_branch}` result) do you mean by *with X as the base*? – torek Aug 03 '18 at 18:40

3 Answers3

2

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.

torek
  • 448,244
  • 59
  • 642
  • 775
  • interesting, but why would the local upstream (remotes/origin/dev) branch ever have changes from other local branches if `git push` was never called? – Alexander Mills Aug 04 '18 at 00:40
  • In that case, it shouldn't: commits you make to a branch cause that branch name to point to the new commit, while remote-tracking names like `origin/dev` should be adjusted only if the *other Git* (at `origin`) has changed *its* `dev` to point to a different commit. If you have the commit and have never given it to `origin`, `origin`'s `dev` cannot point to that commit and your own `origin/dev` should therefore not point to that commit. (Whew!) On the other hand, if you *did* run `git push` with no arguments, and if your Git is configured to push your commits to `origin` and then [continued] – torek Aug 04 '18 at 01:13
  • ... and then request that `origin`'s Git set *its* `dev` accordingly, and if they do so, your Git will adjust your `origin/dev` to match their `dev` that now points to that commit. Since that's entirely normal for Git, that's probably what actually happened: you, or someone else while you were not watching, pushed your commit to `origin`'s `dev`. – torek Aug 04 '18 at 01:14
1

My guess is what's happening is that this command:

git checkout -b "${new_branch}" "origin/dev"

means that the new branch will track origin/dev, which means origin/dev gets updated with local changes.

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}"

I haven't completely verified that it works, but it seems to so far. Kind of a nightmare tho.

Alexander Mills
  • 90,741
  • 139
  • 482
  • 817
1

I created this alias to perform this task:

git config --global alias.nb '!bash -c "git fetch --prune; git checkout -b $1 --no-track ${2-origin/dev}" -'

You'd run it using this command:

$ git nb <branch name> [<source commitish>]

It performs a full fetch with prune, and accepts two parameters, the first is the new branch name, the second is the source branch, but defaults to origin/dev if none is supplied.

Using --track would set you up to push your feature directly to origin/dev.

Also remember that branches are just pointers, so the command could be read as "Create a new branch pointer named <branch name> at the commit referenced by origin/dev without setting up remote tracking and check out the new branch"

LightBender
  • 4,046
  • 1
  • 15
  • 31