11

I have the following situation:

  1. I made some commits to my local repository, and then a huge merge of another branch (~150 commits) into the master. It had a lot of conflicts in it.

  2. Now, I want to move a commit I made before the merge to be after it before pushing.

Normally, I would use rebase -i for it.

Unfortunately, the default behavior is to break the one-merge-commit I did that actually added 150 more commits to master into separate commits (I understand it's like if I were to use rebase instead of merge to begin with)—which is bad behavior for me for several reasons.

I was very happy to discover the -p flag for rebase, which preserves merges. Unfortunately, this actually applied the same merge again and forgot all about my hard work in conflict resolving. Again—bad behavior!

Is there a solution for what I want? Using rebase -i after merge to re-order or edit specific commits without having to repeat my post-merge operations?

Thanks!

Michael
  • 8,362
  • 6
  • 61
  • 88
yonix
  • 11,665
  • 7
  • 34
  • 52
  • 3
    Did you have a look at [git rerere](http://www.kernel.org/pub/software/scm/git/docs/git-rerere.html)? – Sven Marnach Nov 11 '10 at 09:34
  • 3
    `git rerere` can't help you now, since it records the conflict resolution when you commit the merge... except there's a script called [rerere-train.sh](http://git.kernel.org/?p=git/git.git;a=blob;f=contrib/rerere-train.sh;hb=HEAD) in git's contrib directory which "primes" the rerere database from merge commits you've already made. – Cascabel Nov 11 '10 at 14:03

3 Answers3

19

Here's what the rerere-train.sh script I mentioned in my comment does—essentially it redoes the merge, uses your resolution, and just lets rerere see it. You could do this manually just for your single commit if you like:

git checkout <parent of merge commit>
git merge <merged commit>         # if this goes cleanly, we're done
git rerere                        # done automatically if rerere.enabled is true
git checkout <merge commit> -- .  # check out the files from the result of the merge
git rerere                        # done automatically if rerere.enabled is true
git reset --hard                  # wipe away the merge

# and you'd want to follow with git checkout <branch> to return to where you were

But you could also just set rerere.enabled to true, and do those steps minus the direct calls to git rerere—and you'd be set in the future, with rerere automatically being run whenever you resolve conflicts. This is what I do—it's awesome.

If you want to run the script directly, you'll probably want to run it with arguments like rerere-train.sh ^<commit before the merge> <current branch>. (The ^commit notation means "don't walk past this into the history", so it won't bother doing this for all the merge commits in your repo.)

However you get rerere to do its thing, you should end up with the desired resolution recorded. That means you can go ahead and do your rebase -i, and when you run into the conflict, rerere will REuse the REcorded REsolution. Just a heads-up: it still leaves the files marked as conflicted in the index, so that you can inspect them and make sure that what it did makes sense. Once you do, use git add to check them in as if you'd resolved the conflicts yourself, and go on as usual!

The git-rerere manpage contains a very nice, lengthy description of normal use of rerere, which doesn't ever involve actually calling rerere—it's all done automatically. And a note it doesn't emphasize: it's all based on conflict hunks, so it can reuse a resolution even if the conflict ends up in a completely different place, as long as it's still the same textual conflict.

Michael
  • 8,362
  • 6
  • 61
  • 88
Cascabel
  • 479,068
  • 72
  • 370
  • 318
  • Wow, this looks like serious git-fu! – Benjol Nov 11 '10 at 14:33
  • @Benjol: Conflict resolution is serious business! (But it's not really scary - just enable rerere in the config and let the magic happen.) – Cascabel Nov 11 '10 at 14:40
  • @Jez: As are essentially all the links to manpages posted here before the kernel.org shutdown last fall. I don't think anyone has any intention of trying to find *all* of them at this point. – Cascabel Mar 28 '12 at 15:44
  • 1
    To run `rerere-train` on a range of commits use the `..` notation i.e. `^..`. The script (https://github.com/git/git/blob/master/contrib/rerere-train.sh) just passes the args to `git rev-list --parents` and the syntax specified in the answer will not limit the range as described. – Raman Jun 07 '17 at 14:01
  • I hope the parent commit refers to the parent of last commit in the target branch, where merge happened...There are two parents for a merge commit, including the one in the source branch. – cryptickey May 24 '21 at 11:27
1

I've made a script to do this here. See the open issues for known limitations.

You'll need to first install rerere-train.sh onto your PATH. On Fedora this can be done with:

    install "$(rpm -ql git|grep '/rerere-train.sh$')" ~/bin
Robin Green
  • 32,079
  • 16
  • 104
  • 187
1

I used rerere-train.sh from user @Jefromi's answer. However I had this error:

$ ../rerere-train.sh HEAD
C:\Program Files\Git\mingw64/libexec/git-core/git-sh-setup: line 6: .: git-sh-i18n: file not found

From this http://kernel.opensuse.org/cgit/kernel-source/commit/?h=linux-next&id=d52b9f00588bb18d7f7a0e043eea15e6c27ec40c I saw this:

Michal Marek

The git-sh-setup helper has apparently never been meant to be used by scripts outside of git. This became apparent with git 2.10:

After downloading an older version - 2.3.4 portable - I managed to make rerere-train.sh work.

sashoalm
  • 75,001
  • 122
  • 434
  • 781