简体   繁体   中英

How to set up branches with different pull/push upstreams

I have a local git repo with two remotes: upstream , the original main repo, and origin , my GitHub fork of it.

I want to create new branches based off of upstream/master , push them to origin for PRs, and periodically pull in new changes from upstream/master .

Is there a way to set up my branches so that this happens by default? Ie:

$ git checkout -b my-new-branch --some-other-flags
$ git maybe some other command
# branch 'my-new-branch' points to 'upstream/master' and is checked out
# make changes, git commit
$ git push  # pushes to origin/my-new-branch
$ git pull  # pulls from upstream/master

The following seems to work:

git config push.default current && git config remote.pushdefault origin

Then, create branches with git checkout -b new-branch upstream/master .

git push pushes to origin/my-branch , git pull pulls from upstream/master .


For branches based on other local branches instead of upstream/master, things seem a bit trickier. I could git config branch.autoSetupMerge always , but then branches would pull from the local branch they started from, not upstream/master . Or I could set the upstream to upstream/master explicitly with -u when creating the branch. I'm not sure which would be more appropriate though.

Another annoyance is that when I checkout a branch with changes, git sometimes tells me:

Your branch is ahead of 'upstream/master' by 7 commits. (use "git push" to publish your local commits)

But A) it's fine that I'm ahead of my upstream, I'm waiting to merge these changes in a PR, and B) more importantly, git push will push to new-branch at origin , not master at upstream.

This doesn't always happen though, so I think there's some other variable I'm missing here.

Is there a way to set up my branches so that this happens by default?

No: you get only one "upstream" or @{u} setting per branch (you can have a branch with no upstream set, if you like, but your other option is one upstream). git fetch fetches from the the remote in this upstream and git merge merges with the branch named in this upstream (and as usual, git pull essentially equals fetch + merge); git push pushes ... well, now things get complicated.

Is there a way to set up my repository so that this happens by default?

Yes, but with a flaw. How big this flaw is depends on your usage and needs. Using (and configuring) this flaw is extra-complicated. Let's run through the items.

Each local branch can have only one upstream, but:

  • The upstream names two parts. One is a remote like origin or upstream ; the other is a merge like refs/heads/master .

These get combined to make origin/master , for instance. So we're already out of luck with having the default upstream be both origin/master and anything /my-new-branch , whether or not the anything is origin .

But:

  • You can configure Git for a "triangular workflow" where you fetch from one URL but push to another, by setting two URLs for any one given remote.

This means you can make some remote, let's call it tri for triangular, fetch from the URL you have for origin and push to the one you have for upstream . If branch B has tri as its remote, and tri fetches from the same URL as origin but pushes to the same URL as upstream , then you will, in effect, fetch from origin and push to upstream .

Your Git will be a little confused, though, as to what refs/remotes/tri/master means . If a push of the form:

git push tri somebranch:master

succeeds, your Git now thinks that refs/remotes/tri/master has the same hash you just pushed. Your Git thinks: Well, sure! The guy I called up under tri says he took it! Once you run git fetch tri you'll get some other hash and your Git will fix this up to remember what's on the same URL as origin again. Your Git thinks: Well, that's funny, the guy I called up under tri says he has reset his master . Oh well, that's the other Git for you, resetting his master all the time...

Furthermore, when you run git push tri or git push with tri implied, but no refspec arguments on the command line ... well, this comes from the git push documentation :

If git push [<repository>] without any <refspec> argument is set to update some ref at the destination with <src> with remote.<repository>.push configuration variable, :<dst> part can be omitted—such a push will update a ref that <src> normally updates without any <refspec> on the command line. Otherwise, missing :<dst> means to update the same ref as the <src> .

This means you can set a special remote.tri.push configuration, so that git push tri without naming a source, or git push tri somebranch without naming :<dst> , you can make somebranch map to somebranch here, even if the one-upstream-setting you are allowed for somebranch says master .

Putting these all together

  • You only get one @{u} .
  • But that @{u} has two parts, a remote and a merge .
  • Your @{u} can name a special remote that has two URLs: one for fetching, and a second for pushing.
  • This remote can also have a remote. remote .pushdefault remote. remote .pushdefault that forces the push :dst part to go to a different name than the fetch's merge setting. (Let's call this pushtarget for the last point.)
  • The side effect is that the remote-tracking branch specialremote/pushtarget will become wrong until you run git fetch specialremote . This will make things confusing, as if they weren't already confusing because of all of the above.

If you ask me whether doing the above is a good idea , I will say: definitely not .

The new solution for a triangular workflow (pull from one branch, push to another) would be:

git config --global merge.autosetupmerge=true  # pull from origin/master
git config push.default current                # push to origin/feature1
git switch -c feature1 origin/master

With Git 2.37 (Q3 2022), " git -c branch.autosetupmerge=simple branch $A $B " ( man ) " will set the $B as $A 's upstream only when $A and $B shares the same name, and " git -c push.default=simple " on branch $A would push to update the branch $A at the remote $B came from.
Also more places use the sole remote, if exists, before defaulting to ' origin '.

See commit 05d5775 , commit 8a649be , commit bdaf1df (29 Apr 2022) by Tao Klerks ( TaoK ) .
(Merged by Junio C Hamano -- gitster -- in commit f49c478 , 26 May 2022)

branch : new autosetupmerge option ' simple ' for matching branches

Signed-off-by: Tao Klerks

