Merge branch 'ah/rebase-merges-config'

Streamline --rebase-merges command line option handling and
introduce rebase.merges configuration variable.

* ah/rebase-merges-config:
  rebase: add a config option for --rebase-merges
  rebase: deprecate --rebase-merges=""
  rebase: add documentation and test for --no-rebase-merges
This commit is contained in:
Junio C Hamano 2023-04-04 14:28:27 -07:00
commit 9142fce9b0
5 changed files with 138 additions and 22 deletions

View file

@ -67,3 +67,13 @@ rebase.rescheduleFailedExec::
rebase.forkPoint:: rebase.forkPoint::
If set to false set `--no-fork-point` option by default. If set to false set `--no-fork-point` option by default.
rebase.rebaseMerges::
Whether and how to set the `--rebase-merges` option by default. Can
be `rebase-cousins`, `no-rebase-cousins`, or a boolean. Setting to
true or to `no-rebase-cousins` is equivalent to
`--rebase-merges=no-rebase-cousins`, setting to `rebase-cousins` is
equivalent to `--rebase-merges=rebase-cousins`, and setting to false is
equivalent to `--no-rebase-merges`. Passing `--rebase-merges` on the
command line, with or without an argument, overrides any
`rebase.rebaseMerges` configuration.

View file

@ -529,20 +529,25 @@ See also INCOMPATIBLE OPTIONS below.
-r:: -r::
--rebase-merges[=(rebase-cousins|no-rebase-cousins)]:: --rebase-merges[=(rebase-cousins|no-rebase-cousins)]::
--no-rebase-merges::
By default, a rebase will simply drop merge commits from the todo By default, a rebase will simply drop merge commits from the todo
list, and put the rebased commits into a single, linear branch. list, and put the rebased commits into a single, linear branch.
With `--rebase-merges`, the rebase will instead try to preserve With `--rebase-merges`, the rebase will instead try to preserve
the branching structure within the commits that are to be rebased, the branching structure within the commits that are to be rebased,
by recreating the merge commits. Any resolved merge conflicts or by recreating the merge commits. Any resolved merge conflicts or
manual amendments in these merge commits will have to be manual amendments in these merge commits will have to be
resolved/re-applied manually. resolved/re-applied manually. `--no-rebase-merges` can be used to
countermand both the `rebase.rebaseMerges` config option and a previous
`--rebase-merges`.
+ +
By default, or when `no-rebase-cousins` was specified, commits which do not When rebasing merges, there are two modes: `rebase-cousins` and
have `<upstream>` as direct ancestor will keep their original branch point, `no-rebase-cousins`. If the mode is not specified, it defaults to
i.e. commits that would be excluded by linkgit:git-log[1]'s `no-rebase-cousins`. In `no-rebase-cousins` mode, commits which do not have
`--ancestry-path` option will keep their original ancestry by default. If `<upstream>` as direct ancestor will keep their original branch point, i.e.
the `rebase-cousins` mode is turned on, such commits are instead rebased commits that would be excluded by linkgit:git-log[1]'s `--ancestry-path`
onto `<upstream>` (or `<onto>`, if specified). option will keep their original ancestry by default. In `rebase-cousins` mode,
such commits are instead rebased onto `<upstream>` (or `<onto>`, if
specified).
+ +
It is currently only possible to recreate the merge commits using the It is currently only possible to recreate the merge commits using the
`ort` merge strategy; different merge strategies can be used only via `ort` merge strategy; different merge strategies can be used only via

View file

