1
0
mirror of https://github.com/git/git.git synced 2024-09-28 06:30:37 +02:00

rebase (interactive-backend): make --keep-empty the default

Different rebase backends have different treatment for commits which
start empty (i.e. have no changes relative to their parent), and the
--keep-empty option was added at some point to allow adjusting behavior.
The handling of commits which start empty is actually quite similar to
commit b00bf1c9a8 (git-rebase: make --allow-empty-message the default,
2018-06-27), which pointed out that the behavior for various backends is
often more happenstance than design.  The specific change made in that
commit is actually quite relevant as well and much of the logic there
directly applies here.

It makes a lot of sense in 'git commit' to error out on the creation of
empty commits, unless an override flag is provided.  However, once
someone determines that there is a rare case that merits using the
manual override to create such a commit, it is somewhere between
annoying and harmful to have to take extra steps to keep such
intentional commits around.  Granted, empty commits are quite rare,
which is why handling of them doesn't get considered much and folks tend
to defer to existing (accidental) behavior and assume there was a reason
for it, leading them to just add flags (--keep-empty in this case) that
allow them to override the bad defaults.  Fix the interactive backend so
that --keep-empty is the default, much like we did with
--allow-empty-message.  The am backend should also be fixed to have
--keep-empty semantics for commits that start empty, but that is not
included in this patch other than a testcase documenting the failure.

