260

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?

Adam Dymitruk
  • 124,556
  • 26
  • 146
  • 141
Ondra Žižka
  • 43,948
  • 41
  • 217
  • 277

11 Answers11

163

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
Adam Dymitruk
  • 124,556
  • 26
  • 146
  • 141
  • 7
    I like this RISC-like approach more :) Will try. Thanks. – Ondra Žižka Oct 13 '11 at 03:35
  • 11
    I wonder why it does not work for me, in a [slightly different scenario](http://i.imgur.com/zy8aHSp.png). I want *group* skip the *pep8* and be based on *master*. `git rebase temp` (when on *group*) gives up with "Current branch groups is up to date.". – Alois Mahdal Jun 25 '14 at 15:32
  • 9
    This solution won't work for the scenario where the topic has already been rebased onto master, but you want to rebase it on an ancestor to master. In that case you must use `git rebase --onto ` so that you can specify the commit. – mirzmaster Jul 16 '18 at 15:45
  • 1
    This is by far the easiest option if you want to rebase onto the same commit the branch is based on. – ash Jul 18 '18 at 15:48
  • 1
    I seems to work, but the GitLab says something else. If I had 10 commits behind, 5 commits ahead, my expectation was to have 8 commits behind, 5 commits ahead after getting 2 commits. But instead of this it adds more commits to those 5. – ROMANIA_engineer Apr 30 '19 at 08:45
  • But this approach is rebasing from the head, i mean D has been rebased with C commit of master branch. – Ashish-BeJovial Nov 27 '19 at 06:34
  • What to do whis 'Current branch `branch-name` is up to date.'? – Egor Okhterov Jan 17 '20 at 07:53
  • @ROMANIA_engineer Give it some time. Platforms like GitLab and GitHub don't process changes in a repo's history right away. On GH for example it may take like 2 days or so. I once had the same problem. – bkis Oct 14 '20 at 11:22
  • Why would one want to avoid `--into` option? – Top-Master Feb 10 '22 at 06:37
96

You can even take a direct approach:

git checkout topic
git rebase <commitB>
morten.c
  • 3,414
  • 5
  • 40
  • 45
r0hitsharma
  • 1,592
  • 12
  • 16
  • 8
    For me, this doesn't actually do what's intended. As far as I can tell, it tries to rebase onto the "last common ancestor" of `topic` and `commitB`. – Dan Lenski Jan 26 '15 at 18:20
  • 5
    @DanLenski, that isn't how rebase works.Quoting the [docs](http://git-scm.com/book/en/v2/Git-Branching-Rebasing), `It works by going to the common ancestor of the two branches (the one you’re on and the one you’re rebasing onto), getting the diff introduced by each commit of the branch you’re on, saving those diffs to temporary files, resetting the current branch to the same commit as the branch you are rebasing onto, and finally applying each change in turn.` I tried it again now and seemed to work just fine. – r0hitsharma Jan 29 '15 at 09:53
  • 2
    Super easy and works! I now have: commitB_from_master->topicCommit1->topicCommit2. – Martin Konicek Nov 17 '16 at 18:36
  • It didn't work for me. Before this, GitLab said "n commits ahead". And now, it says "m commits ahead" where `m > n`. – ROMANIA_engineer Apr 30 '19 at 12:04
92

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).
Iulian Onofrei
  • 9,188
  • 10
  • 67
  • 113
