anderas
anderas

Reputation: 5844

How to change a commit in common history?

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

Answers (4)

Maksym Semenykhin
Maksym Semenykhin

Reputation: 1945

How to modify far commit and do not lose following commits

  1. Rollback to B
  2. Implimed B2 commit to B
  3. Apply C commit
  4. Stitch to branch_y
  5. merge new B2 and C commits
  6. push updated branch
  7. switch back to branch_x
  8. Apply D commit
  9. Push branch with 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

poke
poke

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

choroba
choroba

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

tuhaj
tuhaj

Reputation: 537

  1. use 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 )

  1. rebase your branch Y with branch X

(when you are on the branch Y, use git rebase Y)

  1. remove the commit D

(just reset the last two commits and git cherry-pick the E commit)

Upvotes: 1

Related Questions