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 D
—D
, 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:
- enumerates the commits reachable from
D
: D
, C
, B
, A
, ...;
- enumerates the commits reachable from
X
: X
, A
, ...;
- subtracts the set in step 2 from the set in step 1, leaving
D
, C
, B
;
- 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.
git checkout cool-feature-B && git rebase --fork-point master
? I've had very good results with--fork-point
when rebasing several dependent branches (e.g.cool-feature-A
,cool-feature-B
,cool-feature-C
) though admittedly I haven't tried it with a squashed merge. – Cowart--fork-point
uses the branch's upstream's reflog, so for this to work you'd have have had commitsB
andC
on the upstream (and have seen them go into your own repository), none of which is going to hold in the general case here. – Perplexity