From 22a69fda197f78410190469bf0fcb51f6f69730f Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Thu, 16 Jan 2020 06:14:15 +0000 Subject: git-rebase.txt: update description of --allow-empty-message Commit b00bf1c9a8dd ("git-rebase: make --allow-empty-message the default", 2018-06-27) made --allow-empty-message the default and thus turned --allow-empty-message into a no-op but did not update the documentation to reflect this. Update the documentation now, and hide the option from the normal -h output since it is not useful. Signed-off-by: Elijah Newren Signed-off-by: Junio C Hamano diff --git a/Documentation/git-rebase.txt b/Documentation/git-rebase.txt index 0c4f038..c83be7f 100644 --- a/Documentation/git-rebase.txt +++ b/Documentation/git-rebase.txt @@ -265,9 +265,10 @@ See also INCOMPATIBLE OPTIONS below. See also INCOMPATIBLE OPTIONS below. --allow-empty-message:: - By default, rebasing commits with an empty message will fail. - This option overrides that behavior, allowing commits with empty - messages to be rebased. + No-op. Rebasing commits with an empty message used to fail + and this option would override that behavior, allowing commits + with empty messages to be rebased. Now commits with an empty + message do not cause rebasing to halt. + See also INCOMPATIBLE OPTIONS below. diff --git a/builtin/rebase.c b/builtin/rebase.c index 8081741..faa4e0d 100644 --- a/builtin/rebase.c +++ b/builtin/rebase.c @@ -453,8 +453,9 @@ int cmd_rebase__interactive(int argc, const char **argv, const char *prefix) OPT_NEGBIT(0, "ff", &opts.flags, N_("allow fast-forward"), REBASE_FORCE), OPT_BOOL(0, "keep-empty", &opts.keep_empty, N_("keep empty commits")), - OPT_BOOL(0, "allow-empty-message", &opts.allow_empty_message, - N_("allow commits with empty messages")), + OPT_BOOL_F(0, "allow-empty-message", &opts.allow_empty_message, + N_("allow commits with empty messages"), + PARSE_OPT_HIDDEN), OPT_BOOL(0, "rebase-merges", &opts.rebase_merges, N_("rebase merge commits")), OPT_BOOL(0, "rebase-cousins", &opts.rebase_cousins, N_("keep original branch points of cousins")), @@ -1495,9 +1496,10 @@ int cmd_rebase(int argc, const char **argv, const char *prefix) OPT_STRING_LIST('x', "exec", &exec, N_("exec"), N_("add exec lines after each commit of the " "editable list")), - OPT_BOOL(0, "allow-empty-message", - &options.allow_empty_message, - N_("allow rebasing commits with empty messages")), + OPT_BOOL_F(0, "allow-empty-message", + &options.allow_empty_message, + N_("allow rebasing commits with empty messages"), + PARSE_OPT_HIDDEN), {OPTION_STRING, 'r', "rebase-merges", &rebase_merges, N_("mode"), N_("try to rebase merges instead of skipping them"), -- cgit v0.10.2-6-g49f6 From a9ae8fde2eb0c00723d850ba23ea8401864fab83 Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Thu, 16 Jan 2020 06:14:16 +0000 Subject: t3404: directly test the behavior of interest t3404.3 is a simple test added by commit d078c3910689 ("t3404: todo list with commented-out commands only aborts", 2018-08-10) which was designed to test a todo list that only contained commented-out commands. There were two problems with this test: (1) its title did not reflect the purpose of the test, and (2) it tested the desired behavior through a side-effect of other functionality instead of directly testing the desired behavior discussed in the commit message. Modify the test to directly test the desired behavior and update the test title. Signed-off-by: Elijah Newren Signed-off-by: Junio C Hamano diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh index ae6e55c..c41531f 100755 --- a/t/t3404-rebase-interactive.sh +++ b/t/t3404-rebase-interactive.sh @@ -72,15 +72,16 @@ test_expect_success 'rebase --keep-empty' ' test_line_count = 6 actual ' -test_expect_success 'rebase -i with empty HEAD' ' +test_expect_success 'rebase -i with empty todo list' ' cat >expect <<-\EOF && error: nothing to do EOF ( set_fake_editor && - test_must_fail env FAKE_LINES="1 exec_true" \ - git rebase -i HEAD^ >actual 2>&1 + test_must_fail env FAKE_LINES="#" \ + git rebase -i HEAD^ >output 2>&1 ) && + tail -n 1 output >actual && # Ignore output about changing todo list test_i18ncmp expect actual ' -- cgit v0.10.2-6-g49f6 From d48e5e21da980c6d439655a1292d0332b341c7d1 Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Sat, 15 Feb 2020 21:36:24 +0000 Subject: 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 Signed-off-by: Junio C Hamano diff --git a/Documentation/git-rebase.txt b/Documentation/git-rebase.txt index c83be7f..1d19542 100644 --- a/Documentation/git-rebase.txt +++ b/Documentation/git-rebase.txt @@ -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 ~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/builtin/rebase.c b/builtin/rebase.c index faa4e0d..537b324 100644 --- a/builtin/rebase.c +++ b/builtin/rebase.c @@ -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 []"), 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); diff --git a/rebase-interactive.c b/rebase-interactive.c index aa18ae8..7e7b406 100644 --- a/rebase-interactive.c +++ b/rebase-interactive.c @@ -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, diff --git a/rebase-interactive.h b/rebase-interactive.h index 44dbb06..05354ca 100644 --- a/rebase-interactive.h +++ b/rebase-interactive.h @@ -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, diff --git a/sequencer.c b/sequencer.c index b9dbf1a..c21fc20 100644 --- a/sequencer.c +++ b/sequencer.c @@ -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); diff --git a/sequencer.h b/sequencer.h index 9f9ae29..c165e0f 100644 --- a/sequencer.h +++ b/sequencer.h @@ -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) diff --git a/t/t3421-rebase-topology-linear.sh b/t/t3421-rebase-topology-linear.sh index 325072b..57334dc 100755 --- a/t/t3421-rebase-topology-linear.sh +++ b/t/t3421-rebase-topology-linear.sh @@ -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 diff --git a/t/t3424-rebase-empty.sh b/t/t3424-rebase-empty.sh new file mode 100755 index 0000000..3b716e9 --- /dev/null +++ b/t/t3424-rebase-empty.sh @@ -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 diff --git a/t/t3427-rebase-subtree.sh b/t/t3427-rebase-subtree.sh index bec48e6..8dceef6 100755 --- a/t/t3427-rebase-subtree.sh +++ b/t/t3427-rebase-subtree.sh @@ -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" -- cgit v0.10.2-6-g49f6 From e98c4269c86019bfe057a91b4305f784365b6f0b Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Sat, 15 Feb 2020 21:36:25 +0000 Subject: rebase (interactive-backend): fix handling of commits that become empty As established in the previous commit and commit b00bf1c9a8dd (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/xmqqfu1fswdh.fsf@gitster-ct.c.googlers.com/ 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 Signed-off-by: Junio C Hamano diff --git a/Documentation/git-rebase.txt b/Documentation/git-rebase.txt index 1d19542..e1c6f91 100644 --- a/Documentation/git-rebase.txt +++ b/Documentation/git-rebase.txt @@ -258,6 +258,24 @@ See also INCOMPATIBLE OPTIONS below. original branch. The index and working tree are also left unchanged as a result. +--empty={drop,keep,ask}:: + How to handle commits that are not empty to start and are not + clean cherry-picks of any upstream commit, but which become + empty after rebasing (because they contain a subset of already + upstream changes). With drop (the default), commits that + become empty are dropped. With keep, such commits are kept. + With ask (implied by --interactive), the rebase will halt when + an empty commit is applied allowing you to choose whether to + drop it, edit files more, or just commit the empty changes. + Other options, like --exec, will use the default of drop unless + -i/--interactive is explicitly specified. ++ +Note that commits which start empty are kept, and commits which are +clean cherry-picks (as determined by `git log --cherry-mark ...`) are +always dropped. ++ +See also INCOMPATIBLE OPTIONS below. + --keep-empty:: No-op. Rebasing commits that started empty (had no change relative to their parent) used to fail and this option would @@ -561,6 +579,7 @@ are incompatible with the following options: * --interactive * --exec * --keep-empty + * --empty= * --edit-todo * --root when used in combination with --onto @@ -569,6 +588,7 @@ In addition, the following pairs of options are incompatible: * --preserve-merges and --interactive * --preserve-merges and --signoff * --preserve-merges and --rebase-merges + * --preserve-merges and --empty= * --keep-base and --onto * --keep-base and --root @@ -585,9 +605,12 @@ 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 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. +The interactive backend keeps intentionally empty commits. Similar to +the am backend, by default the interactive backend drops commits that +become empty unless -i/--interactive is specified (in which case it +stops and asks the user what to do). The interactive backend also has +an --empty={drop,keep,ask} option for changing the behavior of +handling commits that become empty. Directory rename detection ~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/builtin/rebase.c b/builtin/rebase.c index 537b324..669690f 100644 --- a/builtin/rebase.c +++ b/builtin/rebase.c @@ -50,8 +50,16 @@ enum rebase_type { REBASE_PRESERVE_MERGES }; +enum empty_type { + EMPTY_UNSPECIFIED = -1, + EMPTY_DROP, + EMPTY_KEEP, + EMPTY_ASK +}; + struct rebase_options { enum rebase_type type; + enum empty_type empty; const char *state_dir; struct commit *upstream; const char *upstream_name; @@ -91,6 +99,7 @@ struct rebase_options { #define REBASE_OPTIONS_INIT { \ .type = REBASE_UNSPECIFIED, \ + .empty = EMPTY_UNSPECIFIED, \ .flags = REBASE_NO_QUIET, \ .git_am_opts = ARGV_ARRAY_INIT, \ .git_format_patch_opt = STRBUF_INIT \ @@ -109,6 +118,8 @@ static struct replay_opts get_replay_opts(const struct rebase_options *opts) replay.allow_rerere_auto = opts->allow_rerere_autoupdate; replay.allow_empty = 1; replay.allow_empty_message = opts->allow_empty_message; + replay.drop_redundant_commits = (opts->empty == EMPTY_DROP); + replay.keep_redundant_commits = (opts->empty == EMPTY_KEEP); replay.verbose = opts->flags & REBASE_VERBOSE; replay.reschedule_failed_exec = opts->reschedule_failed_exec; 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, BUG_ON_OPT_ARG(arg); + /* + * If we ever want to remap --keep-empty to --empty=keep, insert: + * opts->empty = unset ? EMPTY_UNSPECIFIED : EMPTY_KEEP; + */ opts->type = REBASE_INTERACTIVE; return 0; } @@ -1350,6 +1365,29 @@ static int parse_opt_interactive(const struct option *opt, const char *arg, return 0; } +static enum empty_type parse_empty_value(const char *value) +{ + if (!strcasecmp(value, "drop")) + return EMPTY_DROP; + else if (!strcasecmp(value, "keep")) + return EMPTY_KEEP; + else if (!strcasecmp(value, "ask")) + return EMPTY_ASK; + + die(_("unrecognized empty type '%s'; valid values are \"drop\", \"keep\", and \"ask\"."), value); +} + +static int parse_opt_empty(const struct option *opt, const char *arg, int unset) +{ + struct rebase_options *options = opt->value; + enum empty_type value = parse_empty_value(arg); + + BUG_ON_OPT_NEG(unset); + + options->empty = value; + return 0; +} + static void NORETURN error_on_missing_default_upstream(void) { struct branch *current_branch = branch_get(NULL); @@ -1494,6 +1532,9 @@ 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_CALLBACK_F(0, "empty", &options, N_("{drop,keep,ask}"), + N_("how to handle commits that become empty"), + PARSE_OPT_NONEG, parse_opt_empty), { OPTION_CALLBACK, 'k', "keep-empty", &options, NULL, N_("(DEPRECATED) keep empty commits"), PARSE_OPT_NOARG | PARSE_OPT_HIDDEN, @@ -1760,6 +1801,9 @@ 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.empty != EMPTY_UNSPECIFIED) + imply_interactive(&options, "--empty"); + if (gpg_sign) { free(options.gpg_sign_opt); options.gpg_sign_opt = xstrfmt("-S%s", gpg_sign); @@ -1843,6 +1887,14 @@ int cmd_rebase(int argc, const char **argv, const char *prefix) break; } + if (options.empty == EMPTY_UNSPECIFIED) { + if (options.flags & REBASE_INTERACTIVE_EXPLICIT) + options.empty = EMPTY_ASK; + else if (exec.nr > 0) + options.empty = EMPTY_KEEP; + else + options.empty = EMPTY_DROP; + } if (reschedule_failed_exec > 0 && !is_interactive(&options)) die(_("--reschedule-failed-exec requires " "--exec or --interactive")); diff --git a/sequencer.c b/sequencer.c index c21fc20..fdb8f91 100644 --- a/sequencer.c +++ b/sequencer.c @@ -158,6 +158,8 @@ static GIT_PATH_FUNC(rebase_path_strategy, "rebase-merge/strategy") static GIT_PATH_FUNC(rebase_path_strategy_opts, "rebase-merge/strategy_opts") static GIT_PATH_FUNC(rebase_path_allow_rerere_autoupdate, "rebase-merge/allow_rerere_autoupdate") static GIT_PATH_FUNC(rebase_path_reschedule_failed_exec, "rebase-merge/reschedule-failed-exec") +static GIT_PATH_FUNC(rebase_path_drop_redundant_commits, "rebase-merge/drop_redundant_commits") +static GIT_PATH_FUNC(rebase_path_keep_redundant_commits, "rebase-merge/keep_redundant_commits") static int git_sequencer_config(const char *k, const char *v, void *cb) { @@ -1483,7 +1485,11 @@ static int is_original_commit_empty(struct commit *commit) } /* - * Do we run "git commit" with "--allow-empty"? + * Should empty commits be allowed? Return status: + * <0: Error in is_index_unchanged(r) or is_original_commit_empty(commit) + * 0: Halt on empty commit + * 1: Allow empty commit + * 2: Drop empty commit */ static int allow_empty(struct repository *r, struct replay_opts *opts, @@ -1492,14 +1498,17 @@ static int allow_empty(struct repository *r, int index_unchanged, originally_empty; /* - * Three cases: + * Four cases: * * (1) we do not allow empty at all and error out. * - * (2) we allow ones that were initially empty, but - * forbid the ones that become empty; + * (2) we allow ones that were initially empty, and + * just drop the ones that become empty * - * (3) we allow both. + * (3) we allow ones that were initially empty, but + * halt for the ones that become empty; + * + * (4) we allow both. */ if (!opts->allow_empty) return 0; /* let "git commit" barf as necessary */ @@ -1516,10 +1525,12 @@ static int allow_empty(struct repository *r, originally_empty = is_original_commit_empty(commit); if (originally_empty < 0) return originally_empty; - if (!originally_empty) - return 0; - else + if (originally_empty) return 1; + else if (opts->drop_redundant_commits) + return 2; + else + return 0; } static struct { @@ -1730,7 +1741,7 @@ static int do_pick_commit(struct repository *r, char *author = NULL; struct commit_message msg = { NULL, NULL, NULL, NULL }; struct strbuf msgbuf = STRBUF_INIT; - int res, unborn = 0, reword = 0, allow; + int res, unborn = 0, reword = 0, allow, drop_commit; if (opts->no_commit) { /* @@ -1935,13 +1946,20 @@ static int do_pick_commit(struct repository *r, goto leave; } + drop_commit = 0; allow = allow_empty(r, opts, commit); if (allow < 0) { res = allow; goto leave; - } else if (allow) + } else if (allow == 1) { flags |= ALLOW_EMPTY; - if (!opts->no_commit) { + } else if (allow == 2) { + drop_commit = 1; + fprintf(stderr, + _("dropping %s %s -- patch contents already upstream\n"), + oid_to_hex(&commit->object.oid), msg.subject); + } /* else allow == 0 and there's nothing special to do */ + if (!opts->no_commit && !drop_commit) { if (author || command == TODO_REVERT || (flags & AMEND_MSG)) res = do_commit(r, msg_file, author, opts, flags); else @@ -2495,6 +2513,12 @@ static int read_populate_opts(struct replay_opts *opts) if (file_exists(rebase_path_reschedule_failed_exec())) opts->reschedule_failed_exec = 1; + if (file_exists(rebase_path_drop_redundant_commits())) + opts->drop_redundant_commits = 1; + + if (file_exists(rebase_path_keep_redundant_commits())) + opts->keep_redundant_commits = 1; + read_strategy_opts(opts, &buf); strbuf_release(&buf); @@ -2574,6 +2598,10 @@ int write_basic_state(struct replay_opts *opts, const char *head_name, write_file(rebase_path_gpg_sign_opt(), "-S%s\n", opts->gpg_sign); if (opts->signoff) write_file(rebase_path_signoff(), "--signoff\n"); + if (opts->drop_redundant_commits) + write_file(rebase_path_drop_redundant_commits(), "%s", ""); + if (opts->keep_redundant_commits) + write_file(rebase_path_keep_redundant_commits(), "%s", ""); if (opts->reschedule_failed_exec) write_file(rebase_path_reschedule_failed_exec(), "%s", ""); diff --git a/sequencer.h b/sequencer.h index c165e0f..3b0ab91 100644 --- a/sequencer.h +++ b/sequencer.h @@ -39,6 +39,7 @@ struct replay_opts { int allow_rerere_auto; int allow_empty; int allow_empty_message; + int drop_redundant_commits; int keep_redundant_commits; int verbose; int quiet; diff --git a/t/t3424-rebase-empty.sh b/t/t3424-rebase-empty.sh index 3b716e9..cfb1ebc 100755 --- a/t/t3424-rebase-empty.sh +++ b/t/t3424-rebase-empty.sh @@ -34,7 +34,7 @@ test_expect_success 'setup test repository' ' git commit -m "Five letters ought to be enough for anybody" ' -test_expect_failure 'rebase (am-backend) with a variety of empty commits' ' +test_expect_failure 'rebase (am-backend)' ' test_when_finished "git rebase --abort" && git checkout -B testing localmods && # rebase (--am) should not drop commits that start empty @@ -45,10 +45,17 @@ test_expect_failure 'rebase (am-backend) with a variety of empty commits' ' test_cmp expect actual ' -test_expect_failure 'rebase --merge with a variety of empty commits' ' - test_when_finished "git rebase --abort" && +test_expect_success 'rebase --merge --empty=drop' ' + git checkout -B testing localmods && + git rebase --merge --empty=drop upstream && + + test_write_lines D C B A >expect && + git log --format=%s >actual && + test_cmp expect actual +' + +test_expect_success 'rebase --merge uses default of --empty=drop' ' 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 && @@ -56,7 +63,56 @@ test_expect_failure 'rebase --merge with a variety of empty commits' ' test_cmp expect actual ' -test_expect_success 'rebase --interactive with a variety of empty commits' ' +test_expect_success 'rebase --merge --empty=keep' ' + git checkout -B testing localmods && + git rebase --merge --empty=keep upstream && + + test_write_lines D C2 C B A >expect && + git log --format=%s >actual && + test_cmp expect actual +' + +test_expect_success 'rebase --merge --empty=ask' ' + git checkout -B testing localmods && + test_must_fail git rebase --merge --empty=ask upstream && + + git rebase --skip && + + test_write_lines D C B A >expect && + git log --format=%s >actual && + test_cmp expect actual +' + +test_expect_success 'rebase --interactive --empty=drop' ' + git checkout -B testing localmods && + git rebase --interactive --empty=drop upstream && + + test_write_lines D C B A >expect && + git log --format=%s >actual && + test_cmp expect actual +' + +test_expect_success 'rebase --interactive --empty=keep' ' + git checkout -B testing localmods && + git rebase --interactive --empty=keep upstream && + + test_write_lines D C2 C B A >expect && + git log --format=%s >actual && + test_cmp expect actual +' + +test_expect_success 'rebase --interactive --empty=ask' ' + git checkout -B testing localmods && + test_must_fail git rebase --interactive --empty=ask upstream && + + git rebase --skip && + + test_write_lines D C B A >expect && + git log --format=%s >actual && + test_cmp expect actual +' + +test_expect_success 'rebase --interactive uses default of --empty=ask' ' git checkout -B testing localmods && test_must_fail git rebase --interactive upstream && diff --git a/t/t3427-rebase-subtree.sh b/t/t3427-rebase-subtree.sh index 8dceef6..79e43a3 100755 --- a/t/t3427-rebase-subtree.sh +++ b/t/t3427-rebase-subtree.sh @@ -85,10 +85,10 @@ test_expect_failure REBASE_P 'Rebase -Xsubtree --keep-empty --preserve-merges -- verbose test "$(commit_message HEAD)" = "Empty commit" ' -test_expect_success 'Rebase -Xsubtree --onto commit' ' +test_expect_success 'Rebase -Xsubtree --empty=ask --onto commit' ' reset_rebase && git checkout -b rebase-onto to-rebase && - test_must_fail git rebase -Xsubtree=files_subtree --onto files-master master && + test_must_fail git rebase -Xsubtree=files_subtree --empty=ask --onto files-master master && : first pick results in no changes && git rebase --skip && verbose test "$(commit_message HEAD~2)" = "master4" && @@ -96,10 +96,10 @@ test_expect_success 'Rebase -Xsubtree --onto commit' ' verbose test "$(commit_message HEAD)" = "Empty commit" ' -test_expect_success 'Rebase -Xsubtree --rebase-merges --onto commit' ' +test_expect_success 'Rebase -Xsubtree --empty=ask --rebase-merges --onto commit' ' reset_rebase && git checkout -b rebase-merges-onto to-rebase && - test_must_fail git rebase -Xsubtree=files_subtree --rebase-merges --onto files-master --root && + test_must_fail git rebase -Xsubtree=files_subtree --empty=ask --rebase-merges --onto files-master --root && : first pick results in no changes && git rebase --skip && verbose test "$(commit_message HEAD~2)" = "master4" && -- cgit v0.10.2-6-g49f6 From 7db00f0b3bc2c0bde616d45cf6e3a130e74e3024 Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Sat, 15 Feb 2020 21:36:26 +0000 Subject: t3406: simplify an already simple test When the merge backend was re-implemented on top of the interactive backend, the output of rebase --merge changed a little. This change allowed this test to be simplified, though it wasn't noticed until now. Simplify the testcase a little. Signed-off-by: Elijah Newren Signed-off-by: Junio C Hamano diff --git a/t/t3406-rebase-message.sh b/t/t3406-rebase-message.sh index b393e1e..0c2c569 100755 --- a/t/t3406-rebase-message.sh +++ b/t/t3406-rebase-message.sh @@ -18,11 +18,8 @@ test_expect_success 'setup' ' ' test_expect_success 'rebase -m' ' - git rebase -m master >report && - >expect && - sed -n -e "/^Already applied: /p" \ - -e "/^Committed: /p" report >actual && - test_cmp expect actual + git rebase -m master >actual && + test_must_be_empty actual ' test_expect_success 'rebase against master twice' ' -- cgit v0.10.2-6-g49f6 From 8a997ed132f7b8709d1752b64779566600345375 Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Sat, 15 Feb 2020 21:36:27 +0000 Subject: rebase, sequencer: remove the broken GIT_QUIET handling The GIT_QUIET environment variable was used to signal the non-am backends that the rebase should perform quietly. The preserve-merges backend does not make use of the quiet flag anywhere (other than to write out its state whenever it writes state), and this mechanism was broken in the conversion from shell to C. Since this environment variable was specifically designed for scripts and the only backend that would still use it is no longer a script, just gut this code. A subsequent commit will fix --quiet for the interactive/merge backend in a different way. Signed-off-by: Elijah Newren Signed-off-by: Junio C Hamano diff --git a/builtin/rebase.c b/builtin/rebase.c index 669690f..7551f95 100644 --- a/builtin/rebase.c +++ b/builtin/rebase.c @@ -699,8 +699,8 @@ static int rebase_write_basic_state(struct rebase_options *opts) opts->onto ? oid_to_hex(&opts->onto->object.oid) : ""); write_file(state_dir_path("orig-head", opts), "%s", oid_to_hex(&opts->orig_head)); - write_file(state_dir_path("quiet", opts), "%s", - opts->flags & REBASE_NO_QUIET ? "" : "t"); + if (!(opts->flags & REBASE_NO_QUIET)) + write_file(state_dir_path("quiet", opts), "%s", ""); if (opts->flags & REBASE_VERBOSE) write_file(state_dir_path("verbose", opts), "%s", ""); if (opts->strategy) @@ -1153,8 +1153,6 @@ static int run_specific_rebase(struct rebase_options *opts, enum action action) add_var(&script_snippet, "revisions", opts->revisions); add_var(&script_snippet, "restrict_revision", opts->restrict_revision ? oid_to_hex(&opts->restrict_revision->object.oid) : NULL); - add_var(&script_snippet, "GIT_QUIET", - opts->flags & REBASE_NO_QUIET ? "" : "t"); sq_quote_argv_pretty(&buf, opts->git_am_opts.argv); add_var(&script_snippet, "git_am_opt", buf.buf); strbuf_release(&buf); diff --git a/sequencer.c b/sequencer.c index fdb8f91..f475d2a 100644 --- a/sequencer.c +++ b/sequencer.c @@ -2570,8 +2570,6 @@ static void write_strategy_opts(struct replay_opts *opts) int write_basic_state(struct replay_opts *opts, const char *head_name, struct commit *onto, const char *orig_head) { - const char *quiet = getenv("GIT_QUIET"); - if (head_name) write_file(rebase_path_head_name(), "%s\n", head_name); if (onto) @@ -2580,8 +2578,8 @@ int write_basic_state(struct replay_opts *opts, const char *head_name, if (orig_head) write_file(rebase_path_orig_head(), "%s\n", orig_head); - if (quiet) - write_file(rebase_path_quiet(), "%s\n", quiet); + if (opts->quiet) + write_file(rebase_path_quiet(), "%s", ""); if (opts->verbose) write_file(rebase_path_verbose(), "%s", ""); if (opts->strategy) -- cgit v0.10.2-6-g49f6 From 55d2b6d785da8c49d693d989c22f47622783903a Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Sat, 15 Feb 2020 21:36:28 +0000 Subject: rebase: make sure to pass along the quiet flag to the sequencer Signed-off-by: Elijah Newren Signed-off-by: Junio C Hamano diff --git a/builtin/rebase.c b/builtin/rebase.c index 7551f95..1a66413 100644 --- a/builtin/rebase.c +++ b/builtin/rebase.c @@ -120,6 +120,7 @@ static struct replay_opts get_replay_opts(const struct rebase_options *opts) replay.allow_empty_message = opts->allow_empty_message; replay.drop_redundant_commits = (opts->empty == EMPTY_DROP); replay.keep_redundant_commits = (opts->empty == EMPTY_KEEP); + replay.quiet = !(opts->flags & REBASE_NO_QUIET); replay.verbose = opts->flags & REBASE_VERBOSE; replay.reschedule_failed_exec = opts->reschedule_failed_exec; replay.gpg_sign = xstrdup_or_null(opts->gpg_sign_opt); @@ -1476,7 +1477,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix) N_("allow pre-rebase hook to run")), OPT_NEGBIT('q', "quiet", &options.flags, N_("be quiet. implies --no-stat"), - REBASE_NO_QUIET| REBASE_VERBOSE | REBASE_DIFFSTAT), + REBASE_NO_QUIET | REBASE_VERBOSE | REBASE_DIFFSTAT), OPT_BIT('v', "verbose", &options.flags, N_("display a diffstat of what changed upstream"), REBASE_NO_QUIET | REBASE_VERBOSE | REBASE_DIFFSTAT), diff --git a/t/t3400-rebase.sh b/t/t3400-rebase.sh index 221b35f..79762b9 100755 --- a/t/t3400-rebase.sh +++ b/t/t3400-rebase.sh @@ -206,12 +206,18 @@ test_expect_success 'cherry-picked commits and fork-point work together' ' test_cmp expect D ' -test_expect_success 'rebase -q is quiet' ' +test_expect_success 'rebase --am -q is quiet' ' git checkout -b quiet topic && git rebase -q master >output.out 2>&1 && test_must_be_empty output.out ' +test_expect_success 'rebase --merge -q is quiet' ' + git checkout -B quiet topic && + git rebase --merge -q master >output.out 2>&1 && + test_must_be_empty output.out +' + test_expect_success 'Rebase a commit that sprinkles CRs in' ' ( echo "One" && -- cgit v0.10.2-6-g49f6 From 93122c985a3671bd4e15b3dbc6b457c981eb8c23 Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Sat, 15 Feb 2020 21:36:29 +0000 Subject: rebase: fix handling of restrict_revision restrict_revision in the original shell script was an excluded revision range. It is also treated that way by the am-backend. In the conversion from shell to C (see commit 6ab54d17be3f ("rebase -i: implement the logic to initialize $revisions in C", 2018-08-28)), the interactive-backend accidentally treated it as a positive revision rather than a negated one. This was missed as there were no tests in the testsuite that tested an interactive rebase with fork-point behavior. Signed-off-by: Elijah Newren Signed-off-by: Junio C Hamano diff --git a/builtin/rebase.c b/builtin/rebase.c index 1a66413..8264a92 100644 --- a/builtin/rebase.c +++ b/builtin/rebase.c @@ -348,8 +348,8 @@ static int do_interactive_rebase(struct rebase_options *opts, unsigned flags) argv_array_pushl(&make_script_args, "", revisions, NULL); if (opts->restrict_revision) - argv_array_push(&make_script_args, - oid_to_hex(&opts->restrict_revision->object.oid)); + argv_array_pushf(&make_script_args, "^%s", + oid_to_hex(&opts->restrict_revision->object.oid)); ret = sequencer_make_script(the_repository, &todo_list.buf, make_script_args.argc, make_script_args.argv, diff --git a/t/t3400-rebase.sh b/t/t3400-rebase.sh index 79762b9..71fd639 100755 --- a/t/t3400-rebase.sh +++ b/t/t3400-rebase.sh @@ -165,11 +165,29 @@ test_expect_success 'rebase works with format.useAutoBase' ' git rebase master ' -test_expect_success 'default to common base in @{upstream}s reflog if no upstream arg' ' +test_expect_success 'default to common base in @{upstream}s reflog if no upstream arg (--merge)' ' git checkout -b default-base master && git checkout -b default topic && git config branch.default.remote . && git config branch.default.merge refs/heads/default-base && + git rebase --merge && + git rev-parse --verify default-base >expect && + git rev-parse default~1 >actual && + test_cmp expect actual && + git checkout default-base && + git reset --hard HEAD^ && + git checkout default && + git rebase --merge && + git rev-parse --verify default-base >expect && + git rev-parse default~1 >actual && + test_cmp expect actual +' + +test_expect_success 'default to common base in @{upstream}s reflog if no upstream arg' ' + git checkout -B default-base master && + git checkout -B default topic && + git config branch.default.remote . && + git config branch.default.merge refs/heads/default-base && git rebase && git rev-parse --verify default-base >expect && git rev-parse default~1 >actual && -- cgit v0.10.2-6-g49f6 From 9a70f3d4ae673e02e78b1061fcc8fc01bc8d9276 Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Sat, 15 Feb 2020 21:36:30 +0000 Subject: t3432: make these tests work with either am or merge backends t3432 had several stress tests for can_fast_forward(), whose intent was to ensure we were using the optimization of just fast forwarding when possible. However, these tests verified that fast forwards had happened based on the output that rebase printed to the terminal. We can instead test more directly that we actually fast-forwarded by checking the reflog, which also has the side effect of making the tests applicable for the merge/interactive backend. This change does lose the distinction between "noop" and "noop-force", but as stated in commit c9efc216830f ("t3432: test for --no-ff's interaction with fast-forward", 2019-08-27) which introduced that distinction: "These tests aren't supposed to endorse the status quo, just test for what we're currently doing.". This change does not actually run these tests with the merge/interactive backend; instead this is just a preparatory commit. A subsequent commit which fixes can_fast_forward() to work with that backend will then also change t3432 to add tests of that backend as well. Signed-off-by: Elijah Newren Signed-off-by: Junio C Hamano diff --git a/t/t3432-rebase-fast-forward.sh b/t/t3432-rebase-fast-forward.sh index 92f95b5..7432c0e 100755 --- a/t/t3432-rebase-fast-forward.sh +++ b/t/t3432-rebase-fast-forward.sh @@ -44,19 +44,15 @@ test_rebase_same_head_ () { test_expect_$status "git rebase$flag $* with $changes is $what with $cmp HEAD" " oldhead=\$(git rev-parse HEAD) && test_when_finished 'git reset --hard \$oldhead' && + cp .git/logs/HEAD expect && git rebase$flag $* >stdout && if test $what = work then - # Must check this case first, for 'is up to - # date, rebase forced[...]rewinding head' cases - test_i18ngrep 'rewinding head' stdout + old=\$(wc -l Date: Sat, 15 Feb 2020 21:36:31 +0000 Subject: rebase: allow more types of rebases to fast-forward In the past, we dis-allowed rebases using the interactive backend from performing a fast-forward to short-circuit the rebase operation. This made sense for explicitly interactive rebases and some implicitly interactive rebases, but certainly became overly stringent when the merge backend was re-implemented via the interactive backend. Just as the am-based rebase has always had to disable the fast-forward based on a variety of conditions or flags (e.g. --signoff, --whitespace, etc.), we need to do the same but now with a few more options. However, continuing to use REBASE_FORCE for tracking this is problematic because the interactive backend used it for a different purpose. (When REBASE_FORCE wasn't set, the interactive backend would not fast-forward the whole series but would fast-forward individual "pick" commits at the beginning of the todo list, and then a squash or something would cause it to start generating new commits.) So, introduce a new allow_preemptive_ff flag contained within cmd_rebase() and use it to track whether we are going to allow a pre-emptive fast-forward that short-circuits the whole rebase. Signed-off-by: Elijah Newren Signed-off-by: Junio C Hamano diff --git a/builtin/rebase.c b/builtin/rebase.c index 8264a92..6e9a2fe 100644 --- a/builtin/rebase.c +++ b/builtin/rebase.c @@ -1467,6 +1467,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix) struct object_id squash_onto; char *squash_onto_name = NULL; int reschedule_failed_exec = -1; + int allow_preemptive_ff = 1; struct option builtin_rebase_options[] = { OPT_STRING(0, "onto", &options.onto_name, N_("revision"), @@ -1774,13 +1775,20 @@ int cmd_rebase(int argc, const char **argv, const char *prefix) state_dir_base, cmd_live_rebase, buf.buf); } + if ((options.flags & REBASE_INTERACTIVE_EXPLICIT) || + (action != ACTION_NONE) || + (exec.nr > 0) || + options.autosquash) { + allow_preemptive_ff = 0; + } + for (i = 0; i < options.git_am_opts.argc; i++) { const char *option = options.git_am_opts.argv[i], *p; if (!strcmp(option, "--committer-date-is-author-date") || !strcmp(option, "--ignore-date") || !strcmp(option, "--whitespace=fix") || !strcmp(option, "--whitespace=strip")) - options.flags |= REBASE_FORCE; + allow_preemptive_ff = 0; else if (skip_prefix(option, "-C", &p)) { while (*p) if (!isdigit(*(p++))) @@ -2116,12 +2124,14 @@ int cmd_rebase(int argc, const char **argv, const char *prefix) /* * Check if we are already based on onto with linear history, * in which case we could fast-forward without replacing the commits - * with new commits recreated by replaying their changes. This - * optimization must not be done if this is an interactive rebase. + * with new commits recreated by replaying their changes. + * + * Note that can_fast_forward() initializes merge_base, so we have to + * call it before checking allow_preemptive_ff. */ if (can_fast_forward(options.onto, options.upstream, options.restrict_revision, &options.orig_head, &merge_base) && - !is_interactive(&options)) { + allow_preemptive_ff) { int flag; if (!(options.flags & REBASE_FORCE)) { diff --git a/t/t3432-rebase-fast-forward.sh b/t/t3432-rebase-fast-forward.sh index 7432c0e..40388cc 100755 --- a/t/t3432-rebase-fast-forward.sh +++ b/t/t3432-rebase-fast-forward.sh @@ -30,6 +30,8 @@ test_rebase_same_head () { shift && test_rebase_same_head_ $status_n $what_n $cmp_n "" "$*" && test_rebase_same_head_ $status_f $what_f $cmp_f " --no-ff" "$*" + test_rebase_same_head_ $status_n $what_n $cmp_n " --merge" "$*" && + test_rebase_same_head_ $status_f $what_f $cmp_f " --merge --no-ff" "$*" } test_rebase_same_head_ () { -- cgit v0.10.2-6-g49f6 From be50c938dfc1555ea62df455a41138e41503b952 Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Sat, 15 Feb 2020 21:36:32 +0000 Subject: git-rebase.txt: add more details about behavioral differences of backends Signed-off-by: Elijah Newren Signed-off-by: Junio C Hamano diff --git a/Documentation/git-rebase.txt b/Documentation/git-rebase.txt index e1c6f91..6e2569c 100644 --- a/Documentation/git-rebase.txt +++ b/Documentation/git-rebase.txt @@ -416,7 +416,7 @@ with `--keep-base` in order to drop those commits from your branch. --ignore-whitespace:: --whitespace=