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::
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::
--rebase-merges[=(rebase-cousins|no-rebase-cousins)]::
--no-rebase-merges::
By default, a rebase will simply drop merge commits from the todo
list, and put the rebased commits into a single, linear branch.
With `--rebase-merges`, the rebase will instead try to preserve
the branching structure within the commits that are to be rebased,
by recreating the merge commits. Any resolved merge conflicts or
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
have `<upstream>` as direct ancestor will keep their original branch point,
i.e. commits that would be excluded by linkgit:git-log[1]'s
`--ancestry-path` option will keep their original ancestry by default. If
the `rebase-cousins` mode is turned on, such commits are instead rebased
onto `<upstream>` (or `<onto>`, if specified).
When rebasing merges, there are two modes: `rebase-cousins` and
`no-rebase-cousins`. If the mode is not specified, it defaults to
`no-rebase-cousins`. In `no-rebase-cousins` mode, commits which do not have
`<upstream>` as direct ancestor will keep their original branch point, i.e.
commits that would be excluded by linkgit:git-log[1]'s `--ancestry-path`
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
`ort` merge strategy; different merge strategies can be used only via

View file

@ -124,6 +124,7 @@ struct rebase_options {
int fork_point;
int update_refs;
int config_autosquash;
int config_rebase_merges;
int config_update_refs;
};
@ -141,6 +142,8 @@ struct rebase_options {
.allow_empty_message = 1, \
.autosquash = -1, \
.config_autosquash = -1, \
.rebase_merges = -1, \
.config_rebase_merges = -1, \
.update_refs = -1, \
.config_update_refs = -1, \
}
@ -772,6 +775,16 @@ static int run_specific_rebase(struct rebase_options *opts)
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)
{
struct rebase_options *opts = data;
@ -801,6 +814,17 @@ static int rebase_config(const char *var, const char *value, void *data)
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")) {
opts->config_update_refs = git_config_bool(var, value);
return 0;
@ -981,6 +1005,28 @@ static int parse_opt_empty(const struct option *opt, const char *arg, int unset)
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)
{
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;
int ignore_whitespace = 0;
const char *gpg_sign = NULL;
const char *rebase_merges = NULL;
struct string_list strategy_options = STRING_LIST_INIT_NODUP;
struct object_id squash_onto;
char *squash_onto_name = NULL;
@ -1138,10 +1183,9 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
&options.allow_empty_message,
N_("allow rebasing commits with empty messages"),
PARSE_OPT_HIDDEN),
{OPTION_STRING, 'r', "rebase-merges", &rebase_merges,
N_("mode"),
OPT_CALLBACK_F('r', "rebase-merges", &options, N_("mode"),
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,
N_("use 'merge-base --fork-point' to refine upstream")),
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)
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 (ignore_whitespace)
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"));
else if (options.autosquash == -1 && options.config_autosquash == 1)
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)
die(_("apply options are incompatible with rebase.updateRefs. Consider adding --no-update-refs"));
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.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)
imply_merge(&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_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" "
git checkout B^0 &&
test_must_fail git rebase $opt --update-refs A
@ -101,6 +106,12 @@ test_rebase_am_only () {
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" "
git checkout B^0 &&
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
"
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_when_finished \"git reset --hard 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
'
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' '
git checkout -b cousins main &&
before="$(git rev-parse --verify HEAD)" &&
@ -268,6 +278,40 @@ test_expect_success 'do not rebase cousins unless asked for' '
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' '
git worktree add wt &&
cat >wt/script-from-scratch <<-\EOF &&