Reputation: 46796
I'd like to rebase to a specific commit, not to a HEAD of the other branch:
A --- B --- C master
\
\-- D topic
to
A --- B --- C master
\
\-- D topic
instead of
A --- B --- C master
\
\-- D topic
How can I achieve that?
Upvotes: 305
Views: 518922
Reputation: 6595
The comment by jsz above saved me tons of pain, so here's a step-by-step recipe based on it that I've been using to rebase/move any commit on top of any other commit:
git rebase --onto <new parent> <old parent>
In the example above that's as simple as:
git checkout topic
git rebase --onto B A
Upvotes: 56
Reputation: 1594
The git rebase --onto B_hash A_hash D_hash
answer is definetly a correct one, but in this case you also get a detached head.
In your case it's better to do it by explicitly naming a branch. Assuming the name is topic
:
git rebase --onto B_hash A_hash topic
You could also create a branch on a start (B), e.g. name it b-branch
and end up with:
git rebase --onto b-branch A_hash topic
Having a branch makes your life much easier for some follow-up commands, like exporting the diff (git diff b-branch topic > diff-after.txt
) for comparison to the one before rebasing.
Upvotes: 0
Reputation: 46796
Adding to the answers using --onto
:
I never learned the order of the arguments by heart, so I wrote this little helper script:
Git: Rebase a (sub)branch from one base to another, leaving the other base's commits.
Usage:
moveBranch <branch> from <previous-base> to <new-base>
Which does this:
git rebase --onto "$ONTO" "$FROM" "$WHAT"
Besides that, one more solution usable for similar purposes, is cherry-pick
ing a streak of commits:
git co <new-base>
git cherry-pick <previous-base>..<branch>
git branch -f branch
Which has more less the same effect. Note that this syntax SKIPS the commit at <previous-branch>
itself, so it cherry-picks the next and the following up to, including, the commit at <branch>
.
Upvotes: 6
Reputation: 129526
Use the "onto" option:
git rebase --onto master^ D^ D
OR
git rebase --onto <commitB> <commitA> <commitD>
The 3 last arguments mean:
- destination (new-parent, here it's commitB),
- start-after (current-parent, parent of first commit to be moved),
- and end-inclusive (last commit to be moved).
Upvotes: 108
Reputation: 1323553
git rebase --onto <commitB> <commitA> <commitD>
Actually, using as last parameter a commit (<commitD>
) instead of a branch name (topic
), should create a detached branch:
A --- B --- C master
\ \
\ -- D' <detached HEAD>
\
\-- D topic
Except... it would not in some case, before Git 2.36:
"git rebase $base $non_branch_commit
"(man), when $base
is an ancestor or the $non_branch_commit
, modified the current branch, which has been corrected with Git 2.36 (Q2 2022).
See commit bdff97a, commit 77ab58c (18 Mar 2022) by John Cai (john-cai
).
(Merged by Junio C Hamano -- gitster
-- in commit f818536, 29 Mar 2022)
rebase
: setREF_HEAD_DETACH
incheckout_up_to_date()
Reported-by: Michael McClimon
Signed-off-by: John Cai
"
git rebase A B
"(man) whereB
is not a commit should behave as if the HEAD got detached atB
and then the detached HEAD got rebased on top ofA
.A bug however overwrites the current branch to point at
B
, whenB
is a descendant ofA
(i.e. the rebase ends up being a fast-forward).
See this thread for the original bug report.The callstack from
checkout_up_to_date()
is the following:
cmd_rebase()
->checkout_up_to_date()
-> reset_head() -> update_refs() -> update_ref()When
B
is not a valid branch but an oid, rebase sets thehead_name
ofrebase_options
toNULL
.
This value gets passed down this call chain through the branch member ofreset_head_opts
also getting set toNULL
all the way toupdate_refs()
.Then
update_refs()
checks ropts.branch to decide whether or not to switch branches.
Ifropts.branch
isNULL
, it callsupdate_ref()
to update HEAD.
At this point however, from rebase's point of view, we want a detached HEAD.
But, sincecheckout_up_to_date()
does not set theRESET_HEAD_DETACH
flag, theupdate_ref()
call will deference HEAD and update the branch its pointing to.
We want the HEAD detached at B instead.Fix this bug by adding the
RESET_HEAD_DETACH
flag incheckout_up_to_date
if B is not a valid branch, so that oncereset_head()
callsupdate_refs()
, it callsupdate_ref()
withREF_NO_DEREF
which updates HEAD directly intead of deferencing it and updating the branch that HEAD points to.
Upvotes: 0
Reputation: 129526
You can avoid using the --onto parameter by making a temp branch on the commit you like and then use rebase in its simple form:
git branch temp master^
git checkout topic
git rebase temp
git branch -d temp
Upvotes: 178
Reputation: 824
The correct command to answer the posted question could be any of the following (assuming branch topic
is already checked out):
git rebase --onto B master
git rebase --onto master~1 master
git rebase --onto B A
git rebase --onto B C
git rebase --onto B
If topic
is not checked out, you simply append topic
to the command (except the last one) like so:
git rebase --onto B master topic
Alternatively, check out the branch first with:
git checkout topic
The basic form of the command we need, cribbed from the documentation, is:
git rebase --onto <Target> [<Upstream> [<Branch>]]
<Branch>
is optional and all it does is checks out the branch specified before executing the rest of the command. If you've already checked out the branch you want to rebase, then you don't need this. Note that you must have specified <Upstream>
in order to specify <Branch>
or git will think you are specifying <Upstream>
.
<Target>
is the commit we will attach our string of commits to. When providing a branch name, you are simply specifying the head commit of that branch. <Target>
can be any commit that won't be contained in the string of commits being moved. For example:
A --- B --- C --- D master
\
\-- X --- Y --- Z feature
To move the entire feature branch, you can not select X
, Y
, Z
, or feature
as the <Target>
since those all are commits inside the group being moved.
<Upstream>
is special because it can mean two different things. If it is a commit that is an ancestor of the checked out branch, then it serves as the cut point. In the example I provided, this would be anything that isn't C
, D
, or master
. All commits after <Upstream>
until the head of the checked out branch are the ones that will be moved.
However, if <Upstream>
is not an ancestor, then git backs up the chain from the specified commit until if finds a common ancestor with the checked out branch (and aborts if it can't find one). In our case, an <Upstream>
of B
, C
, D
, or master
will all result in commit B
serving as the cut point. <Upstream>
is itself an optional command and if it is not specified, then git looks at the parent of the checked out branch which is the equivalent of entering master
.
Now that git has selected the commits it will cut and move, it applies them in order to <Target>
, skipping any that are already applied to target.
Using this starting point:
A --- B --- C --- D --- E master
\
\-- X --- Y --- Z feature
git rebase --onto D A feature
Will apply commits B
, C
, X
, Y
, Z
to commit D
and end up skipping B
and C
because they already have been applied.
git rebase --onto C X feature
Will apply commits Y
and Z
to commit C
, effectively deleting commit X
Upvotes: 32
Reputation: 4940
A simpler solution is git rebase <SHA1 of B> topic
. This works irrespective of where your HEAD
is.
We can confirm this behaviour from git rebase doc
<upstream>
Upstream branch to compare against. May be any valid commit, not just an existing branch name. Defaults to the configured upstream for the current branch.
topic
too in the above command ?
git rebase <SHA1 of B> <SHA1 of topic>
This will also work but rebase then won't make Topic
point to new branch so created and HEAD
will be in detached state. So from here you have to manually delete old Topic
and create a new branch reference on new branch created by rebase.
Upvotes: 7
Reputation: 4823
Since rebasing is so fundamental, here's an expansion of Nestor Milyaev's answer. Combining jsz's and Simon South's comments from Adam Dymitruk's answer yields this command which works on the topic
branch regardless of whether it branches from the master
branch's commit A
or C
:
git checkout topic
git rebase --onto <commit-B> <pre-rebase-A-or-post-rebase-C-or-base-branch-name>
Note that the last argument is required (otherwise it rewinds your branch to commit B
).
Examples:
# if topic branches from master commit A:
git checkout topic
git rebase --onto <commit-B> <commit-A>
# if topic branches from master commit C:
git checkout topic
git rebase --onto <commit-B> <commit-C>
# regardless of whether topic branches from master commit A or C:
git checkout topic
git rebase --onto <commit-B> master
So the last command is the one that I typically use.
Upvotes: 6
Reputation: 1245
There is another way of doing it or if you wish to move back to more than just one commit.
Here is a an example to move back to n
number of commits:
git branch topic master~n
For the sake of this question, this can also be done:
git branch topic master~1
The command works perfectly on git version 2.7.4
. Haven't tested it on any other version.
Upvotes: -3
Reputation: 12489
I've used a mixture of solutions described above:
$ git branch temp <specific sha1>
$ git rebase --onto temp master topic
$ git branch -d temp
I found it much easier to read and understand. The accepted solution lead me to a merge conflict (too lazy to fix by hand):
$ git rebase temp
First, rewinding head to replay your work on top of it...
Applying: <git comment>
Using index info to reconstruct a base tree...
M pom.xml
.git/rebase-apply/patch:10: trailing whitespace.
<some code>
.git/rebase-apply/patch:17: trailing whitespace.
<some other code>
warning: 2 lines add whitespace errors.
Falling back to patching base and 3-way merge...
Auto-merging pom.xml
CONFLICT (content): Merge conflict in pom.xml
error: Failed to merge in the changes.
Patch failed at 0001 <git comment>
The copy of the patch that failed is found in: .git/rebase-apply/patch
When you have resolved this problem, run "git rebase --continue".
If you prefer to skip this patch, run "git rebase --skip" instead.
To check out the original branch and stop rebasing, run "git rebase --abort".
Upvotes: 4
Reputation: 1762
You can even take a direct approach:
git checkout topic
git rebase <commitB>
Upvotes: 112