@ -124,6 +124,7 @@ struct rebase_options {
int fork_point; int fork_point;
int update_refs; int update_refs;
int config_autosquash; int config_autosquash;
int config_rebase_merges;
int config_update_refs; int config_update_refs;
}; };
@ -141,6 +142,8 @@ struct rebase_options {
.allow_empty_message = 1, \ .allow_empty_message = 1, \
.autosquash = -1, \ .autosquash = -1, \
.config_autosquash = -1, \ .config_autosquash = -1, \
.rebase_merges = -1, \
.config_rebase_merges = -1, \
.update_refs = -1, \ .update_refs = -1, \
.config_update_refs = -1, \ .config_update_refs = -1, \
} }
@ -772,6 +775,16 @@ static int run_specific_rebase(struct rebase_options *opts)
return status ? -1 : 0; return status ? -1 : 0;
} }
static void parse_rebase_merges_value(struct rebase_options *options, const char *value)
{
if (!strcmp("no-rebase-cousins", value))
options->rebase_cousins = 0;
else if (!strcmp("rebase-cousins", value))
options->rebase_cousins = 1;
else
die(_("Unknown rebase-merges mode: %s"), value);
}
static int rebase_config(const char *var, const char *value, void *data) static int rebase_config(const char *var, const char *value, void *data)
{ {
struct rebase_options *opts = data; struct rebase_options *opts = data;
@ -801,6 +814,17 @@ static int rebase_config(const char *var, const char *value, void *data)
return 0; return 0;
} }
if (!strcmp(var, "rebase.rebasemerges")) {
opts->config_rebase_merges = git_parse_maybe_bool(value);
if (opts->config_rebase_merges < 0) {
opts->config_rebase_merges = 1;
parse_rebase_merges_value(opts, value);
} else {
opts->rebase_cousins = 0;
}
return 0;
}
if (!strcmp(var, "rebase.updaterefs")) { if (!strcmp(var, "rebase.updaterefs")) {
opts->config_update_refs = git_config_bool(var, value); opts->config_update_refs = git_config_bool(var, value);
return 0; return 0;
@ -981,6 +1005,28 @@ static int parse_opt_empty(const struct option *opt, const char *arg, int unset)
return 0; return 0;
} }
static int parse_opt_rebase_merges(const struct option *opt, const char *arg, int unset)
{
struct rebase_options *options = opt->value;
options->rebase_merges = !unset;
options->rebase_cousins = 0;
if (arg) {
if (!*arg) {
warning(_("--rebase-merges with an empty string "
"argument is deprecated and will stop "
"working in a future version of Git. Use "
"--rebase-merges without an argument "
"instead, which does the same thing."));
return 0;
}
parse_rebase_merges_value(options, arg);
}
return 0;
}
static void NORETURN error_on_missing_default_upstream(void) static void NORETURN error_on_missing_default_upstream(void)
{ {
struct branch *current_branch = branch_get(NULL); struct branch *current_branch = branch_get(NULL);
@ -1036,7 +1082,6 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
struct object_id branch_base; struct object_id branch_base;
int ignore_whitespace = 0; int ignore_whitespace = 0;
const char *gpg_sign = NULL; const char *gpg_sign = NULL;
const char *rebase_merges = NULL;
struct string_list strategy_options = STRING_LIST_INIT_NODUP; struct string_list strategy_options = STRING_LIST_INIT_NODUP;
struct object_id squash_onto; struct object_id squash_onto;
char *squash_onto_name = NULL; char *squash_onto_name = NULL;
@ -1138,10 +1183,9 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
&options.allow_empty_message, &options.allow_empty_message,
N_("allow rebasing commits with empty messages"), N_("allow rebasing commits with empty messages"),
PARSE_OPT_HIDDEN), PARSE_OPT_HIDDEN),
{OPTION_STRING, 'r', "rebase-merges", &rebase_merges, OPT_CALLBACK_F('r', "rebase-merges", &options, N_("mode"),
N_("mode"),
N_("try to rebase merges instead of skipping them"), N_("try to rebase merges instead of skipping them"),
PARSE_OPT_OPTARG, NULL, (intptr_t)""}, PARSE_OPT_OPTARG, parse_opt_rebase_merges),
OPT_BOOL(0, "fork-point", &options.fork_point, OPT_BOOL(0, "fork-point", &options.fork_point,
N_("use 'merge-base --fork-point' to refine upstream")), N_("use 'merge-base --fork-point' to refine upstream")),
OPT_STRING('s', "strategy", &options.strategy, OPT_STRING('s', "strategy", &options.strategy,
@ -1437,17 +1481,6 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
if (options.exec.nr) if (options.exec.nr)
imply_merge(&options, "--exec"); imply_merge(&options, "--exec");
if (rebase_merges) {
if (!*rebase_merges)
; /* default mode; do nothing */
else if (!strcmp("rebase-cousins", rebase_merges))
options.rebase_cousins = 1;
else if (strcmp("no-rebase-cousins", rebase_merges))
die(_("Unknown mode: %s"), rebase_merges);
options.rebase_merges = 1;
imply_merge(&options, "--rebase-merges");
}
if (options.type == REBASE_APPLY) { if (options.type == REBASE_APPLY) {
if (ignore_whitespace) if (ignore_whitespace)
strvec_push(&options.git_am_opts, strvec_push(&options.git_am_opts,
@ -1515,6 +1548,8 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
"cannot be used together")); "cannot be used together"));
else if (options.autosquash == -1 && options.config_autosquash == 1) else if (options.autosquash == -1 && options.config_autosquash == 1)
die(_("apply options are incompatible with rebase.autoSquash. Consider adding --no-autosquash")); die(_("apply options are incompatible with rebase.autoSquash. Consider adding --no-autosquash"));
else if (options.rebase_merges == -1 && options.config_rebase_merges == 1)
die(_("apply options are incompatible with rebase.rebaseMerges. Consider adding --no-rebase-merges"));
else if (options.update_refs == -1 && options.config_update_refs == 1) else if (options.update_refs == -1 && options.config_update_refs == 1)
die(_("apply options are incompatible with rebase.updateRefs. Consider adding --no-update-refs")); die(_("apply options are incompatible with rebase.updateRefs. Consider adding --no-update-refs"));
else else
@ -1527,6 +1562,11 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
options.update_refs = (options.update_refs >= 0) ? options.update_refs : options.update_refs = (options.update_refs >= 0) ? options.update_refs :
((options.config_update_refs >= 0) ? options.config_update_refs : 0); ((options.config_update_refs >= 0) ? options.config_update_refs : 0);
if (options.rebase_merges == 1)
imply_merge(&options, "--rebase-merges");
options.rebase_merges = (options.rebase_merges >= 0) ? options.rebase_merges :
((options.config_rebase_merges >= 0) ? options.config_rebase_merges : 0);
if (options.autosquash == 1) if (options.autosquash == 1)
imply_merge(&options, "--autosquash"); imply_merge(&options, "--autosquash");
options.autosquash = (options.autosquash >= 0) ? options.autosquash : options.autosquash = (options.autosquash >= 0) ? options.autosquash :

View file

@ -85,6 +85,11 @@ test_rebase_am_only () {
test_must_fail git rebase $opt --reapply-cherry-picks A test_must_fail git rebase $opt --reapply-cherry-picks A
" "
test_expect_success "$opt incompatible with --rebase-merges" "
git checkout B^0 &&
test_must_fail git rebase $opt --rebase-merges A
"
test_expect_success "$opt incompatible with --update-refs" " test_expect_success "$opt incompatible with --update-refs" "
git checkout B^0 && git checkout B^0 &&
test_must_fail git rebase $opt --update-refs A test_must_fail git rebase $opt --update-refs A
@ -101,6 +106,12 @@ test_rebase_am_only () {
grep -e --no-autosquash err grep -e --no-autosquash err
" "
test_expect_success "$opt incompatible with rebase.rebaseMerges" "
git checkout B^0 &&
test_must_fail git -c rebase.rebaseMerges=true rebase $opt A 2>err &&
grep -e --no-rebase-merges err
"
test_expect_success "$opt incompatible with rebase.updateRefs" " test_expect_success "$opt incompatible with rebase.updateRefs" "
git checkout B^0 && git checkout B^0 &&
test_must_fail git -c rebase.updateRefs=true rebase $opt A 2>err && test_must_fail git -c rebase.updateRefs=true rebase $opt A 2>err &&
@ -113,6 +124,12 @@ test_rebase_am_only () {
git -c rebase.autosquash=true rebase --no-autosquash $opt A git -c rebase.autosquash=true rebase --no-autosquash $opt A
" "
test_expect_success "$opt okay with overridden rebase.rebaseMerges" "
test_when_finished \"git reset --hard B^0\" &&
git checkout B^0 &&
git -c rebase.rebaseMerges=true rebase --no-rebase-merges $opt A
"
test_expect_success "$opt okay with overridden rebase.updateRefs" " test_expect_success "$opt okay with overridden rebase.updateRefs" "
test_when_finished \"git reset --hard B^0\" && test_when_finished \"git reset --hard B^0\" &&
git checkout B^0 && git checkout B^0 &&

View file

@ -250,6 +250,16 @@ test_expect_success 'with a branch tip that was cherry-picked already' '
EOF EOF
' '
test_expect_success '--no-rebase-merges countermands --rebase-merges' '
git checkout -b no-rebase-merges E &&
git rebase --rebase-merges --no-rebase-merges C &&
test_cmp_graph C.. <<-\EOF
* B
* D
o C
EOF
'
test_expect_success 'do not rebase cousins unless asked for' ' test_expect_success 'do not rebase cousins unless asked for' '
git checkout -b cousins main && git checkout -b cousins main &&
before="$(git rev-parse --verify HEAD)" && before="$(git rev-parse --verify HEAD)" &&
@ -268,6 +278,40 @@ test_expect_success 'do not rebase cousins unless asked for' '
EOF EOF
' '
test_expect_success 'rebase.rebaseMerges=rebase-cousins is equivalent to --rebase-merges=rebase-cousins' '
test_config rebase.rebaseMerges rebase-cousins &&
git checkout -b config-rebase-cousins main &&
git rebase HEAD^ &&
test_cmp_graph HEAD^.. <<-\EOF
* Merge the topic branch '\''onebranch'\''
|\
| * D
| * G
|/
o H
EOF
'
test_expect_success '--no-rebase-merges overrides rebase.rebaseMerges=no-rebase-cousins' '
test_config rebase.rebaseMerges no-rebase-cousins &&
git checkout -b override-config-no-rebase-cousins E &&
git rebase --no-rebase-merges C &&
test_cmp_graph C.. <<-\EOF
* B
* D
o C
EOF
'
test_expect_success '--rebase-merges overrides rebase.rebaseMerges=rebase-cousins' '
test_config rebase.rebaseMerges rebase-cousins &&
git checkout -b override-config-rebase-cousins E &&
before="$(git rev-parse --verify HEAD)" &&
test_tick &&
git rebase --rebase-merges C &&
test_cmp_rev HEAD $before
'
test_expect_success 'refs/rewritten/* is worktree-local' ' test_expect_success 'refs/rewritten/* is worktree-local' '
git worktree add wt && git worktree add wt &&
cat >wt/script-from-scratch <<-\EOF && cat >wt/script-from-scratch <<-\EOF &&