Reputation: 5844
Suppose I have the following history:
--E <- branch y
/
A---B---C---D <- branch x
I want to modify commit B
, e.g. by adding a commit B2
and squashing it into B
using git rebase -i
.
The history should then look like this:
--E' <- branch y
/
A---B'--C'--D' <- branch x
My problem is: How do I do this?
I tried committing it after E
or D
and rebasing, but this results in:
--B'--C'--E' <- branch y
/
A---B---C---D <- branch x
A workaround is to rebase E
onto D
; then I can go to E'
, add and squash B2
, resulting in
-E' <- branch y
/
A---B'--C'--D'
\
--B---C---D <- branch x
Where I can make branch x point to D'
:
-E' <- branch y
/
A--B'--C'--D' <- branch x
Which is almost similar to what I want to achieve, but feels a bit hacky because I'm manually moving the branch pointer. (The rebase is acceptable in my special case, but I'm wondering about the general case.) If there is no easy solution to the main question, I'm wondering whether this workaround is possible without manually moving the branch pointer.
Additional info: None of the respective commits have been pushed yet.
Upvotes: 3
Views: 114
Reputation: 1945
How to modify far commit and do not lose following commits
force
git reset B_HASH
git commit B2 --amend
git cherry-pick C_HASH
git checkout branch_y
git merge branch_x
git push origin branch_y
git checkout branch_x
git cherry-pick D__HASH
git push origin branch_x -f
UPDATE
Additional info: None of the respective commits have been pushed yet.
If so then you do not need all steps with push
git commit --amend
Replace the tip of the current branch by creating a new commit. The recorded tree is prepared as usual (including the effect of the -i and -o options and explicit pathspec), and the message from the original commit is used as the starting point, instead of an empty message, when no other message is specified from the command line via options such as -m, -F, -c, etc. The new commit has the same parents and author as the current one (the --reset-author option can countermand this).
git cherry-pick
Given one or more existing commits, apply the change each one introduces, recording a new commit for each. This requires your working tree to be clean (no modifications from the HEAD commit).
git push -f
Usually, the command refuses to update a remote ref that is not an ancestor of the local ref used to overwrite it. Also, when --force-with-lease option is used, the command refuses to update a remote ref whose current value does not match what is expected.
This flag disables these checks, and can cause the remote repository to lose commits; use it with care.
Note that --force applies to all the refs that are pushed, hence using it with push.default set to matching or with multiple push destinations configured with remote.*.push may overwrite refs other than the current branch (including local refs that are strictly behind their remote counterpart). To force a push to only one branch, use a + in front of the refspec to push (e.g git push origin +master to force a push to the master branch). See the ... section above for details.
Upvotes: 1
Reputation: 387607
A simple solution for this is to perform an interactive rebase on one of the branches first, make our changes, and then rebase the other branch onto the new C'
:
git checkout branch_x
git rebase -i A
# mark commit B as edit, modify it, and complete the rebase
# rebase branch_y to be based on the new C' instead of C
git rebase --onto C' C branch_y
Because we tell branch_y
which is originally based on C
to get rebased onto the new C'
, we restore the original branching structure of the branches with regards to the modified commits.
Note that we need to know the commit hashes for both C
and the new C'
here. We have to look them up after the interactive rebase on the first branch before we can execute that second rebase command.
Another solution, which avoids having to know the commit hashes, is to do a merge-preserving interactive rebase here. Note that this requires you to first merge those two branches. But we only do this for Git to keep the branch relationship, so afterwards, we can get rid of that merge again.
So to do this, we start on any branch, merge the other in. Then we perform a merge-preserving interactive rebase to modify the commit B
(in whatever way we want). Afterwards, Git will replay those branches correctly (including the merge), so we end up with this history:
A -- B' -- C' -- D' ---- M' ← branch
\ /
\ /
E' ------
Then we can move our branch pointers to the updated commits D'
and E'
and we’re done.
git checkout branch_x
git merge branch_y
# merge preserving interactive rebase
git rebase -p -i A
# mark commit B as edit, modify it, and complete the rebase
# we’re now on M', so move branch pointers to the new commits
git update-ref refs/heads/branch_y HEAD^2
git reset --hard HEAD^
The history should now look like this:
A -- B' -- C' -- D' ← branch_x
\
\
E' ← branch_y
Unfortunately, we have to manually move the branch pointers here, because we can only ever rebase one branch at a time. So because we already rebased the history for the other branch (because of the merge), we still have to move its pointer. So we just accept that we have to reset branches here manually (which is not really a bad thing anyway).
Upvotes: 1
Reputation: 241848
Using --fixup
and --autosquash
plus rebase --onto
:
#!/bin/bash
set -eu
t-a-c () {
touch "$1"
git add "$1"
git commit -m "$1"
}
cd "$1"
git init
t-a-c A
git checkout -b x
git branch -d master
t-a-c B
t-a-c C
git branch y
t-a-c D
git checkout y
t-a-c E
git checkout @~2
git checkout -b z
echo "B'" > B
git add B
git commit --fixup @
git checkout x
git rebase --onto z z~
git rebase -i --autosquash z~2
git checkout y
git rebase --onto x~ x~2
git branch -D z
git log --oneline --graph --decorate --all
Output:
* a92a035 (HEAD -> y) E
| * d5c169a (x) D
|/
* 0a2da9c C
* 2b0087e B
* b395d3a A
Upvotes: 1
Reputation: 537
git rebase -i
on the branch X and edit your commit B(set e
next to the commit, use git commit --amend
and then git rebase --continue
)
(when you are on the branch Y, use git rebase Y
)
(just reset the last two commits and git cherry-pick
the E commit)
Upvotes: 1