Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to rebase branch against master after parent is squashed and committed?

I use an optimistic work-flow in Gitlab, which assumes the majority of my merge requests will be accepted without change. The flow looks like this:

  1. Submit a merge request for branch cool-feature-A
  2. Create a new branch, based on cool-feature-A, called cool-feature-B. Begin developing on this branch.
  3. A colleague approves my merge request for cool-feature-A.
  4. I rebase cool-feature-B against master (which is painless) and continue development.

The problem occurs if a colleague does a squash merge at step 3. This rewrites large chunks of history which are present in cool-feature-B. When I get to step 4, there is a world of merge pain ahead of me.

How can I avoid this happening?

like image 461
Duncan Jones Avatar asked Jun 28 '19 09:06

Duncan Jones


People also ask

How do I rebase a branch against a master?

To rebase, make sure you have all the commits you want in the rebase in your master branch. Check out the branch you want to rebase and type git rebase master (where master is the branch you want to rebase on).

How to rebase the current branch from Master in Git?

The following command rebase the current branch from master (or choose any other branch like develop, suppose, the name of remote is origin, which is by default): After git rebase, conflicts may occur. You should resolve them and add your changes by running git add command: git add . Do not run git commit after git add .

How do I REBASE the server branch to master?

You can rebase the server branch onto the master branch without having to check it out first by running git rebase <basebranch> <topicbranch> — which checks out the topic branch (in this case, server) for you and replays it onto the base branch ( master ):

How do I REBASE feature branches with many commits?

It can be tiresome to rebase feature branches with many commits. You may have several commits that conflict with your main branch. Before rebasing such branches, you may want to squash your commits together, and then rebase that single commit, so you can handle all conflicts at once. Here’s how to do that.

What is REBASE command in Git?

Rebase is a Git command which is used to integrate changes from one branch into another. The following command rebase the current branch from master (or choose any other branch like develop, suppose, the name of remote is origin, which is by default): git rebase origin/ master After git rebase, conflicts may occur.


Video Answer


1 Answers

Essentially, you have to tell Git: I want to rebase cool-feature-B against master, but I want to copy a different set of commits than the ones you'd normally compute here. The easiest way to do this is going to be to use --onto. That is, normally you run, as you said:

git checkout cool-feature-B
git rebase master

but you'll need to do:

git rebase --onto master cool-feature-A

before you delete your own branch-name / label cool-feature-A.

You can always do this, even if they use a normal merge. It won't hurt to do it, except in that you have to type a lot more and remember (however you like) that you'll want this --onto, which needs the right name or hash ID, later.

(If you can get the hash ID of the commit to which cool-feature-A points at the moment into the reflog for your upstream of cool-feature-B, you can use the --fork-point feature to make Git compute this for you automatically later, provided the reflog entry in question has not expired. But that's probably harder, in general, than just doing this manually. Plus it has that whole "provided" part.)

Why this is the case

Let's start, as usual, with the graph drawings. Initially, you have this setup in your own repository:

...--A   <-- master, origin/master
      \
       B--C   <-- cool-feature-A
           \
            D   <-- cool-feature-B

Once you have run git push origin cool-feature-A cool-feature-B, you have:

...--A   <-- master, origin/master
      \
       B--C   <-- cool-feature-A, origin/cool-feature-A
           \
            D   <-- cool-feature-B, origin/cool-feature-B

Note that all we did here was add two origin/ names (two remote-tracking names): in their repository, over at origin, they acquired commits B, C, and D and they set their cool-feature-A and cool-feature-B names to remember commits C and D respectively, so your own Git added your origin/ variants of these two names.

If they (whoever "they" are—the people who control the repository over on origin) do a fast-forward merge, they'll slide their master up to point to commit C. (Note that the GitHub web interface has no button to make a fast-forward merge. I have not used GitLab's web interface; it may be different in various ways.) If they force a real merge—which is what the GitHub web page "merge this now" clicky button does by default; again, I don't know what GitLab does here—they'll make a new merge commit E:

...--A------E
      \    /
       B--C
           \
            D

(here I've deliberately stripped off all the names as theirs don't quite match yours). They'll presumably delete (or maybe even never actually created) their cool-feature-A name. Either way, you can have your own Git fast-forward your master name, while updating your origin/* names:

...--A------E   <-- master, origin/master
      \    /
       B--C   <-- cool-feature-A [, origin/cool-feature-A if it exists]
           \
            D   <-- cool-feature-B, origin/cool-feature-B

or:

...--A
      \
       B--C   <-- cool-feature-A, master, origin/master [, origin/cool-feature-A]
           \
            D   <-- cool-feature-B, origin/cool-feature-B

Whether or not you delete your name cool-feature-A now—for convenience in later drawings, let's say you do—if you run:

git checkout cool-feature-B
git rebase master

Git will now enumerate the list of commits reachable from DD, then C, then B, and so on—and subtract away the list of commits reachable from master: E (if it exists), then A (if E exists) and C (whether or not E exists), then B, and so on. The result of the subtraction is just commit D.

Your Git now copies the commits in this list so that the new copies come after the tip of master: i.e., after E if they made a real merge, or after C if they did a fast-forward merge. So Git either copies D to a new commit D' that comes after E:

              D'   <-- cool-feature-B
             /
...--A------E   <-- master, origin/master
      \    /
       B--C
           \
            D   <-- origin/cool-feature-B

or it leaves D alone because it already comes after C (so there's nothing new to draw).

The tricky parts occur when they use whatever GitLab's equivalent is of GitHub's "squash and merge" or "rebase and merge" clicky buttons. I'll skip the "rebase and merge" case (which usually causes no problems because Git's rebase checks patch-IDs too) and go straight for the hard case, the "squash and merge". As you correctly noted, this makes a new and different, single, commit. When you bring that new commit into your own repository—e.g., after git fetch—you have:

...--A--X   <-- master, origin/master
      \
       B--C   <-- cool-feature-A [, origin/cool-feature-A]
           \
            D   <-- cool-feature-B, origin/cool-feature-B

where X is the result of making a new commit whose snapshot would match merge E (if they were to make merge E), but whose (single) parent is existing commit A. So the history—the list of commits enumerated by working backwards—from X is just X, then A, then whatever commits come before A.

If you run a regular git rebase master while on cool-feature-B, Git:

  1. enumerates the commits reachable from D: D, C, B, A, ...;
  2. enumerates the commits reachable from X: X, A, ...;
  3. subtracts the set in step 2 from the set in step 1, leaving D, C, B;
  4. copies those commits (in un-backwards-ized order) so that they come after X.

Note that steps 2 and 3 both use the word master to find the commits: the commits to copy, for step 2, are those that aren't reachable from master. The place to put the copies, for step 3, is after the tip of master.

But if you run:

git rebase --onto master cool-feature-A

you have Git use different items in steps 2 and 3:

  • The list of commits to copy, from step 2, comes from cool-feature-A..cool-feature-B: subtract C-B-A-... from D-C-B-A-.... That leaves just commit D.
  • The place to put the copies, in step 3, comes from --onto master: put them after X.

So now Git only copies D to D', after which git rebase yanks the name cool-feature-B over to point to D':

          D'  <-- cool-feature-B
         /
...--A--X   <-- master, origin/master
      \
       B--C   <-- cool-feature-A [, origin/cool-feature-A]
           \
            D   <-- origin/cool-feature-B

which is what you wanted.

Had they—the people in control of the GitLab repo—used a true merge or a fast-forward not-really-a-merge-at-all, this would all still work: you would have your Git enumerate D-on-backwards but remove C-on-backwards from the list, leaving just D to copy; and then Git would copy D so that it comes after either E (the true merge case) or C (the fast-forward case). The fast-forward case, "copy D so that it comes where it already is", would cleverly not bother to copy at all and just leave everything in place.

like image 161
torek Avatar answered Oct 30 '22 18:10

torek