Following up on Git: rebase onto development branch from upstream, basically the same ask as:
I have local
master
anddevelop
branches. I do all my work ondevelop
and then merge them intomaster
for releases. There is a remote branch,upstream/master
which has changes I want, but I want to rebase my changes indevelop
(which shares a common ancestor) on top of its changes and put them back intodevelop
. I've already donegit fetch upstream
.
Here is the (a bit more complicated/advanced) situation I'm facing with, starting from scratch.
- I forked an upstream, and made my own changes from its development branches (
dev/br-1
), in my own new branch (dev/br-2
), andgit push
to my own repo. - Now, upstream has advanced, both in its
master
anddevelop
branches. - The way upstream advanced its
develop
branch is viarebase
, i.e., all its own changes are put back on top after rebase. - My local repo is gone with my old machine, and I need to pick-up/git-clone from my own repo to continue with my customization.
- I want to rebase my changes in my
dev/br-2
branch (which shares a common ancestor) on top of all the upstream changes. - I've already done
git fetch upstream
and was able to rebase mymaster
with upstream's. - It is how to rebase my current
dev/br-1
from upstream, thendev/br-2
fromdev/br-1
branch that makes my head spinning nonstop.
Although it looks like a very specific case, but the principle of how to do git branching and rebasing still applies, and the answer would be very educational to the general public. Please help.
UPDATE: So I looked at @torek suggested git rebase --onto
command, like How to git rebase a branch with the onto command?, and all their refed docs, and think my problem is still one or two levels up than what I've read (because there are two repos and 5 branches involved). Here is the summary:
Situation at point#1:
A---B---C---D master (upstream)
\
E---F---G dev/br-1 (upstream)
\
H---I---J dev/br-2 (myown)
Situation at point#2&3:
A---B---C---D master (upstream)
\
E'---F'---G' dev/br-1 (upstream)
And I don't even know where should I draw my myown
branch. Here is my current situation (which may have already been messed up, as I see HEAD
might be in a weird place):
$ git log --all --decorate --oneline --graph
* 7aec18c (upstream/dev/br-1) more updates
* b83c3f8 more updates
* 4cf241f update file-a
* 200959c update file-a from main
* dc45a94 (upstream/master, master) update file-a from main
| * ce2a804 (origin/dev/br-2) update file-a
| * 0006c5e (HEAD -> dev/br-1, origin/dev/br-1) more updates
| * cdee8bb more updates
| * 85afa56 update file-a
|/
* 2f5eaaf (origin/master, origin/HEAD) add file-a
The ultimate goal is to place myown
H---I---J dev/br-2
branch on top of the newly rebased G'
, in my own repo, after catching up with upstream. I.e., in my own repo, in the end, it should look like this:
A---B---C---D rebased master (upstream)
\
E'---F'---G' rebased dev/br-1 (upstream)
\
H'---I'---J' rebased dev/br-2
How to do that?
More explain by command:
cd /tmp
mkdir upstream
cd upstream
# prepare its `master` and `dev/br-1` branches
cd /tmp
git clone upstream myfork
# prepare my own changes based on the `dev/br-1` branch into `dev/br-2`
cd /tmp/upstream
# advance its `master`
. . .
# and its`dev/br-1` branches
git checkout dev/br-1
git rebase -X theirs master dev/br-1
. . .
Now upstream has advanced, both in its master
and its develop
branches (via rebase
), and I need to pick-up from my own repo to continue with my customization.
cd /tmp
mv myfork myfork0
git clone myfork0 myfork1
cd myfork1
git remote -v
git remote add upstream /tmp/upstream
git remote -v
git fetch upstream
git rebase upstream/master
git checkout --track origin/dev/br-1
$ git remote -v
origin /tmp/myfork0 (fetch)
origin /tmp/myfork0 (push)
upstream /tmp/upstream (fetch)
upstream /tmp/upstream (push)
$ git branch -avv
* dev/br-1 0006c5e [origin/dev/br-1] more updates
master dc45a94 [origin/master: ahead 1] update file-a from main
remotes/origin/HEAD -> origin/master
remotes/origin/dev/br-1 0006c5e more updates
remotes/origin/dev/br-2 ce2a804 update file-a
remotes/origin/master 2f5eaaf add file-a
remotes/upstream/dev/br-1 7aec18c more updates
remotes/upstream/master dc45a94 update file-a from main
$ git log --all --decorate --oneline --graph
* 7aec18c (upstream/dev/br-1) more updates
* b83c3f8 more updates
* 4cf241f update file-a
* 200959c update file-a from main
* dc45a94 (upstream/master, master) update file-a from main
| * ce2a804 (origin/dev/br-2) update file-a
| * 0006c5e (HEAD -> dev/br-1, origin/dev/br-1) more updates
| * cdee8bb more updates
| * 85afa56 update file-a
|/
* 2f5eaaf (origin/master, origin/HEAD) add file-a
UPDATE2:
With above status, when I tried --rebase-merges
as suggested by @VonC, I'm getting:
$ git rebase --rebase-merges --onto master $(git merge-base dev/br-2 master) dev/br2
fatal: Not a valid object name dev/br-2
fatal: invalid upstream 'dev/br2'
$ git checkout --track origin/dev/br-2
Branch 'dev/br-2' set up to track remote branch 'dev/br-2' from 'origin' by rebasing.
Switched to a new branch 'dev/br-2'
$ git rebase --rebase-merges --onto master $(git merge-base dev/br-2 master) dev/br-2
Successfully rebased and updated refs/heads/dev/br-2.
$ git log --all --decorate --oneline --graph
* 344418c (HEAD -> dev/br-2) update file-a
* 4de3dec more updates
* 81af2ac more updates
* 1e3f9fb update file-a
| * 7aec18c (upstream/dev/br-1) more updates
| * b83c3f8 more updates
| * 4cf241f update file-a
| * 200959c update file-a from main
|/
* dc45a94 (upstream/master, master) update file-a from main
| * ce2a804 (origin/dev/br-2) update file-a
| * 0006c5e (origin/dev/br-1, dev/br-1) more updates
| * cdee8bb more updates
| * 85afa56 update file-a
|/
* 2f5eaaf (origin/master, origin/HEAD) add file-a
Here, how to rebase my current dev/br-1
from upstream, then dev/br-2
from dev/br-1
branch please
(detailed preparation can be found at https://pastebin.com/Df8VbCp2, if necessary).
OK, I might draw this horizontally as:
A
=2f5eaaf add file-a
;B
=85afa56 update file-a
; and so on. I picked the later ones to be the "primes" based on subject lines, although that might be backwards: perhaps the one labeledB
should beB'
for instance. It seems likely that despite the same subject line for commitsB
andE
, they have different patch-IDs (do different things tofile-a
), and it's hard to say whether matching up the two "more updates" commits here (calling the second one on the bottom rowC'
) was right.(Note that there's no
dev/br2
here.)But let's go back to this:
I reformatted this slightly into my own preferred style (two dashes between commits, or one if using a prime marker to indicate that it's a copy).
The commits that must be copied are precisely
E-F-G-H-I-J
(in that order).If you have all six commits in your own repository (plus of course the four
A-B-C-D
commits), then—ignoring the desired labels for a moment—all we need to do is convince Git to copy, as by cherry-picking, the six commits in question.The two options for doing this are:
git cherry-pick
, which leaves both the originals and the copies for us to find easily; orgit rebase
, which does the copying, then moves one (1) branch name around for us.Moving one branch name is insufficient. It's not particularly harmful and we could allow Git to do it, but let's just do this directly with cherry-pick. We'll start by checking out the commit onto which everything should land, creating a new branch name:
or:
(assuming
upstream/master
names commitD
). Then:or:
(again the two names used here are just ways of typing in the raw commit hash IDs without having to type in raw commit hash IDs). The two-dot syntax here means any commits reachable from the second specifier, excluding all commits reachable from the first specifier, so this means commits from
J
all the way back to the root, minus commits fromD
all the way back to the root which meansE-F-G-H-I-J
.The result of this copying would be:
Now that we have the commits we want, we merely need to place the particular labels. Since one or more of these labels will point to commit
G'
, it helps to redraw the above withH'-I'-J'
on a row of its own:The labels we want moved are:
dev/br1
, maybe; it should point toG'
;upstream/dev/br1
—but this is our copy ofupstream
'sdev/br1
, so we don't move it directly, we have the remote namedupstream
move theirs so that our Git updates our memory of their name;origin/dev/br1
: this is just likeupstream/dev/br1
;dev/br2
: this should point toJ'
; andmyown/dev/br2
or maybeorigin/dev/br2
, but once again this is our Git's memory of some other repository's branch name, so we have to convince that other Git repository to move their name.To move our own
dev/br1
, we can simply usegit branch -f
now:for instance. Since the name
copied
selects commitJ'
, and the~3
suffix means "move back three first-parents", that will select commitG'
. The-f
means force and causes our Git to move ourdev/br1
.To move
upstream
'sdev/br1
and cause ourupstream/dev/br1
to move, we now need togit push --force-with-lease
or similar toupstream
, which also assumes that we have permission (on whatever system hostsupstream
: Git itself doesn't "do" permissions, but sites like GitHub and others do, for obvious reasons). The--force-with-lease
tells our Git to verify that theirdev/br1
still points where we expect; if we're sure that this will be the case, we can use plain--force
. Either way the command is, or resembles:which uses the fact that we forced our
br1
to point toG'
.The same process applies to making the various names point to
H'
, except that now we can use the namecopied
:Once we have done all this, we can switch to
dev/br2
or whatever and delete the extra branch namecopied
. It existed only so that we had a nice simple way to find commitH'
after all the copying.The key here is to understand that the names are largely irrelevant. All that actually matters are the commits, which are identified by their hash IDs. The trick is that we find the commits using the names, so that makes the names relevant.
Alternatives
If you like, you can still do this with two
git rebase
operations. Since things are relatively simple, we don't need the fancy--onto
for the first one (but we will need it for the second):This takes our starting point, which looks like this—note that I'm assuming things about the remote-tracking names this time:
and copies
E-F-G
to new-and-improvedE'-F'-G'
, placing them after commitD
, as named byupstream/master
:Having made the three copies,
git rebase
yanked the namedev/br-1
off commitG
and made it point to commitG'
instead.Now we will separately copy
H-I-J
:Here we needed the
--onto
to tell Git:upstream/dev/br-1
backwards, i.e., don't copy commitsG
and earlier, but that's not where we want to put the copies!G'
, i.e., the newly updateddev/br-1
.The result of this is:
As with the first
git rebase
, Git did the copying of the commits found from the current branch name (i.e.,J
and back), excluding the commits we said not to copy (i.e.,G
and back), placing the copies wherever they go—this time that's separate from the "what not to copy" part—and then, having made the copies,git rebase
yanked the namedev/br-2
to point to the final copied commit (J'
).With the (local) repository's names now pointing to the copies, it's once again just a matter of using
git push --force-with-lease
orgit push --force
to get the other Git software, working with the two other repositories, to update their branch names, so that our own Git's memories in our remote-tracking names get updated.(If you can't literally force-push to
upstream
ororigin
, you can still send the updated commits, e.g., via pull requests, but someone else will have to convince the other repositories to actually move their branch names to take in the new commits.)