With the default push.default option, " simple ", beginners are protected from accidentally pushing to the "wrong" branch in centralized workflows: if the remote tracking branch they would push to does not have the same name as the local branch, and they try to do a "default push ", they get an error and explanation with options.

There is a particular centralized workflow where this often happens: a user branches to a new local topic branch from an existing remote branch, eg with " checkout -b feature1 origin/master ".

Problem:

With the default branch.autosetupmerge configuration (value " true "), git will automatically add origin/master as the upstream tracking branch.

When the user pushes with a default " git push " ( man ) , with the intention of pushing their (new) topic branch to the remote, they get an error, and (amongst other things) a suggestion to run " git push origin HEAD ".

If they follow this suggestion the push succeeds, but on subsequent default pushes they continue to get an error - so eventually they figure out to add " -u " to change the tracking branch, or they spelunk the push.default config doc as proposed and set it to " current ", or some GUI tooling does one or the other of these things for them.

When one of their coworkers later works on the same topic branch, they don't get any of that "weirdness".
They just " git checkout feature1 " ( man ) and everything works exactly as they expect, with the shared remote branch set up as remote tracking branch, and push and pull working out of the box.

The "stable state" for this way of working is that local branches have the same-name remote tracking branch ( origin/feature1 in this example), and multiple people can work on that remote feature branch at the same time, trusting " git pull " ( man ) to merge or rebase as required for them to be able to push their interim changes to that same feature branch on that same remote.

(merging from the upstream " master " branch, and merging back to it, are separate more involved processes in this flow).

Problem (bis):

There is a problem in this flow/way of working, however, which is that the first user, when they first branched from origin/master , ended up with the "wrong" remote tracking branch (different from the stable state).

For a while, before they pushed (and maybe longer, if they don't use -u / --set-upstream ), their " git pull " was not getting other users' changes to the feature branch - it was getting any changes from the remote " master " branch instead (a completely different class of changes!)

An experienced Git user might say well yeah, that's what it means to have the remote tracking branch set to origin/master !" - but the original user above didn't ask to have the remote master branch added as remote tracking branch - that just happened automatically when they branched their feature branch.
They didn't necessarily even notice or understand the meaning of the "set up to track ' origin/master '" message when they created the branch - especially if they are using a GUI.

Looking at how to fix this, you might think "OK, so disable auto setup of remote tracking - set branch.autosetupmerge to false" - but that will inconvenience the second user in this story - the one who just wanted to start working on the topic branch.
The first and second users swap roles at different points in time of course - they should both have a sane configuration that does the right thing in both situations.

Make this "branches have the same name locally as on the remote" workflow less painful / more obvious by introducing a new branch.autosetupmerge option called " simple ", to match the same-name " push.default " option that makes similar assumptions.

This new option automatically sets up tracking in a subset of the current default situations: when the original ref is a remote tracking branch and has the same branch name on the remote (as the new local branch name).

Update the error displayed when the 'push.default=simple' configuration rejects a mismatching-upstream-name default push, to offer this new branch.autosetupmerge option that will prevent this class of error.

With this new configuration, in the example situation above, the first user does not get origin/master set up as the tracking branch for the new local branch.
If they " git pull " in their new local-only branch, they get an error explaining there is no upstream branch - which makes sense and is helpful.
If they " git push ", they get an error explaining how to push and suggesting they specify --set-upstream - which is exactly the right thing to do for them.

This new option is likely not appropriate for users intentionally implementing a " triangular workflow " with a shared upstream tracking branch, that they " git pull " in and a private feature branch that they push/force-push to just for remote safe-keeping until they are ready to push up to the shared branch explicitly/separately.
Such users are likely to prefer keeping the current default merge.autosetupmerge=true behavior, and change their push.default to " current ".

git config now includes in its man page :

branch.autoSetupMerge

simple -- automatic setup is done only when the starting point is a remote-tracking branch and the new branch has the same name as the remote branch.
This option defaults to true.

git branch now includes in its man page :

-t / --track[=(direct|inherit)]

The branch.autoSetupMerge configuration variable specifies how git switch , git checkout and git branch should behave when neither --track nor --no-track are specified:

  • The default option, true , behaves as though --track=direct were given whenever the start-point is a remote-tracking branch.
  • false behaves as if --no-track were given. always behaves as though --track=direct were given.
  • inherit behaves as though --track=inherit were given.
  • simple behaves as though --track=direct were given only when the start-point is a remote-tracking branch and the new branch has the same name as the remote branch.

Once you create the branch, you'll need to push it upstream and set it so your local can keep track of the remote .

You can do this as follows (assuming you're already in the master branch):

// Create and checkout the new branch
git checkout -b <your_branch_nane>

// Push new branch upstream and set it to track remote branch
git push -u origin <your_branch_name>

This will push your new branch upstream; the -u argument sets it to track the remote branch. The -u argument is just a shortcut for the --set-upstream argument.

You can read more about working with remote branches .

There are a couple of ways to approach this. I think what you probably want is to adopt a workflow that looks like this:

  1. Start working on a new branch based on the upstream master:

     git checkout -b my-new-branch upstream/master 
  2. Make your changes, and configure the branch to push to your remote repository:

     git push -u origin my-new-branch 

    (Subsequently you can just run git push by itself and changes will be sent to your repository)

  3. When you want to bring in new changes from upstream:

     git remote update git rebase upstream/master 

This allows you to periodically bring in new changes from upstream while preserving a linear history (rather than one littered with merge commits), which simplifies things in the event that you want to submit changes back to upstream.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM