Skip to content

Commit 2c18e6a

Browse files
committed
Merge branch 'js/rebase-recreate-merge'
"git rebase" learned "--rebase-merges" to transplant the whole topology of commit graph elsewhere. * js/rebase-recreate-merge: rebase -i --rebase-merges: add a section to the man page rebase -i: introduce --rebase-merges=[no-]rebase-cousins pull: accept --rebase=merges to recreate the branch topology rebase --rebase-merges: avoid "empty merges" sequencer: handle post-rewrite for merge commands sequencer: make refs generated by the `label` command worktree-local rebase --rebase-merges: add test for --keep-empty rebase: introduce the --rebase-merges option rebase-helper --make-script: introduce a flag to rebase merges sequencer: fast-forward `merge` commands, if possible sequencer: introduce the `merge` command sequencer: introduce new commands to reset the revision git-rebase--interactive: clarify arguments sequencer: offer helpful advice when a command was rescheduled sequencer: refactor how original todo list lines are accessed sequencer: make rearrange_squash() a bit more obvious sequencer: avoid using errno clobbered by rollback_lock_file()
2 parents ad635e8 + 25cff9f commit 2c18e6a

14 files changed

+1352
-59
lines changed

Documentation/config.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1068,6 +1068,10 @@ branch.<name>.rebase::
10681068
"git pull" is run. See "pull.rebase" for doing this in a non
10691069
branch-specific manner.
10701070
+
1071+
When `merges`, pass the `--rebase-merges` option to 'git rebase'
1072+
so that the local merge commits are included in the rebase (see
1073+
linkgit:git-rebase[1] for details).
1074+
+
10711075
When preserve, also pass `--preserve-merges` along to 'git rebase'
10721076
so that locally committed merge commits will not be flattened
10731077
by running 'git pull'.
@@ -2669,6 +2673,10 @@ pull.rebase::
26692673
pull" is run. See "branch.<name>.rebase" for setting this on a
26702674
per-branch basis.
26712675
+
2676+
When `merges`, pass the `--rebase-merges` option to 'git rebase'
2677+
so that the local merge commits are included in the rebase (see
2678+
linkgit:git-rebase[1] for details).
2679+
+
26722680
When preserve, also pass `--preserve-merges` along to 'git rebase'
26732681
so that locally committed merge commits will not be flattened
26742682
by running 'git pull'.

Documentation/git-pull.txt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,13 +101,17 @@ Options related to merging
101101
include::merge-options.txt[]
102102

103103
-r::
104-
--rebase[=false|true|preserve|interactive]::
104+
--rebase[=false|true|merges|preserve|interactive]::
105105
When true, rebase the current branch on top of the upstream
106106
branch after fetching. If there is a remote-tracking branch
107107
corresponding to the upstream branch and the upstream branch
108108
was rebased since last fetched, the rebase uses that information
109109
to avoid rebasing non-local changes.
110110
+
111+
When set to `merges`, rebase using `git rebase --rebase-merges` so that
112+
the local merge commits are included in the rebase (see
113+
linkgit:git-rebase[1] for details).
114+
+
111115
When set to preserve, rebase with the `--preserve-merges` option passed
112116
to `git rebase` so that locally created merge commits will not be flattened.
113117
+

Documentation/git-rebase.txt

Lines changed: 162 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,33 @@ The commit list format can be changed by setting the configuration option
379379
rebase.instructionFormat. A customized instruction format will automatically
380380
have the long commit hash prepended to the format.
381381

382+
-r::
383+
--rebase-merges[=(rebase-cousins|no-rebase-cousins)]::
384+
By default, a rebase will simply drop merge commits from the todo
385+
list, and put the rebased commits into a single, linear branch.
386+
With `--rebase-merges`, the rebase will instead try to preserve
387+
the branching structure within the commits that are to be rebased,
388+
by recreating the merge commits. Any resolved merge conflicts or
389+
manual amendments in these merge commits will have to be
390+
resolved/re-applied manually.
391+
+
392+
By default, or when `no-rebase-cousins` was specified, commits which do not
393+
have `<upstream>` as direct ancestor will keep their original branch point,
394+
i.e. commits that would be excluded by gitlink:git-log[1]'s
395+
`--ancestry-path` option will keep their original ancestry by default. If
396+
the `rebase-cousins` mode is turned on, such commits are instead rebased
397+
onto `<upstream>` (or `<onto>`, if specified).
398+
+
399+
The `--rebase-merges` mode is similar in spirit to `--preserve-merges`, but
400+
in contrast to that option works well in interactive rebases: commits can be
401+
reordered, inserted and dropped at will.
402+
+
403+
It is currently only possible to recreate the merge commits using the
404+
`recursive` merge strategy; Different merge strategies can be used only via
405+
explicit `exec git merge -s <strategy> [...]` commands.
406+
+
407+
See also REBASING MERGES below.
408+
382409
-p::
383410
--preserve-merges::
384411
Recreate merge commits instead of flattening the history by replaying
@@ -776,12 +803,146 @@ The ripple effect of a "hard case" recovery is especially bad:
776803
'everyone' downstream from 'topic' will now have to perform a "hard
777804
case" recovery too!
778805

806+
REBASING MERGES
807+
-----------------
808+
809+
The interactive rebase command was originally designed to handle
810+
individual patch series. As such, it makes sense to exclude merge
811+
commits from the todo list, as the developer may have merged the
812+
then-current `master` while working on the branch, only to rebase
813+
all the commits onto `master` eventually (skipping the merge
814+
commits).
815+
816+
However, there are legitimate reasons why a developer may want to
817+
recreate merge commits: to keep the branch structure (or "commit
818+
topology") when working on multiple, inter-related branches.
819+
820+
In the following example, the developer works on a topic branch that
821+
refactors the way buttons are defined, and on another topic branch
822+
that uses that refactoring to implement a "Report a bug" button. The
823+
output of `git log --graph --format=%s -5` may look like this:
824+
825+
------------
826+
* Merge branch 'report-a-bug'
827+
|\
828+
| * Add the feedback button
829+
* | Merge branch 'refactor-button'
830+
|\ \
831+
| |/
832+
| * Use the Button class for all buttons
833+
| * Extract a generic Button class from the DownloadButton one
834+
------------
835+
836+
The developer might want to rebase those commits to a newer `master`
837+
while keeping the branch topology, for example when the first topic
838+
branch is expected to be integrated into `master` much earlier than the
839+
second one, say, to resolve merge conflicts with changes to the
840+
DownloadButton class that made it into `master`.
841+
842+
This rebase can be performed using the `--rebase-merges` option.
843+
It will generate a todo list looking like this:
844+
845+
------------
846+
label onto
847+
848+
# Branch: refactor-button
849+
reset onto
850+
pick 123456 Extract a generic Button class from the DownloadButton one
851+
pick 654321 Use the Button class for all buttons
852+
label refactor-button
853+
854+
# Branch: report-a-bug
855+
reset refactor-button # Use the Button class for all buttons
856+
pick abcdef Add the feedback button
857+
label report-a-bug
858+
859+
reset onto
860+
merge -C a1b2c3 refactor-button # Merge 'refactor-button'
861+
merge -C 6f5e4d report-a-bug # Merge 'report-a-bug'
862+
------------
863+
864+
In contrast to a regular interactive rebase, there are `label`, `reset`
865+
and `merge` commands in addition to `pick` ones.
866+
867+
The `label` command associates a label with the current HEAD when that
868+
command is executed. These labels are created as worktree-local refs
869+
(`refs/rewritten/<label>`) that will be deleted when the rebase
870+
finishes. That way, rebase operations in multiple worktrees linked to
871+
the same repository do not interfere with one another. If the `label`
872+
command fails, it is rescheduled immediately, with a helpful message how
873+
to proceed.
874+
875+
The `reset` command resets the HEAD, index and worktree to the specified
876+
revision. It is isimilar to an `exec git reset --hard <label>`, but
877+
refuses to overwrite untracked files. If the `reset` command fails, it is
878+
rescheduled immediately, with a helpful message how to edit the todo list
879+
(this typically happens when a `reset` command was inserted into the todo
880+
list manually and contains a typo).
881+
882+
The `merge` command will merge the specified revision into whatever is
883+
HEAD at that time. With `-C <original-commit>`, the commit message of
884+
the specified merge commit will be used. When the `-C` is changed to
885+
a lower-case `-c`, the message will be opened in an editor after a
886+
successful merge so that the user can edit the message.
887+
888+
If a `merge` command fails for any reason other than merge conflicts (i.e.
889+
when the merge operation did not even start), it is rescheduled immediately.
890+
891+
At this time, the `merge` command will *always* use the `recursive`
892+
merge strategy, with no way to choose a different one. To work around
893+
this, an `exec` command can be used to call `git merge` explicitly,
894+
using the fact that the labels are worktree-local refs (the ref
895+
`refs/rewritten/onto` would correspond to the label `onto`, for example).
896+
897+
Note: the first command (`label onto`) labels the revision onto which
898+
the commits are rebased; The name `onto` is just a convention, as a nod
899+
to the `--onto` option.
900+
901+
It is also possible to introduce completely new merge commits from scratch
902+
by adding a command of the form `merge <merge-head>`. This form will
903+
generate a tentative commit message and always open an editor to let the
904+
user edit it. This can be useful e.g. when a topic branch turns out to
905+
address more than a single concern and wants to be split into two or
906+
even more topic branches. Consider this todo list:
907+
908+
------------
909+
pick 192837 Switch from GNU Makefiles to CMake
910+
pick 5a6c7e Document the switch to CMake
911+
pick 918273 Fix detection of OpenSSL in CMake
912+
pick afbecd http: add support for TLS v1.3
913+
pick fdbaec Fix detection of cURL in CMake on Windows
914+
------------
915+
916+
The one commit in this list that is not related to CMake may very well
917+
have been motivated by working on fixing all those bugs introduced by
918+
switching to CMake, but it addresses a different concern. To split this
919+
branch into two topic branches, the todo list could be edited like this:
920+
921+
------------
922+
label onto
923+
924+
pick afbecd http: add support for TLS v1.3
925+
label tlsv1.3
926+
927+
reset onto
928+
pick 192837 Switch from GNU Makefiles to CMake
929+
pick 918273 Fix detection of OpenSSL in CMake
930+
pick fdbaec Fix detection of cURL in CMake on Windows
931+
pick 5a6c7e Document the switch to CMake
932+
label cmake
933+
934+
reset onto
935+
merge tlsv1.3
936+
merge cmake
937+
------------
938+
779939
BUGS
780940
----
781941
The todo list presented by `--preserve-merges --interactive` does not
782942
represent the topology of the revision graph. Editing commits and
783943
rewording their commit messages should work fine, but attempts to
784-
reorder commits tend to produce counterintuitive results.
944+
reorder commits tend to produce counterintuitive results. Use
945+
`--rebase-merges` in such scenarios instead.
785946

786947
For example, an attempt to rearrange
787948
------------

builtin/pull.c

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,16 @@ enum rebase_type {
2727
REBASE_FALSE = 0,
2828
REBASE_TRUE,
2929
REBASE_PRESERVE,
30+
REBASE_MERGES,
3031
REBASE_INTERACTIVE
3132
};
3233

3334
/**
3435
* Parses the value of --rebase. If value is a false value, returns
3536
* REBASE_FALSE. If value is a true value, returns REBASE_TRUE. If value is
36-
* "preserve", returns REBASE_PRESERVE. If value is a invalid value, dies with
37-
* a fatal error if fatal is true, otherwise returns REBASE_INVALID.
37+
* "merges", returns REBASE_MERGES. If value is "preserve", returns
38+
* REBASE_PRESERVE. If value is a invalid value, dies with a fatal error if
39+
* fatal is true, otherwise returns REBASE_INVALID.
3840
*/
3941
static enum rebase_type parse_config_rebase(const char *key, const char *value,
4042
int fatal)
@@ -47,6 +49,8 @@ static enum rebase_type parse_config_rebase(const char *key, const char *value,
4749
return REBASE_TRUE;
4850
else if (!strcmp(value, "preserve"))
4951
return REBASE_PRESERVE;
52+
else if (!strcmp(value, "merges"))
53+
return REBASE_MERGES;
5054
else if (!strcmp(value, "interactive"))
5155
return REBASE_INTERACTIVE;
5256

@@ -130,7 +134,7 @@ static struct option pull_options[] = {
130134
/* Options passed to git-merge or git-rebase */
131135
OPT_GROUP(N_("Options related to merging")),
132136
{ OPTION_CALLBACK, 'r', "rebase", &opt_rebase,
133-
"false|true|preserve|interactive",
137+
"false|true|merges|preserve|interactive",
134138
N_("incorporate changes by rebasing rather than merging"),
135139
PARSE_OPT_OPTARG, parse_opt_rebase },
136140
OPT_PASSTHRU('n', NULL, &opt_diffstat, NULL,
@@ -800,7 +804,9 @@ static int run_rebase(const struct object_id *curr_head,
800804
argv_push_verbosity(&args);
801805

802806
/* Options passed to git-rebase */
803-
if (opt_rebase == REBASE_PRESERVE)
807+
if (opt_rebase == REBASE_MERGES)
808+
argv_array_push(&args, "--rebase-merges");
809+
else if (opt_rebase == REBASE_PRESERVE)
804810
argv_array_push(&args, "--preserve-merges");
805811
else if (opt_rebase == REBASE_INTERACTIVE)
806812
argv_array_push(&args, "--interactive");

builtin/rebase--helper.c

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ static const char * const builtin_rebase_helper_usage[] = {
1212
int cmd_rebase__helper(int argc, const char **argv, const char *prefix)
1313
{
1414
struct replay_opts opts = REPLAY_OPTS_INIT;
15-
unsigned flags = 0, keep_empty = 0;
16-
int abbreviate_commands = 0;
15+
unsigned flags = 0, keep_empty = 0, rebase_merges = 0;
16+
int abbreviate_commands = 0, rebase_cousins = -1;
1717
enum {
1818
CONTINUE = 1, ABORT, MAKE_SCRIPT, SHORTEN_OIDS, EXPAND_OIDS,
1919
CHECK_TODO_LIST, SKIP_UNNECESSARY_PICKS, REARRANGE_SQUASH,
@@ -24,6 +24,9 @@ int cmd_rebase__helper(int argc, const char **argv, const char *prefix)
2424
OPT_BOOL(0, "keep-empty", &keep_empty, N_("keep empty commits")),
2525
OPT_BOOL(0, "allow-empty-message", &opts.allow_empty_message,
2626
N_("allow commits with empty messages")),
27+
OPT_BOOL(0, "rebase-merges", &rebase_merges, N_("rebase merge commits")),
28+
OPT_BOOL(0, "rebase-cousins", &rebase_cousins,
29+
N_("keep original branch points of cousins")),
2730
OPT_CMDMODE(0, "continue", &command, N_("continue rebase"),
2831
CONTINUE),
2932
OPT_CMDMODE(0, "abort", &command, N_("abort rebase"),
@@ -57,8 +60,14 @@ int cmd_rebase__helper(int argc, const char **argv, const char *prefix)
5760

5861
flags |= keep_empty ? TODO_LIST_KEEP_EMPTY : 0;
5962
flags |= abbreviate_commands ? TODO_LIST_ABBREVIATE_CMDS : 0;
63+
flags |= rebase_merges ? TODO_LIST_REBASE_MERGES : 0;
64+
flags |= rebase_cousins > 0 ? TODO_LIST_REBASE_COUSINS : 0;
6065
flags |= command == SHORTEN_OIDS ? TODO_LIST_SHORTEN_IDS : 0;
6166

67+
if (rebase_cousins >= 0 && !rebase_merges)
68+
warning(_("--[no-]rebase-cousins has no effect without "
69+
"--rebase-merges"));
70+
6271
if (command == CONTINUE && argc == 1)
6372
return !!sequencer_continue(&opts);
6473
if (command == ABORT && argc == 1)

builtin/remote.c

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,9 @@ static int add(int argc, const char **argv)
245245
struct branch_info {
246246
char *remote_name;
247247
struct string_list merge;
248-
enum { NO_REBASE, NORMAL_REBASE, INTERACTIVE_REBASE } rebase;
248+
enum {
249+
NO_REBASE, NORMAL_REBASE, INTERACTIVE_REBASE, REBASE_MERGES
250+
} rebase;
249251
};
250252

251253
static struct string_list branch_list = STRING_LIST_INIT_NODUP;
@@ -306,6 +308,8 @@ static int config_read_branches(const char *key, const char *value, void *cb)
306308
info->rebase = v;
307309
else if (!strcmp(value, "preserve"))
308310
info->rebase = NORMAL_REBASE;
311+
else if (!strcmp(value, "merges"))
312+
info->rebase = REBASE_MERGES;
309313
else if (!strcmp(value, "interactive"))
310314
info->rebase = INTERACTIVE_REBASE;
311315
}
@@ -963,9 +967,15 @@ static int show_local_info_item(struct string_list_item *item, void *cb_data)
963967

964968
printf(" %-*s ", show_info->width, item->string);
965969
if (branch_info->rebase) {
966-
printf_ln(branch_info->rebase == INTERACTIVE_REBASE
967-
? _("rebases interactively onto remote %s")
968-
: _("rebases onto remote %s"), merge->items[0].string);
970+
const char *msg;
971+
if (branch_info->rebase == INTERACTIVE_REBASE)
972+
msg = _("rebases interactively onto remote %s");
973+
else if (branch_info->rebase == REBASE_MERGES)
974+
msg = _("rebases interactively (with merges) onto "
975+
"remote %s");
976+
else
977+
msg = _("rebases onto remote %s");
978+
printf_ln(msg, merge->items[0].string);
969979
return 0;
970980
} else if (show_info->any_rebase) {
971981
printf_ln(_(" merges with remote %s"), merge->items[0].string);

contrib/completion/git-completion.bash

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1949,7 +1949,7 @@ _git_rebase ()
19491949
--*)
19501950
__gitcomp "
19511951
--onto --merge --strategy --interactive
1952-
--preserve-merges --stat --no-stat
1952+
--rebase-merges --preserve-merges --stat --no-stat
19531953
--committer-date-is-author-date --ignore-date
19541954
--ignore-whitespace --whitespace=
19551955
--autosquash --no-autosquash
@@ -2120,7 +2120,7 @@ _git_config ()
21202120
return
21212121
;;
21222122
branch.*.rebase)
2123-
__gitcomp "false true preserve interactive"
2123+
__gitcomp "false true merges preserve interactive"
21242124
return
21252125
;;
21262126
remote.pushdefault)

git-rebase--interactive.sh

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -155,13 +155,19 @@ reschedule_last_action () {
155155
append_todo_help () {
156156
gettext "
157157
Commands:
158-
p, pick = use commit
159-
r, reword = use commit, but edit the commit message
160-
e, edit = use commit, but stop for amending
161-
s, squash = use commit, but meld into previous commit
162-
f, fixup = like \"squash\", but discard this commit's log message
163-
x, exec = run command (the rest of the line) using shell
164-
d, drop = remove commit
158+
p, pick <commit> = use commit
159+
r, reword <commit> = use commit, but edit the commit message
160+
e, edit <commit> = use commit, but stop for amending
161+
s, squash <commit> = use commit, but meld into previous commit
162+
f, fixup <commit> = like \"squash\", but discard this commit's log message
163+
x, exec <commit> = run command (the rest of the line) using shell
164+
d, drop <commit> = remove commit
165+
l, label <label> = label current HEAD with a name
166+
t, reset <label> = reset HEAD to a label
167+
m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
168+
. create a merge commit using the original merge commit's
169+
. message (or the oneline, if no original merge commit was
170+
. specified). Use -c <commit> to reword the commit message.
165171
166172
These lines can be re-ordered; they are executed from top to bottom.
167173
" | git stripspace --comment-lines >>"$todo"
@@ -964,6 +970,8 @@ git_rebase__interactive () {
964970
init_revisions_and_shortrevisions
965971

966972
git rebase--helper --make-script ${keep_empty:+--keep-empty} \
973+
${rebase_merges:+--rebase-merges} \
974+
${rebase_cousins:+--rebase-cousins} \
967975
$revisions ${restrict_revision+^$restrict_revision} >"$todo" ||
968976
die "$(gettext "Could not generate todo list")"
969977

0 commit comments

Comments
 (0)