9

The main disadvantage of merging vs. rebasing is that merging results in a messy tree. If the master is frequently being updated, then merging in mastery every time there has been a significant update will create a whole host of unnecessary commits. Now, most of the time, there isn't actually any need for this. Consider the repository as follows:

Master A\--B--C\--D\---------E
Branch   l--m---n---o--p--q

Here n is a merge where we had to resolve a significant amount of conflicts which we want to avoid resolving again. We want to merge E back in without creating a new merge commit

So we go back to o, merge E and cherry-pick p and q on top:

Master A\--B--C\--D\--------E\
Branch   l--m---n\--o--p--q   \
Tmp               -------------o'-p-q

If this works without an error, we can then remove the old branch.

I would be surprised if I was the first person to think of this kind of workflow. Are there any git extensions that can automate this?

I've started working on a script to do this, but the main issue is selecting o. ":/^Merge" - will select o in most cases, but it would be surprised if there wasn't a better way that avoids relying on commit messages not starting with the word Merge.

Casebash
  • 114,675
  • 90
  • 247
  • 350
  • Sorry but I don't quite get the question, what is the difference between your workflow and `rebase`? – J Jiang Feb 20 '14 at 01:42
  • @ErixJiang: Sorry, my diagram was incorrect. I've now modified the diagram so that there is a merge n that we want to avoid having to deal with again – Casebash Feb 20 '14 at 01:50
  • @PaulHicks: Actually, rebasing o on top of e will require dealing with all the merge conflicts in n again – Casebash Feb 20 '14 at 02:16
  • @PaulHicks: Yes, but when you rebase, it makes you resolve them again – Casebash Feb 20 '14 at 02:31
  • Yep confirmed. I guess I hadn't noticed because of the workflows I usually use. This is avoided with both git flow and frequent bi-directional merges, so I've just not run into this problem before. – Paul Hicks Feb 20 '14 at 02:58
  • Note: `git rerere` (as in http://stackoverflow.com/a/10018939/6309) might have helped in recording the merge resolution in n, avoiding those same conflict on a next rebase. – VonC Feb 20 '14 at 07:29
  • I'm still not clear on the question as it relates to rebases. I would suggest you simply rebase. This avoids the entire problem of re-living old conflicts, and keeps your history nice and clean. You can always rebase, and then non-fasforward merge in topic branches to retain the fact that there was a merge. – eddiemoya Feb 24 '14 at 22:18
  • Having trouble finding a situation where merging E after p q or before p q would change the situation with conflicts. – Learath2 Feb 24 '14 at 22:41
  • After re-reading this, I think I understand the question better. I didn't realize you were cherry-picking. This is effectively the same as rebasing, especially if you cherry-pick with a range. I believe you are asking about how to automate your workflow rather than how to handle old conflicts, which what I and I think others initially thought you meant. Can you verify this is what you meant and I will then try to answer. – eddiemoya Feb 24 '14 at 23:49
  • Why would you automate this? It is only three git commands, and you can customize it when you run it by hand? I move commits around like this a ton every day and it takes very little time. – onionjake Feb 25 '14 at 22:10

3 Answers3

7

edit: To retroactively enable rerere, see here.. The script in this answer doesn't turn rerere on and will behave differently than rerere would in the presence of further changes, in E's history (see below), to n...D-conflicted hunks resolved in o. rerere does an n...E merge and applies any n...D resolutions that are still valid, while this script does an standard o...E merge (but replaces parent o with o)

Copying the graph for reference:

      before                                 desired after
Master A\--B--C\--D\....E             \  Master A\--B--C\-D...E\
Branch   l--m---n---o--p--q         /    Branch   l--m---n------o'--p'--q'

rerere will thus bypass E-reverted n...D conflicts, while the script will treat the reversion as a further change. Both scripts produce the "desired after" graph above, but they get there by different routes.

An hour's trying hasn't produced any more comprehensible further textual description of the differences than simply looking at the graphs and deducing.

It's not clear to me that either rerere's or the script's behavior is always better.


When I do

go back to o, merge E

with

git checkout o
git merge E

I get

Master A\--B--C\--D\---------E
Branch   l--m---n---o\-p--q   \
HEAD                  `--------o'

with o for o''s parent. You do want the o' content (to preserve any conflict resolutions you'd already applied in o), but you want it to have n and E as parents, not o and E. That's actually fairly easy.

So the complete procedure is,

#!/bin/sh

# 1. identify the most recent merge on $1
# 2. verify that it's from $2
# 3. update the merge to $2's current state, 
# 4. preserving any conflict resolutions that were already done
# 5. and cherry-pick everything since.

# most recent merge in $1
update=`git rev-list -1 --min-parents=2 $1`

# verify most recent merge is from correct merge base
test "`git merge-base $1 $2`" = "`git rev-parse $update^2`" \
|| { echo "most recent merge isn't of $1 from $2"; exit 1; }

set -e  # exit immediately on error
git checkout $update
git merge $2 
git checkout $(
      # this makes a new commit with HEAD's commit message and tree
      # but with $update^ and $2 as parents
      git cat-file -p HEAD \
      | sed 1,/^$/d \
      | git commit-tree -p $update^ -p $2 HEAD^{tree}
   )
test $update = `git rev-parse $1` || git cherry-pick $update..$1
git checkout -B $1

If you're willing to lose any conflict resolutions done in o you can instead do (from the set -e)

set -e
git checkout $update^
git merge $2
test $update = `git rev-parse $1` || git cherry-pick $update..$1
git checkout -B $1

If doing work in a subshell and exiting from that to continue isn't any trouble you can make this much less fragile by doing

git merge $2 \
|| { echo "Resolve conflicts and \`exit\`, or \`exit 1\` to not continue"; $SHELL; }

edit: update to handle updating the merge when there's nothing to cherry-pick.

Community
  • 1
  • 1
jthill
  • 55,082
  • 5
  • 77
  • 137
  • Nice script. +1 The `merge` + `commit-tree` approach is probably easier than a rebase. – VonC Feb 22 '14 at 10:41
3

As I mentioned in the comments, a git rerere activated when created n would have helped avoiding to deal with the same conflicts when doing a:

git rebase --onto E n q

I've started working on a script to do this,

Speaking of script, the most sophisticated one to handle a complex rebase is called:

git-imerge -- incremental merge and rebase for git (by Michael Haggerty)

See a comparison matrix here.

That will keep the size of the conflicts to a minimum (and don't have to rely on git rerere)

Community
  • 1
  • 1
VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250
  • I wasn't aware of the `-onto` option for rebase - I'll have to give it a go. I'll have to keep imerge in mind too – Casebash Feb 22 '14 at 11:40
1

You're getting the same old conflicts because you should have been rebasing the branch to the trunk, rather than merging stuff from the trunk into the branch (which is a wrong-way merge).

If point "n" had been a rebase, then it would be done. l-m-n would have been replayed on top of C, and so the branch would start from C. The changes before C wouldn't have to be rebased any more.

When you're ready for the branch to go into the trunk, then you rebase it one more time, so that the branch starts at the HEAD revision of trunk. At that point, the branch can replace the trunk: you can check out trunk and reset to the branch.

Kaz
  • 55,781
  • 9
  • 100
  • 149
  • Repeatedly rebasing an ever-longer sequence of commits can get slow, and if the branch isn't going to be merged back (a private branch, say, or swap the labels and this is merging from a staging branch), what does the rebase get you? Have a look at the git mainline for a successful example. – jthill Feb 24 '14 at 14:35
  • @jthill What the rebase gets you is new commits from mainline so that your branch starts later, and the commits before that don't have to be considered any more. However, it's a non-fast-forward change to your branch. – Kaz Feb 24 '14 at 19:33