Note that there was one test in t3421 which appears to have been written
expecting --keep-empty to not be the default as correct behavior.  This
test was introduced in commit 00b8be5a4d ("add tests for rebasing of
empty commits", 2013-06-06), which was part of a series focusing on
rebase topology and which had an interesting original cover letter at
https://lore.kernel.org/git/1347949878-12578-1-git-send-email-martinvonz@gmail.com/
which noted
    Your input especially appreciated on whether you agree with the
    intent of the test cases.
and then went into a long example about how one of the many tests added
had several questions about whether it was correct.  As such, I believe
most the tests in that series were about testing rebase topology with as
many different flags as possible and were not trying to state in general
how those flags should behave otherwise.

Signed-off-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
This commit is contained in:
Elijah Newren 2020-02-15 21:36:24 +00:00 committed by Junio C Hamano
parent a9ae8fde2e
commit d48e5e21da
9 changed files with 126 additions and 55 deletions

View File

@ -259,10 +259,13 @@ See also INCOMPATIBLE OPTIONS below.
unchanged as a result.
--keep-empty::
Keep the commits that do not change anything from its
parents in the result.
No-op. Rebasing commits that started empty (had no change
relative to their parent) used to fail and this option would
override that behavior, allowing commits with empty changes to
be rebased. Now commits with no changes do not cause rebasing
to halt.
+
See also INCOMPATIBLE OPTIONS below.
See also BEHAVIORAL DIFFERENCES and INCOMPATIBLE OPTIONS below.
--allow-empty-message::
No-op. Rebasing commits with an empty message used to fail
@ -577,15 +580,14 @@ There are some subtle differences how the backends behave.
Empty commits
~~~~~~~~~~~~~
The am backend drops any "empty" commits, regardless of whether the
commit started empty (had no changes relative to its parent to
start with) or ended empty (all changes were already applied
upstream in other commits).
The am backend unfortunately drops intentionally empty commits, i.e.
commits that started empty, though these are rare in practice. It
also drops commits that become empty and has no option for controlling
this behavior.
The interactive backend drops commits by default that
started empty and halts if it hits a commit that ended up empty.
The `--keep-empty` option exists for the interactive backend to allow
it to keep commits that started empty.
The interactive backend keeps intentionally empty commits.
Unfortunately, it always halts whenever it runs across a commit that
becomes empty, even when the rebase is not explicitly interactive.
Directory rename detection
~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -77,7 +77,6 @@ struct rebase_options {
const char *action;
int signoff;
int allow_rerere_autoupdate;
int keep_empty;
int autosquash;
char *gpg_sign_opt;
int autostash;
@ -375,7 +374,6 @@ static int run_rebase_interactive(struct rebase_options *opts,
git_config_get_bool("rebase.abbreviatecommands", &abbreviate_commands);
flags |= opts->keep_empty ? TODO_LIST_KEEP_EMPTY : 0;
flags |= abbreviate_commands ? TODO_LIST_ABBREVIATE_CMDS : 0;
flags |= opts->rebase_merges ? TODO_LIST_REBASE_MERGES : 0;
flags |= opts->rebase_cousins > 0 ? TODO_LIST_REBASE_COUSINS : 0;
@ -439,6 +437,17 @@ static int run_rebase_interactive(struct rebase_options *opts,
return ret;
}
static int parse_opt_keep_empty(const struct option *opt, const char *arg,
int unset)
{
struct rebase_options *opts = opt->value;
BUG_ON_OPT_ARG(arg);
opts->type = REBASE_INTERACTIVE;
return 0;
}
static const char * const builtin_rebase_interactive_usage[] = {
N_("git rebase--interactive [<options>]"),
NULL
@ -452,7 +461,10 @@ int cmd_rebase__interactive(int argc, const char **argv, const char *prefix)
struct option options[] = {
OPT_NEGBIT(0, "ff", &opts.flags, N_("allow fast-forward"),
REBASE_FORCE),
OPT_BOOL(0, "keep-empty", &opts.keep_empty, N_("keep empty commits")),
{ OPTION_CALLBACK, 'k', "keep-empty", &options, NULL,
N_("(DEPRECATED) keep empty commits"),
PARSE_OPT_NOARG | PARSE_OPT_HIDDEN,
parse_opt_keep_empty },
OPT_BOOL_F(0, "allow-empty-message", &opts.allow_empty_message,
N_("allow commits with empty messages"),
PARSE_OPT_HIDDEN),
@ -1145,7 +1157,6 @@ static int run_specific_rebase(struct rebase_options *opts, enum action action)
opts->allow_rerere_autoupdate ?
opts->allow_rerere_autoupdate == RERERE_AUTOUPDATE ?
"--rerere-autoupdate" : "--no-rerere-autoupdate" : "");
add_var(&script_snippet, "keep_empty", opts->keep_empty ? "yes" : "");
add_var(&script_snippet, "autosquash", opts->autosquash ? "t" : "");
add_var(&script_snippet, "gpg_sign_opt", opts->gpg_sign_opt);
add_var(&script_snippet, "cmd", opts->cmd);
@ -1483,8 +1494,10 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
"ignoring them"),
REBASE_PRESERVE_MERGES, PARSE_OPT_HIDDEN),
OPT_RERERE_AUTOUPDATE(&options.allow_rerere_autoupdate),
OPT_BOOL('k', "keep-empty", &options.keep_empty,
N_("preserve empty commits during rebase")),
{ OPTION_CALLBACK, 'k', "keep-empty", &options, NULL,
N_("(DEPRECATED) keep empty commits"),
PARSE_OPT_NOARG | PARSE_OPT_HIDDEN,
parse_opt_keep_empty },
OPT_BOOL(0, "autosquash", &options.autosquash,
N_("move commits that begin with "
"squash!/fixup! under -i")),
@ -1747,9 +1760,6 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
if (!(options.flags & REBASE_NO_QUIET))
argv_array_push(&options.git_am_opts, "-q");
if (options.keep_empty)
imply_interactive(&options, "--keep-empty");
if (gpg_sign) {
free(options.gpg_sign_opt);
options.gpg_sign_opt = xstrfmt("-S%s", gpg_sign);

View File

@ -28,7 +28,7 @@ static enum missing_commit_check_level get_missing_commit_check_level(void)
return MISSING_COMMIT_CHECK_IGNORE;
}
void append_todo_help(unsigned keep_empty, int command_count,
void append_todo_help(int command_count,
const char *shortrevisions, const char *shortonto,
struct strbuf *buf)
{
@ -80,11 +80,6 @@ void append_todo_help(unsigned keep_empty, int command_count,
"the rebase will be aborted.\n\n");
strbuf_add_commented_lines(buf, msg, strlen(msg));
if (!keep_empty) {
msg = _("Note that empty commits are commented out");
strbuf_add_commented_lines(buf, msg, strlen(msg));
}
}
int edit_todo_list(struct repository *r, struct todo_list *todo_list,

View File

@ -5,7 +5,7 @@ struct strbuf;
struct repository;
struct todo_list;
void append_todo_help(unsigned keep_empty, int command_count,
void append_todo_help(int command_count,
const char *shortrevisions, const char *shortonto,
struct strbuf *buf);
int edit_todo_list(struct repository *r, struct todo_list *todo_list,

View File

@ -1489,7 +1489,7 @@ static int allow_empty(struct repository *r,
struct replay_opts *opts,
struct commit *commit)
{
int index_unchanged, empty_commit;
int index_unchanged, originally_empty;
/*
* Three cases:
@ -1513,10 +1513,10 @@ static int allow_empty(struct repository *r,
if (opts->keep_redundant_commits)
return 1;
empty_commit = is_original_commit_empty(commit);
if (empty_commit < 0)
return empty_commit;
if (!empty_commit)
originally_empty = is_original_commit_empty(commit);
if (originally_empty < 0)
return originally_empty;
if (!originally_empty)
return 0;
else
return 1;
@ -4566,7 +4566,6 @@ static int make_script_with_merges(struct pretty_print_context *pp,
struct rev_info *revs, struct strbuf *out,
unsigned flags)
{
int keep_empty = flags & TODO_LIST_KEEP_EMPTY;
int rebase_cousins = flags & TODO_LIST_REBASE_COUSINS;
int root_with_onto = flags & TODO_LIST_ROOT_WITH_ONTO;
struct strbuf buf = STRBUF_INIT, oneline = STRBUF_INIT;
@ -4629,8 +4628,6 @@ static int make_script_with_merges(struct pretty_print_context *pp,
if (!to_merge) {
/* non-merge commit: easy case */
strbuf_reset(&buf);
if (!keep_empty && is_empty)
strbuf_addf(&buf, "%c ", comment_line_char);
strbuf_addf(&buf, "%s %s %s", cmd_pick,
oid_to_hex(&commit->object.oid),
oneline.buf);
@ -4797,7 +4794,6 @@ int sequencer_make_script(struct repository *r, struct strbuf *out, int argc,
struct pretty_print_context pp = {0};
struct rev_info revs;
struct commit *commit;
int keep_empty = flags & TODO_LIST_KEEP_EMPTY;
const char *insn = flags & TODO_LIST_ABBREVIATE_CMDS ? "p" : "pick";
int rebase_merges = flags & TODO_LIST_REBASE_MERGES;
@ -4833,12 +4829,10 @@ int sequencer_make_script(struct repository *r, struct strbuf *out, int argc,
return make_script_with_merges(&pp, &revs, out, flags);
while ((commit = get_revision(&revs))) {
int is_empty = is_original_commit_empty(commit);
int is_empty = is_original_commit_empty(commit);
if (!is_empty && (commit->object.flags & PATCHSAME))
continue;
if (!keep_empty && is_empty)
strbuf_addf(out, "%c ", comment_line_char);
strbuf_addf(out, "%s %s ", insn,
oid_to_hex(&commit->object.oid));
pretty_print_commit(&pp, commit, out);
@ -4975,7 +4969,7 @@ int todo_list_write_to_file(struct repository *r, struct todo_list *todo_list,
todo_list_to_strbuf(r, todo_list, &buf, num, flags);
if (flags & TODO_LIST_APPEND_TODO_HELP)
append_todo_help(flags & TODO_LIST_KEEP_EMPTY, count_commands(todo_list),
append_todo_help(count_commands(todo_list),
shortrevisions, shortonto, &buf);
res = write_message(buf.buf, buf.len, file, 0);

View File

@ -132,7 +132,7 @@ int sequencer_rollback(struct repository *repo, struct replay_opts *opts);
int sequencer_skip(struct repository *repo, struct replay_opts *opts);
int sequencer_remove_state(struct replay_opts *opts);
#define TODO_LIST_KEEP_EMPTY (1U << 0)
/* #define TODO_LIST_KEEP_EMPTY (1U << 0) */ /* No longer used */
#define TODO_LIST_SHORTEN_IDS (1U << 1)
#define TODO_LIST_ABBREVIATE_CMDS (1U << 2)
#define TODO_LIST_REBASE_MERGES (1U << 3)

View File

@ -205,17 +205,17 @@ test_expect_success 'setup of linear history for empty commit tests' '
test_run_rebase () {
result=$1
shift
test_expect_$result "rebase $* drops empty commit" "
test_expect_$result "rebase $* keeps begin-empty commits" "
reset_rebase &&
git rebase $* c l &&
test_cmp_rev c HEAD~2 &&
test_linear_range 'd l' c..
git rebase $* j l &&
test_cmp_rev c HEAD~4 &&
test_linear_range 'j d k l' c..
"
}
test_run_rebase success ''
test_run_rebase failure ''
test_run_rebase success -m
test_run_rebase success -i
test_have_prereq !REBASE_P || test_run_rebase success -p
test_have_prereq !REBASE_P || test_run_rebase failure -p
test_run_rebase () {
result=$1
@ -230,7 +230,7 @@ test_run_rebase () {
test_run_rebase success ''
test_run_rebase success -m
test_run_rebase success -i
test_have_prereq !REBASE_P || test_run_rebase failure -p
test_have_prereq !REBASE_P || test_run_rebase success -p
test_run_rebase () {
result=$1
@ -245,7 +245,7 @@ test_run_rebase () {
test_run_rebase success ''
test_run_rebase success -m
test_run_rebase success -i
test_have_prereq !REBASE_P || test_run_rebase failure -p
test_have_prereq !REBASE_P || test_run_rebase success -p
test_run_rebase success --rebase-merges
# m

70
t/t3424-rebase-empty.sh Executable file
View File

@ -0,0 +1,70 @@
#!/bin/sh
test_description='git rebase of commits that start or become empty'
. ./test-lib.sh
test_expect_success 'setup test repository' '
test_write_lines 1 2 3 4 5 6 7 8 9 10 >numbers &&
test_write_lines A B C D E F G H I J >letters &&
git add numbers letters &&
git commit -m A &&
git branch upstream &&
git branch localmods &&
git checkout upstream &&
test_write_lines A B C D E >letters &&
git add letters &&
git commit -m B &&
test_write_lines 1 2 3 4 five 6 7 8 9 ten >numbers &&
git add numbers &&
git commit -m C &&
git checkout localmods &&
test_write_lines 1 2 3 4 five 6 7 8 9 10 >numbers &&
git add numbers &&
git commit -m C2 &&
git commit --allow-empty -m D &&
test_write_lines A B C D E >letters &&
git add letters &&
git commit -m "Five letters ought to be enough for anybody"
'
test_expect_failure 'rebase (am-backend) with a variety of empty commits' '
test_when_finished "git rebase --abort" &&
git checkout -B testing localmods &&
# rebase (--am) should not drop commits that start empty
git rebase upstream &&
test_write_lines D C B A >expect &&
git log --format=%s >actual &&
test_cmp expect actual
'
test_expect_failure 'rebase --merge with a variety of empty commits' '
test_when_finished "git rebase --abort" &&
git checkout -B testing localmods &&
# rebase --merge should not halt on the commit that becomes empty
git rebase --merge upstream &&
test_write_lines D C B A >expect &&
git log --format=%s >actual &&
test_cmp expect actual
'
test_expect_success 'rebase --interactive with a variety of empty commits' '
git checkout -B testing localmods &&
test_must_fail git rebase --interactive upstream &&
git rebase --skip &&
test_write_lines D C B A >expect &&
git log --format=%s >actual &&
test_cmp expect actual
'
test_done

View File

@ -85,23 +85,23 @@ test_expect_failure REBASE_P 'Rebase -Xsubtree --keep-empty --preserve-merges --
verbose test "$(commit_message HEAD)" = "Empty commit"
'
test_expect_success 'Rebase -Xsubtree --keep-empty --onto commit' '
test_expect_success 'Rebase -Xsubtree --onto commit' '
reset_rebase &&
git checkout -b rebase-onto to-rebase &&
test_must_fail git rebase -Xsubtree=files_subtree --keep-empty --onto files-master master &&
test_must_fail git rebase -Xsubtree=files_subtree --onto files-master master &&
: first pick results in no changes &&
git rebase --continue &&
git rebase --skip &&
verbose test "$(commit_message HEAD~2)" = "master4" &&
verbose test "$(commit_message HEAD~)" = "files_subtree/master5" &&
verbose test "$(commit_message HEAD)" = "Empty commit"
'
test_expect_success 'Rebase -Xsubtree --keep-empty --rebase-merges --onto commit' '
test_expect_success 'Rebase -Xsubtree --rebase-merges --onto commit' '
reset_rebase &&
git checkout -b rebase-merges-onto to-rebase &&
test_must_fail git rebase -Xsubtree=files_subtree --keep-empty --rebase-merges --onto files-master --root &&
test_must_fail git rebase -Xsubtree=files_subtree --rebase-merges --onto files-master --root &&
: first pick results in no changes &&
git rebase --continue &&
git rebase --skip &&
verbose test "$(commit_message HEAD~2)" = "master4" &&
verbose test "$(commit_message HEAD~)" = "files_subtree/master5" &&
verbose test "$(commit_message HEAD)" = "Empty commit"