Adam Dymitruk
  • 124,556
  • 26
  • 146
  • 141
  • 3
    `D` and `D^` would be hash of the last and next-to-last commit of "topic"? – Ondra Žižka Oct 13 '11 at 03:33
  • 66
    The syntax is like `git rebase --onto `. See [Setting git parent pointer to a different parent](http://stackoverflow.com/questions/3810348/setting-git-parent-pointer-to-a-different-parent/3810400#3810400). In your case, is B, and is A. – jsz Oct 13 '11 at 09:11
  • 9
    I always use the 3 arguments: desitnation, start and end of commits to rebase. – Adam Dymitruk Nov 16 '12 at 01:23
  • 22
    This worked for me: `git rebase --onto master` –  Jan 21 '16 at 08:08
  • 6
    @jsz's comment is correct, contrary to Simon South's comment, it's the other way around: `git rebase --onto master ` and for OP `git rebase --onto B A`. – gaborous Feb 10 '16 at 00:26
  • What happens if `D` has 2 parents as if it were a merge commit? – Mihai May 04 '18 at 17:40
  • `--onto` was definitely the key. I was trying `git rebase 0320dd` but it was not working. after inserting `git rebase --onto 0320dd` I got my changes too! – Serhat Jan 31 '22 at 11:55
  • 1
    @Mihai, I suggest reading the documentation on git-rebase for that situation. By default, rebase ignores merge commits. Rebasing merge commits is...interesting...and can produce unexpected results under certain circumstances. Hence why I urge you to check out the documentation, which provides far more extensive material on that subject than can be covered adequately in this comment or in an answer that won't be too long and quite possibly incorrect. – fourpastmidnight Feb 15 '23 at 19:54
42

The comment by jsz above saved me tons of pain, so here's a step-by-step recipie based on it that I've been using to rebase/move any commit on top of any other commit:

  1. Find a previous branching point of the branch to be rebased (moved) - call it old parent. In the example above that's A
  2. Find commit on top of which you want to move the branch to - call it new parent. In the exampe that's B
  3. You need to be on your branch (the one you move):
  4. Apply your rebase: git rebase --onto <new parent> <old parent>

In the example above that's as simple as:

   git checkout topic
   git rebase --onto B A
Nestor Milyaev
  • 5,845
  • 2
  • 35
  • 51
  • 10
    **This should be the correct answer.** Except that I use `git rebase --onto B master`, see [my answer](https://stackoverflow.com/a/54224880/539149) for a more thorough explanation. – Zack Morris Jan 16 '19 at 20:30
  • It didn't work fine for me. I chose 2 consecutive commits (the last one from master that was in the current branch and the first one from master that was not in the current branch). I started with 100 behind - 10 ahead and instead of having 99 behind - 10 ahead, now I have 105 behind - 13 ahead. – ROMANIA_engineer Apr 30 '19 at 12:10
  • Sorry to hear it didn't work. Sounds like your branches diverged quite a bit - I'd suggest squashing first before trying to rebase branches with this many differences. – Nestor Milyaev May 01 '19 at 13:09
30

Topic Solution

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

Rebase Any String of Commits to a Target Commit

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.

Interesting Examples and Results

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

CapinWinky
  • 748
  • 7
  • 8
7

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.


You might be thinking what will happen if I mention SHA1 of 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.

Number945
  • 4,631
  • 8
  • 45
  • 83
5

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.

Zack Morris
  • 4,727
  • 2
  • 55
  • 83
5

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-picking 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>.

Ondra Žižka
  • 43,948
  • 41
  • 217
  • 277
4

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".
malat
  • 12,152
  • 13
  • 89
  • 158
  • 1
    same here, several files had conflicts when I used the 2 most popular answers (ie by r0hitsharma and Dymitruk) – Oliver Oct 02 '18 at 19:54
0
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: set REF_HEAD_DETACH in checkout_up_to_date()

Reported-by: Michael McClimon
Signed-off-by: John Cai

"git rebase A B"(man) where B is not a commit should behave as if the HEAD got detached at B and then the detached HEAD got rebased on top of A.

A bug however overwrites the current branch to point at B, when B is a descendant of A (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 the head_name of rebase_options to NULL.
This value gets passed down this call chain through the branch member of reset_head_opts also getting set to NULL all the way to update_refs().

Then update_refs() checks ropts.branch to decide whether or not to switch branches.
If ropts.branch is NULL, it calls update_ref() to update HEAD.
At this point however, from rebase's point of view, we want a detached HEAD.
But, since checkout_up_to_date() does not set the RESET_HEAD_DETACH flag, the update_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 in checkout_up_to_date if B is not a valid branch, so that once reset_head() calls update_refs(), it calls update_ref() with REF_NO_DEREF which updates HEAD directly intead of deferencing it and updating the branch that HEAD points to.

VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250
-3

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.

Talha Ashraf
  • 1,201
  • 2
  • 14
  • 19