Search code examples
gitvisual-studiogithubversion-controlgit-push

Create/Push new local branch to repository based on remote


Can someone tell me what's wrong with my action? I'm trying to create a new branch off a remote branch, and push the new branch to my repository.

Description:
-Currently on branch "test"
-git pull
-git checkout -b <new_branch_name> origin/test

I confirmed my branch was switched to the new branch. However, when I commit/pushed my changes through visual studio the new branch was NOT created and the changes were pushed to the remote "origin/test".

On the contrary, if I run the checkout command without the second parameter "origin/test" (while still currently on the origin/test branch) and then I execute git push, my new branch is created

Why is this occurring?


Solution

  • TL;DR

    It's because the upstream of your new branch is set to origin/test. Visual Studio apparently (mis)uses this to decide to ask the remote to update its test branch.

    Important background

    Branches are not created from branches. Branches are created from commits. It's important to know and understand this; if you think that branches mean something in Git, you will eventually hurt yourself. It's like assuming that a 1980s-era automobile has self-driving capability.

    Upstreams

    That said, each branch, in Git, can have one upstream set. A branch either has an upstream set, or does not have an upstream set. To set a particular upstream, use git branch --set-upstream-to:

    git branch --set-upstream-to=origin/somebranch
    

    for instance. The current branch now has origin/somebranch set as its upstream (unless the command failed, in which case nothing has changed). If it previously had origin/otherbranch set as its upstream, that upstream is now removed, because only one upstream can be set: origin/somebranch is now the upstream.

    To unset the upstream, use git branch --unset-upstream:

    git branch --unset-upstream
    

    The current branch now has no upstream. If it had no upstream before, this operation does nothing; otherwise, it removes the upstream setting.

    The upstream of a branch is:

    • another (local) branch name, or
    • a remote-tracking name like origin/somebranch.

    In command line Git, the behavior git push, with no additional arguments, is controlled by the push.default setting. If this is set to simple—as it normally is today—this kind of git push will only push to the upstream of the current branch, and only if the upstream is set to a remote-tracking name that—once the remote part is removed—matches the current branch.

    For example, suppose the current branch is named br1, and both origin/br1 and origin/br2 exist. If we run:

    git branch --set-upstream-to=origin/br2
    git push
    

    we will get an error from git push because the name br2 in origin/br2 does not match the name br1. However, if we then run:

    git branch --set-upstream-to=origin/br1
    git push
    

    the git push will this time call up origin and attempt to have the Git repository at origin update that other Git repository's branch br1 (the branch on the other Git that we remember, locally, as origin/br1).

    Branch creation and upstream settings

    When we choose to create a new branch—with git branch, git checkout -b, or git switch -c—we must give Git two things:

    1. Git needs the name of the new branch it should create.
    2. Git needs the hash ID of some existing commit. The newly created branch will point to this existing commit.

    The default hash ID, if we do not give Git one, is the hash ID of the current commit (as found by the current branch name in most cases). That is, if we have run git checkout test so that the current branch name is test, and we run:

    git rev-parse test
    

    and

    git rev-parse HEAD
    

    we will get the same hash ID from both operations. That's because the special name HEAD is attached to the branch name test, and the branch name test points to the commit whose hash ID both git rev-parse commands print.

    We can draw this setup like this:

    ... <-F <-G <-H   <-- test (HEAD)
    

    That is, the name test points to some commit with some hash ID, drawn here as H, which stands for "hash". Every commit in Git has both a snapshot and metadata, and the metadata in a commit contain a list of previous commit hash IDs, usually just one entry long. In this case commit H contains the hash ID of—i.e., "points to"—some earlier commit, whose hash ID we're just calling G. (The real hash ID, in each case, is some big ugly random-looking string of letters and digits.) Commit G has a snapshot and metadata, and thereby points to still-earlier commit F, which has a snapshot and metadata, and so on.

    When we ask Git to create a new branch name, with git checkout -b newbr in this case, Git will:

    1. create the new name, pointing to the selected commit—in this case commit H—and then
    2. attach HEAD to the new name.

    We can draw the result like this:

    ... <-F <-G <-H   <-- newbr (HEAD), test
    

    That is, both branch names point to the same commit (whose hash ID is H).

    When we use this form of branch creation, the new branch has no upstream set. So newbr has no upstream. A command-line git push will, with the defaults in modern Git, give us an error; we will have to run git push -u origin newbr or git push -u origin HEAD to create a name newbr over on origin so that our Git will create origin/newbr locally, so that we have an origin/newbr to have as an upstream. The -u option to git push will then immediately set that as the upstream.

    Visual Studio apparently behaves differently. If no upstream is set, VS apparently just runs git push origin newbr:newbr, rather than giving us an error.

    You can, however, tell Git, at the time you create a branch, to set its upstream right then. To do so, you must use the form in which you give Git a specific commit by name rather than by raw hash ID:

    git checkout -b newbr origin/test
    

    for instance. This is what you are doing. When you use this form of git checkout -b or git switch -c or git branch, Git will, by default:

    1. use the name origin/test to find a commit hash ID;
    2. create the new branch name newbr pointing to this commit; and
    3. set the upstream of newbr to origin/test.

    Note this extra step 3, that does not occur when using git checkout -b newbr without that additional argument.

    In command-line Git, this choice—to create a new branch with an upstream set—is actually controllable via multiple options:

    • The -t or --track option says do do this.
    • The --no-track options says do not do this.
    • The configuration setting branch.autoSetupMerge says whether to do this, in which cases, when -t or --no-track is not specified.

    The description I've given above covers the normal behavior when branch.autoSetupMerge is not set, or is set to true. You can set it to false, in which case branch creation options always act as if --no-track were specified, and you can set it to always, which makes Git set a local branch as the upstream of a new branch when you provide a local branch name as the starting point.

    You probably should not set this option, and simply avoid giving git branch a starting name, or use the --no-track option:

    git checkout -b newbr $(git rev-parse origin/test)
    

    or:

    git checkout -b newbr --no-track origin/test
    

    Both methods will avoid setting any upstream for the new branch.