Skip to content

Commit e98c426

Browse files
newrengitster
authored andcommitted
rebase (interactive-backend): fix handling of commits that become empty
As established in the previous commit and commit b00bf1c (git-rebase: make --allow-empty-message the default, 2018-06-27), the behavior for rebase with different backends in various edge or corner cases is often more happenstance than design. This commit addresses another such corner case: commits which "become empty". A careful reader may note that there are two types of commits which would become empty due to a rebase: * [clean cherry-pick] Commits which are clean cherry-picks of upstream commits, as determined by `git log --cherry-mark ...`. Re-applying these commits would result in an empty set of changes and a duplicative commit message; i.e. these are commits that have "already been applied" upstream. * [become empty] Commits which are not empty to start, are not clean cherry-picks of upstream commits, but which still become empty after being rebased. This happens e.g. when a commit has changes which are a strict subset of the changes in an upstream commit, or when the changes of a commit can be found spread across or among several upstream commits. Clearly, in both cases the changes in the commit in question are found upstream already, but the commit message may not be in the latter case. When cherry-mark can determine a commit is already upstream, then because of how cherry-mark works this means the upstream commit message was about the *exact* same set of changes. Thus, the commit messages can be assumed to be fully interchangeable (and are in fact likely to be completely identical). As such, the clean cherry-pick case represents a case when there is no information to be gained by keeping the extra commit around. All rebase types have always dropped these commits, and no one to my knowledge has ever requested that we do otherwise. For many of the become empty cases (and likely even most), we will also be able to drop the commit without loss of information -- but this isn't quite always the case. Since these commits represent cases that were not clean cherry-picks, there is no upstream commit message explaining the same set of changes. Projects with good commit message hygiene will likely have the explanation from our commit message contained within or spread among the relevant upstream commits, but not all projects run that way. As such, the commit message of the commit being rebased may have reasoning that suggests additional changes that should be made to adapt to the new base, or it may have information that someone wants to add as a note to another commit, or perhaps someone even wants to create an empty commit with the commit message as-is. Junio commented on the "become-empty" types of commits as follows[1]: WRT a change that ends up being empty (as opposed to a change that is empty from the beginning), I'd think that the current behaviour is desireable one. "am" based rebase is solely to transplant an existing history and want to stop much less than "interactive" one whose purpose is to polish a series before making it publishable, and asking for confirmation ("this has become empty--do you want to drop it?") is more appropriate from the workflow point of view. [1] https://lore.kernel.org/git/[email protected]/ I would simply add that his arguments for "am"-based rebases actually apply to all non-explicitly-interactive rebases. Also, since we are stating that different cases should have different defaults, it may be worth providing a flag to allow users to select which behavior they want for these commits. Introduce a new command line flag for selecting the desired behavior: --empty={drop,keep,ask} with the definitions: drop: drop commits which become empty keep: keep commits which become empty ask: provide the user a chance to interact and pick what to do with commits which become empty on a case-by-case basis In line with Junio's suggestion, if the --empty flag is not specified, pick defaults as follows: explicitly interactive: ask otherwise: drop Signed-off-by: Elijah Newren <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent d48e5e2 commit e98c426

File tree

6 files changed

+183
-23
lines changed

6 files changed

+183
-23
lines changed

Documentation/git-rebase.txt

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,24 @@ See also INCOMPATIBLE OPTIONS below.
258258
original branch. The index and working tree are also left
259259
unchanged as a result.
260260

261+
--empty={drop,keep,ask}::
262+
How to handle commits that are not empty to start and are not
263+
clean cherry-picks of any upstream commit, but which become
264+
empty after rebasing (because they contain a subset of already
265+
upstream changes). With drop (the default), commits that
266+
become empty are dropped. With keep, such commits are kept.
267+
With ask (implied by --interactive), the rebase will halt when
268+
an empty commit is applied allowing you to choose whether to
269+
drop it, edit files more, or just commit the empty changes.
270+
Other options, like --exec, will use the default of drop unless
271+
-i/--interactive is explicitly specified.
272+
+
273+
Note that commits which start empty are kept, and commits which are
274+
clean cherry-picks (as determined by `git log --cherry-mark ...`) are
275+
always dropped.
276+
+
277+
See also INCOMPATIBLE OPTIONS below.
278+
261279
--keep-empty::
262280
No-op. Rebasing commits that started empty (had no change
263281
relative to their parent) used to fail and this option would
@@ -561,6 +579,7 @@ are incompatible with the following options:
561579
* --interactive
562580
* --exec
563581
* --keep-empty
582+
* --empty=
564583
* --edit-todo
565584
* --root when used in combination with --onto
566585

