Sorry if this is a noob question, but I've been searching for an hour+ and can't get a clear answer.
What I'd like to do is this:
git checkout origin/feature
git checkout -b feature_my_tweak
feature
called feature_my_tweak
that I can play around withcommit -m "added my tweak"
so now my local feature_my_tweak
has diverged from feature
git push --set-upstream origin feature_my_tweak
to push my local branch to the remote serverfeature
on the remote serverAt this point, I would like to be able to git pull
and have git see that there was a change to feature
and pull it (fetch/merge) into my local feature_my_tweak
branch. Is there a way to do that automatically? Or do I have to do a manually git merge feature
to get all the changes from that branch to my local feature_my_tweaks
branch? I guess I thought there was a way to have one branch track another.
Another part of this is that I'd still like to be able to make local changes to feature_my_tweaks
and still be able to git push
and have that only push up to the remote origin/feature_my_tweak
branch and not pollute the origin/feature
branch.
Is there a way to do all that? From what I can read, it seems that if any branch_A is tracking a remote branch_B, any pushes will go to branch_B, which is what I don't want.
I hope that makes sense.
The short version of the answer boils down to no, but it doesn't matter, don't worry about it and just do it manually; it's easy. If you have one particular branch that you do this with often, write a script or a Git alias to do it.
To do this manually, for your branch B where you want to know if there's new stuff on origin/feature
, vs your own branch feature_my_tweaks
with upstream set to origin/feature_my_tweaks
, just run:
git rev-list --count --left-right feature_my_tweaks...origin/feature
This prints out two numbers. The number on the left is the number of commits that you have on feature_my_tweaks
that are not part of your origin/feature
. The number on the right is the number of commits that are on your origin/feature
that are not on your feature_my_tweaks
.
If the second number is not zero, and you want to, you can now run git checkout feature_my_tweaks; git rebase origin/feature
or git checkout feature_my_tweaks; git merge origin/feature
.
Whether and when to use git rebase
is up to you, but after rebasing, you will need to git push --force origin feature_my_tweaks
(or git push --force-with-lease origin feature_my_tweaks
) to get the Git over at origin
to discard the old commits that your git rebase
discarded when it copied your commits to new-and-improved commits.
Probably the biggest problem here is terminology. What does the word tracking mean to you? Git uses this one word in at least three different ways,1 so at most one of them can match up with the one you're thinking of. (Possibly none of them match exactly, depending on what you're thinking happens here.)
Regardless of which word(s) you use, here is the right way to think about this:
What matters are commits. Each commit has a unique hash ID. Every Git everywhere—every clone of this repository—uses that hash ID for that commit. If you have a commit with that hash ID, it's the same commit. If you don't, you don't have that commit.
Names in Git take a bunch of forms. Each name holds one hash ID—just one!
The three forms that you use are:
Branch names: master
, develop
, feature
or feature/one
, and so on. These are yours, to do with as you will. (You'll probably want yours to match up with other peoples' names, though, at least at various times.)
Tag names: v2.1
and so on. While yours are yours, your Git will try to share them with other Git repositories: if your Git sees that theirs has a v2.1
and you don't, your Git is likely to copy theirs to yours.
Remote-tracking names: origin/master
, origin/feature
, and so on. Git calls these remote-tracking branch names and some people shorten that to remote-tracking branch, but they're not quite like branch names. Your Git slaves these to some other Git's branch names. You have your Git call up their Git and get any new commits from them. Then your Git updates your remote-tracking names so that they match theirs.
The remote-tracking name is built by sticking the remote name (in this case origin
) in front of their branch name, with a slash to keep them apart. That's why your origin/master
"tracks" their master
, updated every time you run git fetch origin
.2
All these names work similarly. They all have long forms: refs/heads/master
is the full spelling of master
, for instance, vs refs/remotes/origin/master
for origin/master
. That's how Git knows which one is which kind of name. Git normally then shortens the name for display to you, taking off the refs/heads/
part for branch names, or the refs/remotes/
part for remote-tracking names.
Note that git fetch
is as close as there is to the opposite of git push
. It might seem like these should be push
and pull
, but due to a historical accident3 it's push
and fetch
. Pull just means: Run fetch, then run a second Git command, git merge
by default, to merge with the current branch's upstream.
Fetch and push are very similar, but with two key differences:
Fetch gets things. You tell your Git: call up the Git whose URL you have stored under the name origin
(or whatever remote name you use here). Your Git calls up a server at that URL, which must answer as a Git repository. That server then tells you about its branch names and their commits, and your Git checks to see if you have those commits. If not, your Git asks their Git for those commits, and their parent commits if you don't have them, and the parents' parents if needed, and so on. They give you all the commits they have, that you don't, that your Git needs to complete these.
Having gotten the commits, git fetch
then updates your remote-tracking names by renaming their branches.
Push sends things. As before, you have your Git call up another Git, by a remote name like origin
. Then your Git gives them commits, rather than getting commits from them. But here, things are slightly different:
The commit(s) your Git offers are the tip commits of any branches you are pushing. (If you are just pushing one branch, that's just one commit.) If they don't have those commits, your Git must offer the parents of these tip commits. (Most commits have just one parent, so that's one more commit.) If they don't have those, your Git must offer more parents, and so on. Through this process, your Git finds all the commits their Git needs to have a complete picture of the tip commit you're sending: all of its history.
When you were fetching, they offered all their branches. The difference: you're only pushing whichever branches you specified.
After your Git has sent any commits you have, that they don't, that they need to complete the tip commit(s) for the branch(es) you're pushing, your Git sends a polite request for them to set their branch name(s).
When you were fetching, your Git set your remote-tracking names. You're now asking them to set their branch names.
To summarize these key points:
You can have git push
ask them to set a different name than the one you use to select the commit(s) to send. For instance:
git push origin test-xyz:new-feature
sends the tip commit of your test-xyz
branch (and its parents and other ancestors if/as needed), but asks them to set or create their branch name new-feature
.
So:
... it seems that if any branch_A is tracking a remote branch_B, any pushes will go to branch_B ...
This is entirely wrong, at least by default. (There is a setting for push.default
that does make that happen, though.)
There is a lot more to know here, in particular, what this git rev-list --left-right --count
is doing and what it means for commits to be "on"—or more precisely, reachable from—a branch name, but I've kind of run out of time and space in this answer, at this point.
1In particular, files can be tracked or untracked, some names are remote-tracking names, and a branch with an upstream set is said to be tracking its upstream. The verb (in its present participle form) becomes an adjective, modifying name or file, in the first two cases.
2There are ways to run a limited git fetch
that doesn't update all your remote-tracking names. Using git pull
sometimes does this, for instance. I prefer to separate my git fetch
and my other commands, rather than using the git pull
fetch-then-run-another-Git-command command.
3It's common after fetching to want to integrate what you fetched, so at first, git fetch
was a back-end "plumbing" command, not meant for users to run, and git pull
ran git fetch
, then git merge
. This was before remotes existed as a concept, and hence before remote-tracking names existed either, so there wasn't any reason for users to run git fetch
.
Over time, Git grew remotes and remote-tracking names, and now the difference between pull
—fetch and combine—and just fetch
without combining (or at least, without doing it yet) became both useful and important. But the name pull
was already in use to mean fetch and combine, hence this particular historical accident.
4That is, this is the default in Git 2.0 and later. You can change it with push.default
, and Git versions older than 2.0 default to matching
.
When using matching
, your Git asks their Git about their branch names, matches up your matching branch names, and defaults to pushing from your matching names to their matching names.
If you change this setting to upstream
, your Git asks their Git to set their branch based on the upstream setting of your branch, which is what you were assuming in your question.
The modern default setting is simple
, which requires that the upstream be set to a branch of the same name, so that git push
on your end just fails right away if your upstream is set to a different name of their side. However, you can override this easily by typing in git push origin branch
, which means the same thing as git push origin branch:branch
: ask their Git to use the same branch name as you are using in your Git.