@matheusbsbs / matheus-bagliano / commits / d48e5e21da
Fork 0

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 b00bf1c9a8dd (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 00b8be5a4d38 ("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>

Elijah Newren committed Feb 15, 2020 at 21:36 UTC d48e5e21da980c6d439655a1292d0332b341c7d1
9 files changed +126 -55
Documentation/git-rebase.txt
+13 -11
@@ -259,10 +259,13 @@ See also INCOMPATIBLE OPTIONS below.
259 unchanged as a result.
260
261 --keep-empty::
262 - Keep the commits that do not change anything from its
263 - parents in the result.
262 + No-op. Rebasing commits that started empty (had no change
263 + relative to their parent) used to fail and this option would
264 + override that behavior, allowing commits with empty changes to
265 + be rebased. Now commits with no changes do not cause rebasing
266 + to halt.
267 +
265 -See also INCOMPATIBLE OPTIONS below.
268 +See also BEHAVIORAL DIFFERENCES and INCOMPATIBLE OPTIONS below.
269
270 --allow-empty-message::
271 No-op. Rebasing commits with an empty message used to fail
@@ -577,15 +580,14 @@ There are some subtle differences how the backends behave.
580 Empty commits
581 ~~~~~~~~~~~~~
582
580 -The am backend drops any "empty" commits, regardless of whether the
581 -commit started empty (had no changes relative to its parent to
582 -start with) or ended empty (all changes were already applied
583 -upstream in other commits).
583 +The am backend unfortunately drops intentionally empty commits, i.e.
584 +commits that started empty, though these are rare in practice. It
585 +also drops commits that become empty and has no option for controlling
586 +this behavior.
587
585 -The interactive backend drops commits by default that
586 -started empty and halts if it hits a commit that ended up empty.
587 -The `--keep-empty` option exists for the interactive backend to allow
588 -it to keep commits that started empty.
588 +The interactive backend keeps intentionally empty commits.
589 +Unfortunately, it always halts whenever it runs across a commit that
590 +becomes empty, even when the rebase is not explicitly interactive.
591
592 Directory rename detection
593 ~~~~~~~~~~~~~~~~~~~~~~~~~~
builtin/rebase.c
+19 -9
@@ -77,7 +77,6 @@ struct rebase_options {
77 const char *action;
78 int signoff;
79 int allow_rerere_autoupdate;
80 - int keep_empty;
80 int autosquash;
81 char *gpg_sign_opt;
82 int autostash;
@@ -375,7 +374,6 @@ static int run_rebase_interactive(struct rebase_options *opts,
374
375 git_config_get_bool("rebase.abbreviatecommands", &abbreviate_commands);
376
378 - flags |= opts->keep_empty ? TODO_LIST_KEEP_EMPTY : 0;
377 flags |= abbreviate_commands ? TODO_LIST_ABBREVIATE_CMDS : 0;
378 flags |= opts->rebase_merges ? TODO_LIST_REBASE_MERGES : 0;
379 flags |= opts->rebase_cousins > 0 ? TODO_LIST_REBASE_COUSINS : 0;
@@ -439,6 +437,17 @@ static int run_rebase_interactive(struct rebase_options *opts,
437 return ret;
438 }
439
440 +static int parse_opt_keep_empty(const struct option *opt, const char *arg,
441 + int unset)
442 +{
443 + struct rebase_options *opts = opt->value;
444 +
445 + BUG_ON_OPT_ARG(arg);
446 +
447 + opts->type = REBASE_INTERACTIVE;
448 + return 0;
449 +}
450 +
451 static const char * const builtin_rebase_interactive_usage[] = {
452 N_("git rebase--interactive [<options>]"),
453 NULL
@@ -452,7 +461,10 @@ int cmd_rebase__interactive(int argc, const char **argv, const char *prefix)
461 struct option options[] = {
462 OPT_NEGBIT(0, "ff", &opts.flags, N_("allow fast-forward"),
463 REBASE_FORCE),
455 - OPT_BOOL(0, "keep-empty", &opts.keep_empty, N_("keep empty commits")),
464 + { OPTION_CALLBACK, 'k', "keep-empty", &options, NULL,
465 + N_("(DEPRECATED) keep empty commits"),
466 + PARSE_OPT_NOARG | PARSE_OPT_HIDDEN,
467 + parse_opt_keep_empty },
468 OPT_BOOL_F(0, "allow-empty-message", &opts.allow_empty_message,
469 N_("allow commits with empty messages"),
470 PARSE_OPT_HIDDEN),
@@ -1145,7 +1157,6 @@ static int run_specific_rebase(struct rebase_options *opts, enum action action)
1157 opts->allow_rerere_autoupdate ?
1158 opts->allow_rerere_autoupdate == RERERE_AUTOUPDATE ?
1159 "--rerere-autoupdate" : "--no-rerere-autoupdate" : "");
1148 - add_var(&script_snippet, "keep_empty", opts->keep_empty ? "yes" : "");
1160 add_var(&script_snippet, "autosquash", opts->autosquash ? "t" : "");
1161 add_var(&script_snippet, "gpg_sign_opt", opts->gpg_sign_opt);
1162 add_var(&script_snippet, "cmd", opts->cmd);
@@ -1483,8 +1494,10 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
1494 "ignoring them"),
1495 REBASE_PRESERVE_MERGES, PARSE_OPT_HIDDEN),
1496 OPT_RERERE_AUTOUPDATE(&options.allow_rerere_autoupdate),
1486 - OPT_BOOL('k', "keep-empty", &options.keep_empty,
1487 - N_("preserve empty commits during rebase")),
1497 + { OPTION_CALLBACK, 'k', "keep-empty", &options, NULL,
1498 + N_("(DEPRECATED) keep empty commits"),
1499 + PARSE_OPT_NOARG | PARSE_OPT_HIDDEN,
1500 + parse_opt_keep_empty },
1501 OPT_BOOL(0, "autosquash", &options.autosquash,
1502 N_("move commits that begin with "
1503 "squash!/fixup! under -i")),
@@ -1747,9 +1760,6 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
1760 if (!(options.flags & REBASE_NO_QUIET))
1761 argv_array_push(&options.git_am_opts, "-q");
1762
1750 - if (options.keep_empty)
1751 - imply_interactive(&options, "--keep-empty");
1752 -
1763 if (gpg_sign) {
1764 free(options.gpg_sign_opt);
1765 options.gpg_sign_opt = xstrfmt("-S%s", gpg_sign);
rebase-interactive.c
+1 -6
@@ -28,7 +28,7 @@ static enum missing_commit_check_level get_missing_commit_check_level(void)
28 return MISSING_COMMIT_CHECK_IGNORE;
29 }
30
31 -void append_todo_help(unsigned keep_empty, int command_count,
31 +void append_todo_help(int command_count,
32 const char *shortrevisions, const char *shortonto,
33 struct strbuf *buf)
34 {
@@ -80,11 +80,6 @@ void append_todo_help(unsigned keep_empty, int command_count,
80 "the rebase will be aborted.\n\n");
81
82 strbuf_add_commented_lines(buf, msg, strlen(msg));
83 -
84 - if (!keep_empty) {
85 - msg = _("Note that empty commits are commented out");
86 - strbuf_add_commented_lines(buf, msg, strlen(msg));
87 - }
83 }
84
85 int edit_todo_list(struct repository *r, struct todo_list *todo_list,
rebase-interactive.h
+1 -1
@@ -5,7 +5,7 @@ struct strbuf;
5 struct repository;
6 struct todo_list;
7
8 -void append_todo_help(unsigned keep_empty, int command_count,
8 +void append_todo_help(int command_count,
9 const char *shortrevisions, const char *shortonto,
10 struct strbuf *buf);
11 int edit_todo_list(struct repository *r, struct todo_list *todo_list,
sequencer.c
+7 -13
@@ -1489,7 +1489,7 @@ static int allow_empty(struct repository *r,
1489 struct replay_opts *opts,
1490 struct commit *commit)
1491 {
1492 - int index_unchanged, empty_commit;
1492 + int index_unchanged, originally_empty;
1493
1494 /*
1495 * Three cases:
@@ -1513,10 +1513,10 @@ static int allow_empty(struct repository *r,
1513 if (opts->keep_redundant_commits)
1514 return 1;
1515
1516 - empty_commit = is_original_commit_empty(commit);
1517 - if (empty_commit < 0)
1518 - return empty_commit;
1519 - if (!empty_commit)
1516 + originally_empty = is_original_commit_empty(commit);
1517 + if (originally_empty < 0)
1518 + return originally_empty;
1519 + if (!originally_empty)
1520 return 0;
1521 else
1522 return 1;
@@ -4566,7 +4566,6 @@ static int make_script_with_merges(struct pretty_print_context *pp,
4566 struct rev_info *revs, struct strbuf *out,
4567 unsigned flags)
4568 {
4569 - int keep_empty = flags & TODO_LIST_KEEP_EMPTY;
4569 int rebase_cousins = flags & TODO_LIST_REBASE_COUSINS;
4570 int root_with_onto = flags & TODO_LIST_ROOT_WITH_ONTO;
4571 struct strbuf buf = STRBUF_INIT, oneline = STRBUF_INIT;
@@ -4629,8 +4628,6 @@ static int make_script_with_merges(struct pretty_print_context *pp,
4628 if (!to_merge) {
4629 /* non-merge commit: easy case */
4630 strbuf_reset(&buf);
4632 - if (!keep_empty && is_empty)
4633 - strbuf_addf(&buf, "%c ", comment_line_char);
4631 strbuf_addf(&buf, "%s %s %s", cmd_pick,
4632 oid_to_hex(&commit->object.oid),
4633 oneline.buf);
@@ -4797,7 +4794,6 @@ int sequencer_make_script(struct repository *r, struct strbuf *out, int argc,
4794 struct pretty_print_context pp = {0};
4795 struct rev_info revs;
4796 struct commit *commit;
4800 - int keep_empty = flags & TODO_LIST_KEEP_EMPTY;
4797 const char *insn = flags & TODO_LIST_ABBREVIATE_CMDS ? "p" : "pick";
4798 int rebase_merges = flags & TODO_LIST_REBASE_MERGES;
4799
@@ -4833,12 +4829,10 @@ int sequencer_make_script(struct repository *r, struct strbuf *out, int argc,
4829 return make_script_with_merges(&pp, &revs, out, flags);
4830
4831 while ((commit = get_revision(&revs))) {
4836 - int is_empty = is_original_commit_empty(commit);
4832 + int is_empty = is_original_commit_empty(commit);
4833
4834 if (!is_empty && (commit->object.flags & PATCHSAME))
4835 continue;
4840 - if (!keep_empty && is_empty)
4841 - strbuf_addf(out, "%c ", comment_line_char);
4836 strbuf_addf(out, "%s %s ", insn,
4837 oid_to_hex(&commit->object.oid));
4838 pretty_print_commit(&pp, commit, out);
@@ -4975,7 +4969,7 @@ int todo_list_write_to_file(struct repository *r, struct todo_list *todo_list,
4969
4970 todo_list_to_strbuf(r, todo_list, &buf, num, flags);
4971 if (flags & TODO_LIST_APPEND_TODO_HELP)
4978 - append_todo_help(flags & TODO_LIST_KEEP_EMPTY, count_commands(todo_list),
4972 + append_todo_help(count_commands(todo_list),
4973 shortrevisions, shortonto, &buf);
4974
4975 res = write_message(buf.buf, buf.len, file, 0);
sequencer.h
+1 -1
@@ -132,7 +132,7 @@ int sequencer_rollback(struct repository *repo, struct replay_opts *opts);
132 int sequencer_skip(struct repository *repo, struct replay_opts *opts);
133 int sequencer_remove_state(struct replay_opts *opts);
134
135 -#define TODO_LIST_KEEP_EMPTY (1U << 0)
135 +/* #define TODO_LIST_KEEP_EMPTY (1U << 0) */ /* No longer used */
136 #define TODO_LIST_SHORTEN_IDS (1U << 1)
137 #define TODO_LIST_ABBREVIATE_CMDS (1U << 2)
138 #define TODO_LIST_REBASE_MERGES (1U << 3)
t/t3421-rebase-topology-linear.sh
+8 -8
@@ -205,17 +205,17 @@ test_expect_success 'setup of linear history for empty commit tests' '
205 test_run_rebase () {
206 result=$1
207 shift
208 - test_expect_$result "rebase $* drops empty commit" "
208 + test_expect_$result "rebase $* keeps begin-empty commits" "
209 reset_rebase &&
210 - git rebase $* c l &&
211 - test_cmp_rev c HEAD~2 &&
212 - test_linear_range 'd l' c..
210 + git rebase $* j l &&
211 + test_cmp_rev c HEAD~4 &&
212 + test_linear_range 'j d k l' c..
213 "
214 }
215 -test_run_rebase success ''
215 +test_run_rebase failure ''
216 test_run_rebase success -m
217 test_run_rebase success -i
218 -test_have_prereq !REBASE_P || test_run_rebase success -p
218 +test_have_prereq !REBASE_P || test_run_rebase failure -p
219
220 test_run_rebase () {
221 result=$1
@@ -230,7 +230,7 @@ test_run_rebase () {
230 test_run_rebase success ''
231 test_run_rebase success -m
232 test_run_rebase success -i
233 -test_have_prereq !REBASE_P || test_run_rebase failure -p
233 +test_have_prereq !REBASE_P || test_run_rebase success -p
234
235 test_run_rebase () {
236 result=$1
@@ -245,7 +245,7 @@ test_run_rebase () {
245 test_run_rebase success ''
246 test_run_rebase success -m
247 test_run_rebase success -i
248 -test_have_prereq !REBASE_P || test_run_rebase failure -p
248 +test_have_prereq !REBASE_P || test_run_rebase success -p
249 test_run_rebase success --rebase-merges
250
251 # m
t/t3424-rebase-empty.sh new
+70
@@ -0,0 +1,70 @@
1 +#!/bin/sh
2 +
3 +test_description='git rebase of commits that start or become empty'
4 +
5 +. ./test-lib.sh
6 +
7 +test_expect_success 'setup test repository' '
8 + test_write_lines 1 2 3 4 5 6 7 8 9 10 >numbers &&
9 + test_write_lines A B C D E F G H I J >letters &&
10 + git add numbers letters &&
11 + git commit -m A &&
12 +
13 + git branch upstream &&
14 + git branch localmods &&
15 +
16 + git checkout upstream &&
17 + test_write_lines A B C D E >letters &&
18 + git add letters &&
19 + git commit -m B &&
20 +
21 + test_write_lines 1 2 3 4 five 6 7 8 9 ten >numbers &&
22 + git add numbers &&
23 + git commit -m C &&
24 +
25 + git checkout localmods &&
26 + test_write_lines 1 2 3 4 five 6 7 8 9 10 >numbers &&
27 + git add numbers &&
28 + git commit -m C2 &&
29 +
30 + git commit --allow-empty -m D &&
31 +
32 + test_write_lines A B C D E >letters &&
33 + git add letters &&
34 + git commit -m "Five letters ought to be enough for anybody"
35 +'
36 +
37 +test_expect_failure 'rebase (am-backend) with a variety of empty commits' '
38 + test_when_finished "git rebase --abort" &&
39 + git checkout -B testing localmods &&
40 + # rebase (--am) should not drop commits that start empty
41 + git rebase upstream &&
42 +
43 + test_write_lines D C B A >expect &&
44 + git log --format=%s >actual &&
45 + test_cmp expect actual
46 +'
47 +
48 +test_expect_failure 'rebase --merge with a variety of empty commits' '
49 + test_when_finished "git rebase --abort" &&
50 + git checkout -B testing localmods &&
51 + # rebase --merge should not halt on the commit that becomes empty
52 + git rebase --merge upstream &&
53 +
54 + test_write_lines D C B A >expect &&
55 + git log --format=%s >actual &&
56 + test_cmp expect actual
57 +'
58 +
59 +test_expect_success 'rebase --interactive with a variety of empty commits' '
60 + git checkout -B testing localmods &&
61 + test_must_fail git rebase --interactive upstream &&
62 +
63 + git rebase --skip &&
64 +
65 + test_write_lines D C B A >expect &&
66 + git log --format=%s >actual &&
67 + test_cmp expect actual
68 +'
69 +
70 +test_done
t/t3427-rebase-subtree.sh
+6 -6
@@ -85,23 +85,23 @@ test_expect_failure REBASE_P 'Rebase -Xsubtree --keep-empty --preserve-merges --
85 verbose test "$(commit_message HEAD)" = "Empty commit"
86 '
87
88 -test_expect_success 'Rebase -Xsubtree --keep-empty --onto commit' '
88 +test_expect_success 'Rebase -Xsubtree --onto commit' '
89 reset_rebase &&
90 git checkout -b rebase-onto to-rebase &&
91 - test_must_fail git rebase -Xsubtree=files_subtree --keep-empty --onto files-master master &&
91 + test_must_fail git rebase -Xsubtree=files_subtree --onto files-master master &&
92 : first pick results in no changes &&
93 - git rebase --continue &&
93 + git rebase --skip &&
94 verbose test "$(commit_message HEAD~2)" = "master4" &&
95 verbose test "$(commit_message HEAD~)" = "files_subtree/master5" &&
96 verbose test "$(commit_message HEAD)" = "Empty commit"
97 '
98
99 -test_expect_success 'Rebase -Xsubtree --keep-empty --rebase-merges --onto commit' '
99 +test_expect_success 'Rebase -Xsubtree --rebase-merges --onto commit' '
100 reset_rebase &&
101 git checkout -b rebase-merges-onto to-rebase &&
102 - test_must_fail git rebase -Xsubtree=files_subtree --keep-empty --rebase-merges --onto files-master --root &&
102 + test_must_fail git rebase -Xsubtree=files_subtree --rebase-merges --onto files-master --root &&
103 : first pick results in no changes &&
104 - git rebase --continue &&
104 + git rebase --skip &&
105 verbose test "$(commit_message HEAD~2)" = "master4" &&
106 verbose test "$(commit_message HEAD~)" = "files_subtree/master5" &&
107 verbose test "$(commit_message HEAD)" = "Empty commit"