Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Does git rebase affect the remote branch or local branch

Tags:

git

branch

rebase

I am working on my branch called "role". Remote master is being updated frequently. So after I commit my changes to remote branch, I need to rebase the branch on the latest master. So here are my steps:

# from local
git add .
git commit 'Add feature'
git push origin role

# rebase
git pull --rebase origin master

Does rebase affect only the local branch or remote branch or both? If only local branch is rebased, should I commit to origin again after the rebase?

like image 762
ddd Avatar asked Feb 01 '17 20:02

ddd


1 Answers

Summary: yes, but your question is wrong. :-)

There is a bunch of stuff to keep straight here. The Git terminology is not very good to start with—"remote", "tracking", "branch", and "remote-tracking branch" all mean very different things!—and you're using yet another set of terms, which makes things even trickier. So, before I answer what I think you mean to ask, let's define these terms. (See also gitglossary, though not all of these are in there.)

  • A remote is a short name like origin. It mainly serves as a placeholder for a URL, so that you don't have to keep typing some long URL, and also forms part of the name of a remote-tracking branch (which we'll define in a moment).

  • A branch is an ambiguous term: see What exactly do we mean by "branch"? In this particular case, we'll start by using it to mean a branch name like master. A branch name, in Git, is just a name for a particular commit, which we call the tip of the branch, but a local (ordinary) branch name like master has a special property, which we'll get to soon.

  • A remote-tracking branch is a name your Git assigns, and updates for you, in your repository, based on what your Git saw when your Git called up some other Git. These are the names like origin/master. These remote-tracking branch names do not have the special property of ordinary (local) branch names.

  • Tracking is a terrible verb that Git uses as shorthand for a more modern and verbose way to say the same thing. A local branch can be set to "track" a remote-tracking branch. The more modern way to say this is that the local branch has the remote-tracking branch set as its upstream. Setting an upstream does nothing you could not do "by hand", but makes Git do it automatically for you, which is handy.

    Usually, we would set the remote-tracking branch origin/master as the upstream for the local branch master. Likewise, we would set origin/role as the upstream for the local branch role. It's important to remember, at this point, that both the local branch, and the remote-tracking branch, are in your own Git. They're both actually local! The remote-tracking branches are just called "remote-tracking" because they are automatically updated.

Next, remember that when you are using git fetch and git push (and git pull, but see aside at end), your Git calls up another Git. The other Git has its own separate repository. The way your Git gets hold of the other Git is via some URL, and the URL is stored under the "remote" name, e.g., origin: your Git calls up origin's Git over the Internet-phone and talks with the other Git. Your Git gets things from them (git fetch) or gives things to them (git push), and your Git and their Git both work purely locally, with your Git working on your repository, and their Git working on their repository.

Everything is local

This is why, in Git, to a first approximation at least, everything is local. There aren't really any remote anythings, just local entities, plus these special Internet-phone-calls where multiple Gits exchange data with each other, while working locally. Fortunately, since fetch and push are the main two contact points, it's easy to keep this straight: everything is local until you fetch or push. Those are the points where your Git and another Git transfer data and make (local!) changes.

So, now we can get to your questions:

Does rebase affect only the local branch or remote branch or both?

This one is easy: If you're not pushing, you can only affect local things.

If only local branch is rebased, should I commit to origin again after the rebase?

This one has an embedded misconception. When you make commits, you're still only affecting local things. To send your local things elsewhere, you need to push them (or, if your machine can act as a server, let your machine answer someone else's git fetch phone-call—but let's not go there, at least not yet!).

There's one big complication here, because git fetch and git push are not symmetric.

Push and fetch are not symmetric

