Skip to content

Commit f09c9b8

Browse files
dschogitster
authored andcommitted
Teach rebase -i about --preserve-merges
The option "-p" (or long "--preserve-merges") makes it possible to rebase side branches including merges, without straightening the history. Example: X \ A---M---B / ---o---O---P---Q When the current HEAD is "B", "git rebase -i -p --onto Q O" will yield X \ ---o---O---P---Q---A'---M'---B' Note that this will - _not_ touch X [*1*], it does - _not_ work without the --interactive flag [*2*], it does - _not_ guess the type of the merge, but blindly uses recursive or whatever strategy you provided with "-s <strategy>" for all merges it has to redo, and it does - _not_ make use of the original merge commit via git-rerere. *1*: only commits which reach a merge base between <upstream> and HEAD are reapplied. The others are kept as-are. *2*: git-rebase without --interactive is inherently patch based (at least at the moment), and therefore merges cannot be preserved. Signed-off-by: Johannes Schindelin <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 68a163c commit f09c9b8

File tree

3 files changed

+151
-4
lines changed

3 files changed

+151
-4
lines changed

Documentation/git-rebase.txt

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ SYNOPSIS
99
--------
1010
[verse]
1111
'git-rebase' [-i | --interactive] [-v | --verbose] [--merge] [-C<n>]
12-
[--onto <newbase>] <upstream> [<branch>]
12+
[-p | --preserve-merges] [--onto <newbase>] <upstream> [<branch>]
1313
'git-rebase' --continue | --skip | --abort
1414

1515
DESCRIPTION
@@ -213,6 +213,10 @@ OPTIONS
213213
Make a list of the commits which are about to be rebased. Let the
214214
user edit that list before rebasing.
215215

216+
-p, \--preserve-merges::
217+
Instead of ignoring merges, try to recreate them. This option
218+
only works in interactive mode.
219+
216220
include::merge-strategies.txt[]
217221

218222
NOTES
@@ -304,6 +308,23 @@ $ git rebase -i HEAD~5
304308

305309
And move the first patch to the end of the list.
306310

311+
You might want to preserve merges, if you have a history like this:
312+
313+
------------------
314+
X
315+
\
316+
A---M---B
317+
/
318+
---o---O---P---Q
319+
------------------
320+
321+
Suppose you want to rebase the side branch starting at "A" to "Q". Make
322+
sure that the current HEAD is "B", and call
323+
324+
-----------------------------
325+
$ git rebase -i -p --onto Q O
326+
-----------------------------
327+
307328
Authors
308329
------
309330
Written by Junio C Hamano <[email protected]> and

git-rebase--interactive.sh

Lines changed: 107 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,17 @@
1010
# The original idea comes from Eric W. Biederman, in
1111
# http://article.gmane.org/gmane.comp.version-control.git/22407
1212

13-
USAGE='(--continue | --abort | --skip | [--onto <branch>] <upstream> [<branch>])'
13+
USAGE='(--continue | --abort | --skip | [--preserve-merges] [--verbose]
14+
[--onto <branch>] <upstream> [<branch>])'
1415

1516
. git-sh-setup
1617
require_work_tree
1718

1819
DOTEST="$GIT_DIR/.dotest-merge"
1920
TODO="$DOTEST"/todo
2021
DONE="$DOTEST"/done
22+
REWRITTEN="$DOTEST"/rewritten
23+
PRESERVE_MERGES=
2124
STRATEGY=
2225
VERBOSE=
2326

@@ -68,6 +71,8 @@ die_abort () {
6871
pick_one () {
6972
case "$1" in -n) sha1=$2 ;; *) sha1=$1 ;; esac
7073
git rev-parse --verify $sha1 || die "Invalid commit name: $sha1"
74+
test -d "$REWRITTEN" &&
75+
pick_one_preserving_merges "$@" && return
7176
parent_sha1=$(git rev-parse --verify $sha1^ 2>/dev/null)
7277
current_sha1=$(git rev-parse --verify HEAD)
7378
if [ $current_sha1 = $parent_sha1 ]; then
@@ -79,6 +84,75 @@ pick_one () {
7984
fi
8085
}
8186

87+
pick_one_preserving_merges () {
88+
case "$1" in -n) sha1=$2 ;; *) sha1=$1 ;; esac
89+
sha1=$(git rev-parse $sha1)
90+
91+
if [ -f "$DOTEST"/current-commit ]
92+
then
93+
current_commit=$(cat "$DOTEST"/current-commit) &&
94+
git rev-parse HEAD > "$REWRITTEN"/$current_commit &&
95+
rm "$DOTEST"/current-commit ||
96+
die "Cannot write current commit's replacement sha1"
97+
fi
98+
99+
# rewrite parents; if none were rewritten, we can fast-forward.
100+
fast_forward=t
101+
preserve=t
102+
new_parents=
103+
for p in $(git rev-list --parents -1 $sha1 | cut -d\ -f2-)
104+
do
105+
if [ -f "$REWRITTEN"/$p ]
106+
then
107+
preserve=f
108+
new_p=$(cat "$REWRITTEN"/$p)
109+
test $p != $new_p && fast_forward=f
110+
case "$new_parents" in
111+
*$new_p*)
112+
;; # do nothing; that parent is already there
113+
*)
114+
new_parents="$new_parents $new_p"
115+
esac
116+
fi
117+
done
118+
case $fast_forward in
119+
t)
120+
echo "Fast forward to $sha1"
121+
test $preserve=f && echo $sha1 > "$REWRITTEN"/$sha1
122+
;;
123+
f)
124+
test "a$1" = a-n && die "Refusing to squash a merge: $sha1"
125+
126+
first_parent=$(expr "$new_parents" : " \([^ ]*\)")
127+
# detach HEAD to current parent
128+
git checkout $first_parent 2> /dev/null ||
129+
die "Cannot move HEAD to $first_parent"
130+
131+
echo $sha1 > "$DOTEST"/current-commit
132+
case "$new_parents" in
133+
\ *\ *)
134+
# redo merge
135+
author_script=$(get_author_ident_from_commit $sha1)
136+
eval "$author_script"
137+
msg="$(git cat-file commit $sha1 | \
138+
sed -e '1,/^$/d' -e "s/[\"\\]/\\\\&/g")"
139+
# NEEDSWORK: give rerere a chance
140+
if ! git merge $STRATEGY -m "$msg" $new_parents
141+
then
142+
echo "$msg" > "$GIT_DIR"/MERGE_MSG
143+
warn Error redoing merge $sha1
144+
warn
145+
warn After fixup, please use
146+
die "$author_script git commit"
147+
fi
148+
;;
149+
*)
150+
git cherry-pick $STRATEGY "$@" ||
151+
die_with_patch $sha1 "Could not pick $sha1"
152+
esac
153+
esac
154+
}
155+
82156
do_next () {
83157
read command sha1 rest < "$TODO"
84158
case "$command" in
@@ -155,7 +229,15 @@ do_next () {
155229
HEADNAME=$(cat "$DOTEST"/head-name) &&
156230
OLDHEAD=$(cat "$DOTEST"/head) &&
157231
SHORTONTO=$(git rev-parse --short $(cat "$DOTEST"/onto)) &&
158-
NEWHEAD=$(git rev-parse HEAD) &&
232+
if [ -d "$REWRITTEN" ]
233+
then
234+
test -f "$DOTEST"/current-commit &&
235+
current_commit=$(cat "$DOTEST"/current-commit) &&
236+
git rev-parse HEAD > "$REWRITTEN"/$current_commit
237+
NEWHEAD=$(cat "$REWRITTEN"/$OLDHEAD)
238+
else
239+
NEWHEAD=$(git rev-parse HEAD)
240+
fi &&
159241
message="$GIT_REFLOG_ACTION: $HEADNAME onto $SHORTONTO)" &&
160242
git update-ref -m "$message" $HEADNAME $NEWHEAD $OLDHEAD &&
161243
git symbolic-ref HEAD $HEADNAME &&
@@ -226,6 +308,9 @@ do
226308
-v|--verbose)
227309
VERBOSE=t
228310
;;
311+
-p|--preserve-merges)
312+
PRESERVE_MERGES=t
313+
;;
229314
-i|--interactive)
230315
# yeah, we know
231316
;;
@@ -274,6 +359,25 @@ do
274359
echo $UPSTREAM > "$DOTEST"/upstream
275360
echo $ONTO > "$DOTEST"/onto
276361
test t = "$VERBOSE" && : > "$DOTEST"/verbose
362+
if [ t = "$PRESERVE_MERGES" ]
363+
then
364+
# $REWRITTEN contains files for each commit that is
365+
# reachable by at least one merge base of $HEAD and
366+
# $UPSTREAM. They are not necessarily rewritten, but
367+
# their children might be.
368+
# This ensures that commits on merged, but otherwise
369+
# unrelated side branches are left alone. (Think "X"
370+
# in the man page's example.)
371+
mkdir "$REWRITTEN" &&
372+
for c in $(git merge-base --all $HEAD $UPSTREAM)
373+
do
374+
echo $ONTO > "$REWRITTEN"/$c ||
375+
die "Could not init rewritten commits"
376+
done
377+
MERGES_OPTION=
378+
else
379+
MERGES_OPTION=--no-merges
380+
fi
277381

278382
SHORTUPSTREAM=$(git rev-parse --short $UPSTREAM)
279383
SHORTHEAD=$(git rev-parse --short $HEAD)
@@ -286,7 +390,7 @@ do
286390
# edit = use commit, but stop for amending
287391
# squash = use commit, but meld into previous commit
288392
EOF
289-
git rev-list --no-merges --pretty=oneline --abbrev-commit \
393+
git rev-list $MERGES_OPTION --pretty=oneline --abbrev-commit \
290394
--abbrev=7 --reverse $UPSTREAM..$HEAD | \
291395
sed "s/^/pick /" >> "$TODO"
292396

t/t3404-rebase-interactive.sh

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,4 +166,26 @@ test_expect_success 'retain authorship when squashing' '
166166
git show HEAD | grep "^Author: Nitfol"
167167
'
168168

169+
test_expect_success 'preserve merges with -p' '
170+
git checkout -b to-be-preserved master^ &&
171+
: > unrelated-file &&
172+
git add unrelated-file &&
173+
test_tick &&
174+
git commit -m "unrelated" &&
175+
git checkout -b to-be-rebased master &&
176+
echo B > file1 &&
177+
test_tick &&
178+
git commit -m J file1 &&
179+
test_tick &&
180+
git merge to-be-preserved &&
181+
echo C > file1 &&
182+
test_tick &&
183+
git commit -m K file1 &&
184+
git rebase -i -p --onto branch1 master &&
185+
test $(git rev-parse HEAD^^2) = $(git rev-parse to-be-preserved) &&
186+
test $(git rev-parse HEAD~3) = $(git rev-parse branch1) &&
187+
test $(git show HEAD:file1) = C &&
188+
test $(git show HEAD~2:file1) = B
189+
'
190+
169191
test_done

0 commit comments

Comments
 (0)