Following up on Git: rebase onto development branch from upstream, basically the same ask as:
I have local
masteranddevelopbranches. I do all my work ondevelopand then merge them intomasterfor releases. There is a remote branch,upstream/masterwhich 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 pushto my own repo. - Now, upstream has advanced, both in its
masteranddevelopbranches. - The way upstream advanced its
developbranch 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-2branch (which shares a common ancestor) on top of all the upstream changes. - I've already done
git fetch upstreamand was able to rebase mymasterwith upstream's. - It is how to rebase my current
dev/br-1from upstream, thendev/br-2fromdev/br-1branch 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 labeledBshould beB'for instance. It seems likely that despite the same subject line for commitsBandE, 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/br2here.)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-Dcommits), 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/masternames 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
Jall the way back to the root, minus commits fromDall 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 namedupstreammove 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/br2or 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 -fnow:for instance. Since the name
copiedselects commitJ', and the~3suffix means "move back three first-parents", that will select commitG'. The-fmeans force and causes our Git to move ourdev/br1.To move
upstream'sdev/br1and cause ourupstream/dev/br1to move, we now need togit push --force-with-leaseor 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-leasetells our Git to verify that theirdev/br1still 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
br1to 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/br2or 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 rebaseoperations. Since things are relatively simple, we don't need the fancy--ontofor 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-Gto new-and-improvedE'-F'-G', placing them after commitD, as named byupstream/master:Having made the three copies,
git rebaseyanked the namedev/br-1off commitGand made it point to commitG'instead.Now we will separately copy
H-I-J:Here we needed the
--ontoto tell Git:upstream/dev/br-1backwards, i.e., don't copy commitsGand 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.,Jand back), excluding the commits we said not to copy (i.e.,Gand 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 rebaseyanked the namedev/br-2to 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-leaseorgit push --forceto 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
upstreamororigin, 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.)