When you run git fetch, you have your Git call up their Git and get new stuff from them. Your Git saves the new stuff—commits and whatever else those commits need, really—away in your repository, then updates your remote-tracking branch names. These are quite separate from your own local branch names: your origin/master is separate from your master, and your origin/role is separate from your role. The only thing your origin/* branch names do is remember what your Git saw on their Git, so it's quite safe to change them any time your Git sees new stuff on their Git.

When you run git push, though, your Git calls up their Git, sends them your stuff, and then asks them to change their master or their role. They don't have a ddd/master or ddd/role: you are having your Git ask them to change their own, and only, master or role. If they have something new in their master or role, that you did not already incorporate into what you are pushing, you will be asking them to give up those new things, and take yours instead.

Now it's time to bring up the special property of local branch names, and look at how branch names and commits work, and look at how git rebase actually works. This is also a good time to review that other question, What exactly do we mean by "branch"?

How branches normally grow, and the violence rebase does to this

Git is mostly all about commits. Commits are Git's raison d'être: they are eternal, changeless snapshots of whatever you committed. No commit can ever be changed, although existing commits can be copied to new (different) commits, by extracting the old snapshot, making the change—whatever it may be—and making the new snapshot in some new location. But what do I mean by "location" here? This is where the commit graph enters the picture.

Every commit has a unique ID. This is the big ugly commit face0ff... kind of number-and-letter combination Git shows at various times. (In fact, every Git object has one of these IDs, but right now we only care about commit objects.)

Each commit saves, along with its snapshot of whatever you have git add-ed or kept from the previous commit, your name, your email address, your log message, and—this is the key item here—the ID of that previous commit.

This means that commits form (backwards) chains:

A <- B <- C   <-- master

Branch names, like master, point to tip commits. The tip commit is the most recent in the chain. The most recent points back to its parent (earlier) commit, which points back to another parent, and so on. This chain ends only when we get to the first commit, which has no parent.

Since new commits point back to older commits, we need Git to "advance" the branch name, whenever we make a new commit, to point to the new commit we just made. That is, if we have A <- B <- C as above and we make a new D, we need Git to move master to point to D now:

A <- B <- C <- D   <-- master

This is the special property of local branch names. They automatically advance, to the new commit we just made, when we make a commit while we're on that branch.

This leads to a simple rule about branch names (but this rule is soon violated): they advance in a way that keeps all the old commits. We add a new commit to a branch, and the new commit points back to the old commit, and we've only added to the branch. (Note that we are now talking about a branch as a collection of commits, rather than a name pointing to exactly one commit!)

Rebase violates this simple rule, on purpose. Let's look at what happens when we make branches. Let's also stop drawing all the internal arrows, and just remember that they all point backwards: we always move left-ish when we are working backwards from newer commits to older ones.

A--B--C--D        <-- master
       \
        E--F--G   <-- role

Here we have seven commits total, with three of them being only on branch role, one—commit D—being only on branch master, and three—the A-B-C chain—being on both branches. (This is another peculiar thing about Git: commits are often on many branches.)

When we use git rebase, we want to move commits, generally to come right after some other branch's (new) tip. For instance, in this particular case, we want to move the E-F-G chain to come after D, instead of coming after C. But commits cannot be changed, only copied.

So that's what git rebase does: it copies commits. We copy E-F-G to new commits, E'-F'-G', that are a lot like E-F-G, but they are at a different location in the graph: they come after D:

A--B--C--D           <-- master
       \  \
        \  E'-F'-G'  <-- role
         \
          E--F--G    [previous role, now abandoned]

The rebase command first copies the commits, then peels the branch name role off the old tip commit G and pastes it onto the new copy G'.

This loses the old E-F-G chain, abandoning it in favor of the new shiny E'-F'-G' chain instead. The new commits, being slightly different—if nothing else, E' is different from E in that E' has D as its parent, vs C—have different IDs.

Hence when we push we must force-push

If we've pushed the original E-F-G to origin, we have given them exact-duplicate copies of the original E-F-G. These come after C, not after D. Now we're going to git push origin role again, so we will have our Git call up the other Git and hand over E'-F'-G', and then ask them to set their role to point to commit G'.

They will say "no"!

If they set their role to point to G', they will lose commits E-F-G. Right now the only way for their Git to find G is to look at the (their) name role, and if they change their role to point to G', they will lose this link to G.

Of course, that's what we want them to do! But the default is for them to say "no, if I do that, I will lose some commits." (The reason for this default is simple: they don't know or care that we gave them G, all they know is that, right now, they will lose G.) So we have to change our Git's polite request, "please move role", to a more forceful command: "move role!"

Ideally, we can even say: "We think your role names G, and if so, it should move to name G' instead; but let us know if we're wrong about your role, OK?" Git calls this a --force-with-lease. It's not supported in every version of Git, and it's not necessary if you are sure that you are the only one who ever changes their role; but some kind of forcing is required, because you need to get their Git to abandon the old (copied) commits, just like your Git did.

Aside: git pull

The git pull command is meant as a convenience. Whenever you run git fetch, you can pick up a bunch of new commits from the remote as usual, but those new commits all wind up under their remote-tracking branches. Once you have those commits, you generally want to do something with them. Two main things you can do with them is git merge or git rebase. So git pull combines git fetch with an immediate git merge or git rebase afterward.

The main problem with this is that until you have fetched and inspected any new commits, you don't necessarily know for sure what you will want to do with them. Using git pull makes you decide in advance what to do with them. Imagine saying: "I'm going to reach into this dark closet and pull out a bottle, and whatever it is, I'm going to drink it." If you pull out a beer, that's fine; but what if you pull out a bottle of bleach?

In any case, all of the above still applies to git pull, because it's just git fetch followed by a second command. The fetch calls up another Git, via the URL stored under the remote name. The second step, whether it's git merge or git rebase, then works locally. The particularly confusing thing about git pull is that, because it predates the invention of remote-tracking branch names, it has a special syntax all its own. This is why, even though you are fetching from origin, then rebasing on origin/master, you write "pull (as rebase) origin master" instead of "pull (and then rebase on) origin/master".

like image 62
torek Avatar answered Oct 05 '22 12:10

torek