marcosh
marcosh

Reputation: 9008

git rebase all intermediate branches

Suppose I have a set of BRANCHES (not commits) A, B, C and D like the following

A
|
B   C
|   |
 \ /
  D

I would like to rebase branch A over branch C.

Doing a normal rebase would keep branch B in the same place. Like this:

    A
    |
B   C
|   |
 \ /
  D

But I'd like to also move branch B keeping its relative position to branch A in the rebase.

The end (desired) result would be the following:

A
|
B
|
C
|
D

at the moment I'm doing this manually, rebasing branch A over branch C and then moving branch B. Is there an option for the git rebase command which allows me to do this with one command?

Using a simple git rebase C while being in A (or a git rebase C A) is not enough because it moves only the A branch, while the B branch remains where it is (its commits are copied, but the branch is not moved)

Upvotes: 4

Views: 1239

Answers (4)

friederbluemle
friederbluemle

Reputation: 36987

Git v2.38 (released Oct 3 2022) added a new feature --update-refs that does exactly what you are asking for. So in your case:

git rebase --update-refs C A

The option can also be enabled by default:

git config --global rebase.updateRefs true

Upvotes: 5

torek
torek

Reputation: 488003

If I render your four commits in the style I prefer for StackOverflow postings, it looks like this, with newer commits towards the right:

  C   <-- branch1
 /
D
 \
  B--A   <-- branch2

The two names—you have not told us what they are, so I just used branch1 and branch2 here—contain the raw hash ID of actual commits C and A, which are the tip commits of the two branches. (C and A stand in for real hash IDs, which are big and ugly and random-looking.)

The parent of commit A is commit B. The parent of commit B is commit D. The parent of commit C is commit D as well. (If there are commits before commit D, we cannot see them.)

You say that you would like to have:

D--C--B--A

as your final result. You cannot: commit B's parent is commit D, now and forever. You can have:

D--C--B'-A'

where B' is a copy of B but with a different hash ID because it also has a different parent (and probably a different snapshot as well). Similarly, A' must be the result of taking commits B' and A out and recommitting something slightly different: the parent if A' is B', and A' probably holds a different snapshot than A, just like B' probably holds a different snapshot than B.

Commits B and A will continue to exist, i.e., your repository will have:

D--C--B'-A'
    \
     B--A

in it. If you write down the actual hash IDs of B and A somewhere, and type them back in later, you will see that the commits continue to exist. (They will stick around for at least 30 days by default, in case you decide you want them back.)

What's missing from your final-result, however, is which names you want to have pointing to which commits. You can, if you like, have both names point to commit A', like this:

D--C--B'-A'  <-- branch1, branch2

or you can have branch2 point to A' and leave branch1 pointing to existing commit C, like this:

     B'-A'  <-- branch2
    /
D--C   <-- branch1

The set of Git commands that you can to use to achieve the final result depends on which of these final results you want.

Note that Saurabh P Bhandari's answer is equivalent to:

git checkout --detach branch2     # or git checkout <hash-of-A>
git rebase branch1

which produces:

     B'-A'  <-- HEAD
    /
D--C   <-- branch1
    \
     B--A   <-- branch1

i.e., you are detached HEAD mode in the end, with neither branch name moved.

Since there are two branch names involved—branch1 and branch2 are the two names—it takes, at a minimum, two Git commands to achieve:

D--C--B'-A'  <-- branch1, branch2
    \
     B--A   [abandoned]

as your final result, if that's what you want. A minimal set of commands—there are many command sequences that will achieve this result—is:

git checkout branch2
git rebase branch1
git push . branch2:branch1

which leaves you with your HEAD attached to the name branch2:

D--C--B'-A'  <-- branch1, branch2 (HEAD)
    \
     B--A   [abandoned]

symlink's answer, with name substitutions, amounts to:

git checkout branch2
git rebase branch1
git checkout branch1
git merge --ff-only branch2    # the --ff-only is not required here

which does the same thing but leaves HEAD attached to the name branch1:

D--C--B'-A'  <-- branch1 (HEAD), branch2
    \
     B--A   [abandoned]

The --ff-only flag ensures that if something went wrong and the commit identified by the name branch2 is not now strictly ahead of that identified by branch1, Git will produce an error message, rather than a new merge commit.

The git push . src:dst command is particularly tricky / sneaky and I don't really recommend it in general, but it illustrates a way to ask Git to fast-forward one of your names—dst—to point to any given commit, in this case src. Here we fast-forward the name branch1 to make it point to the same commit as branch2. Again, the push command has the source on the left and the destination on the right, which is why we write this as branch2:branch1.

The key to understanding all this

Git is all about commits. Each commit has a unique hash ID, and once a commit is made, no part of it can ever be changed. So if you want different commits you have to make new ones. That's usually not a big deal. That's what git rebase does: it copies some existing commits to some new-and-improved ones.

You—and Git for that matter—find commits, however, by starting with the last one in a chain, as identified by a branch name, then working backwards. (That's why commit A is the last one, instead of the first one, in each of our drawings: you put the last one at the top, and I put the last one at the right.)

Having copied a series of commits as found by a particular name, git rebase now drags the name over to point to the last of the new commits. For a single name, this is just what you want.

But, after you have copied some series of commits, if there is more than one name that you would like moved, you must move the remaining names. That's really all there is to it.

Upvotes: 0

symlink
symlink

Reputation: 12209

If A is the tip of branch feature and C is the tip of branch master:

A //feature
|
B   C //master
|   |
 \ /
  D
git checkout feature //if not on feature

git rebase master //rebase feature onto master

You can also do this on either branch and get the same result:

git rebase master feature //rebase feature onto master

The result:

A //feature
|
B
| 
C //master
|
D

To fast-forward master to point to A:

git checkout master //if not on master

git merge feature //merge master with feature

Result:

A //feature, master
|
B
| 
C 
|
D

Upvotes: -1

Saurabh P Bhandari
Saurabh P Bhandari

Reputation: 6742

In branch B, run this

git rebase C A

Upvotes: 1

Related Questions