As far as I understand you were in a similar situation as I have right now:
A mixture of changes have been committed to the same branch, but should be
pulled apart into separate branches.
Current state:
... -- A (master) -- C1 -- B1 -- B2 -- C2 -- B3 -- C3 -- B4 (devel)
Wanted state:
... -- A (master) -- B1’ -- B2’ -- B3’ -- B4’ (branch-B)
\
`---------- C1’ -- C2’ -- C3’ (branch-C)
In my case, this state was achieved after messy experimentation by
performing a soft-reset to master
, and committing changes line by line
with git gui
, flagging each change with a commit message A
or B
. So
let’s assume such preparation has already happened, and the commits An
,
Bn
are already cleanly separated.
Cherry-picking manually could end up being error prone, as for long
sequences of commits it is easy to miss one. So instead we use git rebase -i
on a temporary branch, to be on the safe side.
>> git checkout devel
>> git checkout -b tmp
... -- A (master) -- C1 -- B1 -- B2 -- C2 -- B3 -- C3 -- B4 (devel, tmp, HEAD)
>> git rebase -i master
This will open a rebase-task file. It defaults to
pick 552fe03 C1
pick cbf327a B1
pick bd1ca26 B2
pick 95320e6 C2
pick 7f21156 B3
pick 910a6fe C3
pick bfda579 B4
What this does is essentially “reset branch to master
, then perform
commands listed in rebase file”. With the default setting, it essentially
rebuilds the branch, leaving everything unchanged.
1. Reordering commits.
So instead we reorder the commits.
pick cbf327a B1
pick bd1ca26 B2
pick 7f21156 B3
pick bfda579 B4
pick 552fe03 C1
pick 95320e6 C2
pick 910a6fe C3
Depending on what changes have been made, merge conflicts can occur, which
have to be resolved before rebase
can be continued with git rebase --continue
.
It can be useful to move only one commit at a time, repeating git rebase -i master
several times; Otherwise you risk losing work, if you find
yourself having to abort the rebase.
The result will be:
... -- A (master) -- C1 -- B1 -- B2 -- C2 -- B3 -- C3 -- B4 (devel)
\
`-- B1 -- B2 -- B3’ -- B4’ -- C1’ -- C2’ -- C3’ (tmp, HEAD)
2. Create the branches.
We can now create the new branches,
>> git branch branch-C tmp
>> git branch branch-B <hash-of-B4’>
Giving a state
... -- A (master) -- -- B1 -- B2 -- C1 -- B3 -- C2 -- C3 -- B4 (devel)
\
`-- B1’ -- B2’ -- B3’ -- B4’ (branch-B) -- C1’ -- C2’ -- C3’ (tmp, branch-C, HEAD)
3. Pull them apart with another rebase.
As described in this answer
to
Split a git branch into two branches?,
we can now use a rebase command of the form
>> git rebase --onto NEW_PARENT OLD_PARENT BRANCH_TO_MOVE
in this case:
>> git rebase --onto master branch-B branch-C
... -- A (master) -- B1 -- B2 -- C1 -- B3 -- C2 -- C3 -- B4 (devel)
|
|`-- B1’ -- B2’ -- B3’ -- B4’ (branch-B) -- C1’ -- C2’ -- C3’ (tmp)
|
`-- C1’’-- C2’’-- C3’’ (branch-C, HEAD)
If all has worked, all that is left is to force-remove the deprecated/temporary branches (normal delete with -d
doesn’t work here).
>> git branch -D devel
>> git branch -D tmp
... -- A (master) -- B1’ -- B2’ -- B3’ -- B4’ (branch-B)
\
`---------- C1’’-- C2’’-- C3’’ (branch-C, HEAD)
Without temporary branches.
Of course you don’t have to use temporary branches. Then the commands would be
>> git checkout devel
>> git rebase -i master # for reordering
>> git branch -m branch-C # rename ‘devel’ to ‘branch-C’
>> git branch branch-B <hash-of-B4’>
>> git rebase --onto master branch-B branch-C
But rebases can fail and it is easier to recover when using branches,
then having to reconstruct the correct state from git reflog
.