@@ -569,6 +588,7 @@ In addition, the following pairs of options are incompatible:
569588
* --preserve-merges and --interactive
570589
* --preserve-merges and --signoff
571590
* --preserve-merges and --rebase-merges
591+
* --preserve-merges and --empty=
572592
* --keep-base and --onto
573593
* --keep-base and --root
574594

@@ -585,9 +605,12 @@ commits that started empty, though these are rare in practice. It
585605
also drops commits that become empty and has no option for controlling
586606
this behavior.
587607

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.
608+
The interactive backend keeps intentionally empty commits. Similar to
609+
the am backend, by default the interactive backend drops commits that
610+
become empty unless -i/--interactive is specified (in which case it
611+
stops and asks the user what to do). The interactive backend also has
612+
an --empty={drop,keep,ask} option for changing the behavior of
613+
handling commits that become empty.
591614

592615
Directory rename detection
593616
~~~~~~~~~~~~~~~~~~~~~~~~~~

builtin/rebase.c

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,16 @@ enum rebase_type {
5050
REBASE_PRESERVE_MERGES
5151
};
5252

53+
enum empty_type {
54+
EMPTY_UNSPECIFIED = -1,
55+
EMPTY_DROP,
56+
EMPTY_KEEP,
57+
EMPTY_ASK
58+
};
59+
5360
struct rebase_options {
5461
enum rebase_type type;
62+
enum empty_type empty;
5563
const char *state_dir;
5664
struct commit *upstream;
5765
const char *upstream_name;
@@ -91,6 +99,7 @@ struct rebase_options {
9199

92100
#define REBASE_OPTIONS_INIT { \
93101
.type = REBASE_UNSPECIFIED, \
102+
.empty = EMPTY_UNSPECIFIED, \
94103
.flags = REBASE_NO_QUIET, \
95104
.git_am_opts = ARGV_ARRAY_INIT, \
96105
.git_format_patch_opt = STRBUF_INIT \
@@ -109,6 +118,8 @@ static struct replay_opts get_replay_opts(const struct rebase_options *opts)
109118
replay.allow_rerere_auto = opts->allow_rerere_autoupdate;
110119
replay.allow_empty = 1;
111120
replay.allow_empty_message = opts->allow_empty_message;
121+
replay.drop_redundant_commits = (opts->empty == EMPTY_DROP);
122+
replay.keep_redundant_commits = (opts->empty == EMPTY_KEEP);
112123
replay.verbose = opts->flags & REBASE_VERBOSE;
113124
replay.reschedule_failed_exec = opts->reschedule_failed_exec;
114125
replay.gpg_sign = xstrdup_or_null(opts->gpg_sign_opt);
@@ -444,6 +455,10 @@ static int parse_opt_keep_empty(const struct option *opt, const char *arg,
444455

445456
BUG_ON_OPT_ARG(arg);
446457

458+
/*
459+
* If we ever want to remap --keep-empty to --empty=keep, insert:
460+
* opts->empty = unset ? EMPTY_UNSPECIFIED : EMPTY_KEEP;
461+
*/
447462
opts->type = REBASE_INTERACTIVE;
448463
return 0;
449464
}
@@ -1350,6 +1365,29 @@ static int parse_opt_interactive(const struct option *opt, const char *arg,
13501365
return 0;
13511366
}
13521367

1368+
static enum empty_type parse_empty_value(const char *value)
1369+
{
1370+
if (!strcasecmp(value, "drop"))
1371+
return EMPTY_DROP;
1372+
else if (!strcasecmp(value, "keep"))
1373+
return EMPTY_KEEP;
1374+
else if (!strcasecmp(value, "ask"))
1375+
return EMPTY_ASK;
1376+
1377+
die(_("unrecognized empty type '%s'; valid values are \"drop\", \"keep\", and \"ask\"."), value);
1378+
}
1379+
1380+
static int parse_opt_empty(const struct option *opt, const char *arg, int unset)
1381+
{
1382+
struct rebase_options *options = opt->value;
1383+
enum empty_type value = parse_empty_value(arg);
1384+
1385+
BUG_ON_OPT_NEG(unset);
1386+
1387+
options->empty = value;
1388+
return 0;
1389+
}
1390+
13531391
static void NORETURN error_on_missing_default_upstream(void)
13541392
{
13551393
struct branch *current_branch = branch_get(NULL);
@@ -1494,6 +1532,9 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
14941532
"ignoring them"),
14951533
REBASE_PRESERVE_MERGES, PARSE_OPT_HIDDEN),
14961534
OPT_RERERE_AUTOUPDATE(&options.allow_rerere_autoupdate),
1535+
OPT_CALLBACK_F(0, "empty", &options, N_("{drop,keep,ask}"),
1536+
N_("how to handle commits that become empty"),
1537+
PARSE_OPT_NONEG, parse_opt_empty),
14971538
{ OPTION_CALLBACK, 'k', "keep-empty", &options, NULL,
14981539
N_("(DEPRECATED) keep empty commits"),
14991540
PARSE_OPT_NOARG | PARSE_OPT_HIDDEN,
@@ -1760,6 +1801,9 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
17601801
if (!(options.flags & REBASE_NO_QUIET))
17611802
argv_array_push(&options.git_am_opts, "-q");
17621803

1804+
if (options.empty != EMPTY_UNSPECIFIED)
1805+
imply_interactive(&options, "--empty");
1806+
17631807
if (gpg_sign) {
17641808
free(options.gpg_sign_opt);
17651809
options.gpg_sign_opt = xstrfmt("-S%s", gpg_sign);
@@ -1843,6 +1887,14 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
18431887
break;
18441888
}
18451889

1890+
if (options.empty == EMPTY_UNSPECIFIED) {
1891+
if (options.flags & REBASE_INTERACTIVE_EXPLICIT)
1892+
options.empty = EMPTY_ASK;
1893+
else if (exec.nr > 0)
1894+
options.empty = EMPTY_KEEP;
1895+
else
1896+
options.empty = EMPTY_DROP;
1897+
}
18461898
if (reschedule_failed_exec > 0 && !is_interactive(&options))
18471899
die(_("--reschedule-failed-exec requires "
18481900
"--exec or --interactive"));

sequencer.c

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,8 @@ static GIT_PATH_FUNC(rebase_path_strategy, "rebase-merge/strategy")
158158
static GIT_PATH_FUNC(rebase_path_strategy_opts, "rebase-merge/strategy_opts")
159159
static GIT_PATH_FUNC(rebase_path_allow_rerere_autoupdate, "rebase-merge/allow_rerere_autoupdate")
160160
static GIT_PATH_FUNC(rebase_path_reschedule_failed_exec, "rebase-merge/reschedule-failed-exec")
161+
static GIT_PATH_FUNC(rebase_path_drop_redundant_commits, "rebase-merge/drop_redundant_commits")
162+
static GIT_PATH_FUNC(rebase_path_keep_redundant_commits, "rebase-merge/keep_redundant_commits")
161163

162164
static int git_sequencer_config(const char *k, const char *v, void *cb)
163165
{
@@ -1483,7 +1485,11 @@ static int is_original_commit_empty(struct commit *commit)
14831485
}
14841486

14851487
/*
1486-
* Do we run "git commit" with "--allow-empty"?
1488+
* Should empty commits be allowed? Return status:
1489+
* <0: Error in is_index_unchanged(r) or is_original_commit_empty(commit)
1490+
* 0: Halt on empty commit
1491+
* 1: Allow empty commit
1492+
* 2: Drop empty commit
14871493
*/
14881494
static int allow_empty(struct repository *r,
14891495
struct replay_opts *opts,
@@ -1492,14 +1498,17 @@ static int allow_empty(struct repository *r,
14921498
int index_unchanged, originally_empty;
14931499

14941500
/*
1495-
* Three cases:
1501+
* Four cases:
14961502
*
14971503
* (1) we do not allow empty at all and error out.
14981504
*
1499-
* (2) we allow ones that were initially empty, but
1500-
* forbid the ones that become empty;
1505+
* (2) we allow ones that were initially empty, and
1506+
* just drop the ones that become empty
15011507
*
1502-
* (3) we allow both.
1508+
* (3) we allow ones that were initially empty, but
1509+
* halt for the ones that become empty;
1510+
*
1511+
* (4) we allow both.
15031512
*/
15041513
if (!opts->allow_empty)
15051514
return 0; /* let "git commit" barf as necessary */
@@ -1516,10 +1525,12 @@ static int allow_empty(struct repository *r,
15161525
originally_empty = is_original_commit_empty(commit);
15171526
if (originally_empty < 0)
15181527
return originally_empty;
1519-
if (!originally_empty)
1520-
return 0;
1521-
else
1528+
if (originally_empty)
15221529
return 1;
1530+
else if (opts->drop_redundant_commits)
1531+
return 2;
1532+
else
1533+
return 0;
15231534
}
15241535

15251536
static struct {
@@ -1730,7 +1741,7 @@ static int do_pick_commit(struct repository *r,
17301741
char *author = NULL;
17311742
struct commit_message msg = { NULL, NULL, NULL, NULL };
17321743
struct strbuf msgbuf = STRBUF_INIT;
1733-
int res, unborn = 0, reword = 0, allow;
1744+
int res, unborn = 0, reword = 0, allow, drop_commit;
17341745

17351746
if (opts->no_commit) {
17361747
/*
@@ -1935,13 +1946,20 @@ static int do_pick_commit(struct repository *r,
19351946
goto leave;
19361947
}
19371948

1949+
drop_commit = 0;
19381950
allow = allow_empty(r, opts, commit);
19391951
if (allow < 0) {
19401952
res = allow;
19411953
goto leave;
1942-
} else if (allow)
1954+
} else if (allow == 1) {
19431955
flags |= ALLOW_EMPTY;
1944-
if (!opts->no_commit) {
1956+
} else if (allow == 2) {
1957+
drop_commit = 1;
1958+
fprintf(stderr,
1959+
_("dropping %s %s -- patch contents already upstream\n"),
1960+
oid_to_hex(&commit->object.oid), msg.subject);
1961+
} /* else allow == 0 and there's nothing special to do */
1962+
if (!opts->no_commit && !drop_commit) {
19451963
if (author || command == TODO_REVERT || (flags & AMEND_MSG))
19461964
res = do_commit(r, msg_file, author, opts, flags);
19471965
else
@@ -2495,6 +2513,12 @@ static int read_populate_opts(struct replay_opts *opts)
24952513
if (file_exists(rebase_path_reschedule_failed_exec()))
24962514
opts->reschedule_failed_exec = 1;
24972515

2516+
if (file_exists(rebase_path_drop_redundant_commits()))
2517+
opts->drop_redundant_commits = 1;
2518+
2519+
if (file_exists(rebase_path_keep_redundant_commits()))
2520+
opts->keep_redundant_commits = 1;
2521+
24982522
read_strategy_opts(opts, &buf);
24992523
strbuf_release(&buf);
25002524

@@ -2574,6 +2598,10 @@ int write_basic_state(struct replay_opts *opts, const char *head_name,
25742598
write_file(rebase_path_gpg_sign_opt(), "-S%s\n", opts->gpg_sign);
25752599
if (opts->signoff)
25762600
write_file(rebase_path_signoff(), "--signoff\n");
2601+
if (opts->drop_redundant_commits)
2602+
write_file(rebase_path_drop_redundant_commits(), "%s", "");
2603+
if (opts->keep_redundant_commits)
2604+
write_file(rebase_path_keep_redundant_commits(), "%s", "");
25772605
if (opts->reschedule_failed_exec)
25782606
write_file(rebase_path_reschedule_failed_exec(), "%s", "");
25792607

sequencer.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ struct replay_opts {
3939
int allow_rerere_auto;
4040
int allow_empty;
4141
int allow_empty_message;
42+
int drop_redundant_commits;
4243
int keep_redundant_commits;
4344
int verbose;
4445
int quiet;

t/t3424-rebase-empty.sh

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ test_expect_success 'setup test repository' '
3434
git commit -m "Five letters ought to be enough for anybody"
3535
'
3636

37-
test_expect_failure 'rebase (am-backend) with a variety of empty commits' '
37+
test_expect_failure 'rebase (am-backend)' '
3838
test_when_finished "git rebase --abort" &&
3939
git checkout -B testing localmods &&
4040
# rebase (--am) should not drop commits that start empty
@@ -45,18 +45,74 @@ test_expect_failure 'rebase (am-backend) with a variety of empty commits' '
4545
test_cmp expect actual
4646
'
4747

48-
test_expect_failure 'rebase --merge with a variety of empty commits' '
49-
test_when_finished "git rebase --abort" &&
48+
test_expect_success 'rebase --merge --empty=drop' '
49+
git checkout -B testing localmods &&
50+
git rebase --merge --empty=drop upstream &&
51+
52+
test_write_lines D C B A >expect &&
53+
git log --format=%s >actual &&
54+
test_cmp expect actual
55+
'
56+
57+
test_expect_success 'rebase --merge uses default of --empty=drop' '
5058
git checkout -B testing localmods &&
51-
# rebase --merge should not halt on the commit that becomes empty
5259
git rebase --merge upstream &&
5360
5461
test_write_lines D C B A >expect &&
5562
git log --format=%s >actual &&
5663
test_cmp expect actual
5764
'
5865

59-
test_expect_success 'rebase --interactive with a variety of empty commits' '
66+
test_expect_success 'rebase --merge --empty=keep' '
67+
git checkout -B testing localmods &&
68+
git rebase --merge --empty=keep upstream &&
69+
70+
test_write_lines D C2 C B A >expect &&
71+
git log --format=%s >actual &&
72+
test_cmp expect actual
73+
'
74+
75+
test_expect_success 'rebase --merge --empty=ask' '
76+
git checkout -B testing localmods &&
77+
test_must_fail git rebase --merge --empty=ask upstream &&
78+
79+
git rebase --skip &&
80+
81+
test_write_lines D C B A >expect &&
82+
git log --format=%s >actual &&
83+
test_cmp expect actual
84+
'
85+
86+
test_expect_success 'rebase --interactive --empty=drop' '
87+
git checkout -B testing localmods &&
88+
git rebase --interactive --empty=drop upstream &&
89+
90+
test_write_lines D C B A >expect &&
91+
git log --format=%s >actual &&
92+
test_cmp expect actual
93+
'
94+
95+
test_expect_success 'rebase --interactive --empty=keep' '
96+
git checkout -B testing localmods &&
97+
git rebase --interactive --empty=keep upstream &&
98+
99+
test_write_lines D C2 C B A >expect &&
100+
git log --format=%s >actual &&
101+
test_cmp expect actual
102+
'
103+
104+
test_expect_success 'rebase --interactive --empty=ask' '
105+
git checkout -B testing localmods &&
106+
test_must_fail git rebase --interactive --empty=ask upstream &&
107+
108+
git rebase --skip &&
109+
110+
test_write_lines D C B A >expect &&
111+
git log --format=%s >actual &&
112+
test_cmp expect actual
113+
'
114+
115+
test_expect_success 'rebase --interactive uses default of --empty=ask' '
60116
git checkout -B testing localmods &&
61117
test_must_fail git rebase --interactive upstream &&
62118

0 commit comments

Comments
 (0)