Skip to contentSkip to conversation

rebase: make the default backend configurable

PATCH Superseded Elijah Newren via GitGitGadget 11 participants 161 messages Dec 20, 2019 – Mar 17, 2020 Documentation Page 1 of 2

lore
Share & tools
lore
#0 lore raw

cover rebase: make the default backend configurable

This series does a lot of work around making the default rebase backend configurable:

* provide configurability of handling empty commits and iron out differences between backends
* increase/fix capabilities of the merge/interactive backend to make it workable as the default
* document the remaining differences in backends more thoroughly
* add an --am option for explicitly requesting the am-backend
* extend merge/interactive backend testing to put it on par with the am backend
* add a 'rebase.backend' config option and:
* switch the rebase.backend default value from 'am' to 'merge'.

Areas I'd like reviewers to focus, in priority order:

* Patch 15 (Are we ready to switch the default backend? Or should we leave this patch out for now? Junio suggested we may be ready or at least are close[1])
* Patch 1 (Does my empty handling make sense? Do others agree I fixed --keep-empty, or do they view it as breaking it?)
* Patch 8 (Do the updates to the documentation of behavioral differences make sense? Is it too long?) * Patch 11 (okay to change the git-completion shell prompt slightly, especially in light of patches 15 & 16? We did a prompt change previously when we merged the merge backend with the interactive one, so I assume so, but just want to make sure people have a chance to chime in.)

If it's too soon to switch from the 'am' to 'merge' backend, we can just drop the last patch and I'll resubmit it later. I generated this series mostly through switching the default first and then watching what broke, but moved the patch to the end to make it easy to drop.

Briefly, reasons for switching the default backend boil down to the fact that the am-backend drops information and thus limits what it can do. This manifests in different ways:

* lack of tree information that would allow us to warn users that new files in old directories might want to move along with the other files that were renamed with those directories[1]
* incorrect application of patches in the presence of non-unique context lines[2], which could be avoided with access to the original files involved.
* less information available to annotate conflict markers (since am creates fake ancestors and commits on top of them, and doesn't have access to the original commits)

[1] https://lore.kernel.org/git/[email protected]/
[2] https://lore.kernel.org/git/[email protected]/
[3]
https://lore.kernel.org/git/CABPp-BGiu2nVMQY_t-rnFR5GQUz_ipyEE8oDocKeO+h+t4Mn4A@mail.gmail.com/

Elijah Newren (15):
rebase: extend the options for handling of empty commits
t3406: simplify an already simple test
rebase, sequencer: remove the broken GIT_QUIET handling
rebase: make sure to pass along the quiet flag to the sequencer
rebase: fix handling of restrict_revision
t3432: make these tests work with either am or merge backends
rebase: allow more types of rebases to fast-forward git-rebase.txt: add more details about behavioral differences of
backends
rebase: move incompatibility checks between backend options a bit
earlier
rebase: add an --am option
contrib: change the prompt for am-based rebases
rebase tests: mark tests specific to the am-backend with --am rebase tests: repeat some tests using the merge backend instead of am
rebase: make the backend configurable via config setting
rebase: change the default backend from "am" to "merge"

 Documentation/config/rebase.txt         |   8 ++
 Documentation/git-rebase.txt            | 150 ++++++++++++++++----
 builtin/rebase.c                        | 181 +++++++++++++++++++-----
 contrib/completion/git-prompt.sh        |   2 +-
 rebase-interactive.c                    |   4 +-
 rebase-interactive.h                    |   2 +-
 sequencer.c                             |  80 ++++++++---
 sequencer.h                             |   6 +-
 t/t3400-rebase.sh                       |  36 ++++-
 t/t3401-rebase-and-am-rename.sh         |   4 +-
 t/t3404-rebase-interactive.sh           |   2 +-
 t/t3406-rebase-message.sh               |  19 ++-
 t/t3407-rebase-abort.sh                 |   6 +-
 t/t3420-rebase-autostash.sh             |   2 +-
 t/t3421-rebase-topology-linear.sh       |   4 +-
 t/t3424-rebase-empty.sh                 |  89 ++++++++++++
 t/t3425-rebase-topology-merges.sh       |   8 +-
 t/t3427-rebase-subtree.sh               |  16 ++-
 t/t3432-rebase-fast-forward.sh          |  59 ++++----
 t/t3433-rebase-options-compatibility.sh |  13 +-
 t/t5407-post-rewrite-hook.sh            |  12 +-
 t/t5520-pull.sh                         |  27 +++-
 t/t6047-diff3-conflict-markers.sh       |  13 +-
 t/t7512-status-help.sh                  |  12 +-
 t/t9106-git-svn-commit-diff-clobber.sh  |   3 +-
 t/t9903-bash-prompt.sh                  |   6 +-
 26 files changed, 582 insertions(+), 182 deletions(-)
 create mode 100755 t/t3424-rebase-empty.sh

base-commit: 12029dc57db23baef008e77db1909367599210ee
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-679%2Fnewren%2Frebase-fixes-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-679/newren/rebase-fixes-v1
Pull-Request: https://github.com/git/git/pull/679

Signature · 1 line
gitgitgadget
#6 lore raw

PATCH 2/15 t3406: simplify an already simple test

From: Elijah Newren <[email protected]>

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.

---
 t/t3406-rebase-message.sh | 7 ++-----
 1 file changed, 2 insertions(+), 5 deletions(-)
t/t3406-rebase-message.sh +2 -5
index b393e1e9fe..0c2c569f95 100755
@@ -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' '
Signature · 1 line
gitgitgadget
#7 lore raw

PATCH 3/15 rebase, sequencer: remove the broken GIT_QUIET handling

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

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.

---
 builtin/rebase.c | 6 ++----
 sequencer.c      | 6 ++----
 2 files changed, 4 insertions(+), 8 deletions(-)
builtin/rebase.c +2 -4
index 96db10eaed..c71d169688 100644
@@ -718,8 +718,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)
@@ -1178,8 +1178,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);
sequencer.c +2 -4
index d2c11f34b7..71062212a5 100644
@@ -2688,8 +2688,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)
@@ -2698,8 +2696,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)
Signature · 1 line
gitgitgadget
#1 lore raw

PATCH 1/15 rebase: extend the options for handling of empty commits

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

Extend the interactive machinery with the ability to handle the full spread of options for how to handle commits that either start or become empty (by "become empty" I mean the changes in a commit are a subset of changes that exist upstream, so the net effect of applying the commit is no changes). Introduce a new command line flag for selecting the desired behavior:

    --empty={drop,keep,ask}

with the definitions:
drop: drop empty commits
keep: keep empty commits
ask: provide the user a chance to interact and pick what to do with
empty commits on a case-by-case basis

Note that traditionally, am-based rebases have always dropped commits that either started or became empty, while interactive-based rebases have defaulted to ask (and provided an option to keep commits that started empty). This difference made sense since users of an am-based rebase just wanted to quickly batch apply a sequence of commits, while users editing a todo list will likely want the chance to interact and handle unusual cases on a case-by-case basis. However, not all rebases using the interactive machinery are explicitly interactive anymore. In particular --merge was always meant to behave more like --am: just rebase a batch of commits without popping up a todo list.

If the --empty flag is not specified, pick defaults as follows:
explicitly interactive: ask
--exec: keep (exec is about checking existing commits, and often
used without actually changing the base. Thus the expectation is that the user doesn't necessarily want anything to change; they just want to test).
otherwise: drop

Also, this commit makes --keep-empty just imply --empty=keep, and hides it from help so that we aren't confusing users with different ways to do the same thing. (I could have added a --drop-empty flag, but then that invites users to specify both --keep-empty and --drop-empty and we have to add sanity checking around that; it seems cleaner to have a single multi-valued option.) This actually fixes --keep-empty too; previously, it only meant to sometimes keep empty commits, in particular commits which started empty would be kept. But it would still error out and ask the user what to do with commits that became empty. Now it keeps empty commits, as instructed.

---
 Documentation/git-rebase.txt      | 35 ++++++------
 builtin/rebase.c                  | 87 +++++++++++++++++++++++++++---
 rebase-interactive.c              |  4 +-
 rebase-interactive.h              |  2 +-
 sequencer.c                       | 74 +++++++++++++++++++------
 sequencer.h                       |  6 ++-
 t/t3421-rebase-topology-linear.sh |  4 +-
 t/t3424-rebase-empty.sh           | 89 +++++++++++++++++++++++++++++++
 t/t3427-rebase-subtree.sh         | 16 +++---
 9 files changed, 267 insertions(+), 50 deletions(-)
 create mode 100755 t/t3424-rebase-empty.sh
Documentation/git-rebase.txt +20 -15
index 1d0e2d27cc..ff32ca1080 100644
@@ -258,9 +258,25 @@ 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 become empty (because they contain a
+	subset of already upstream changes) or start empty.  With drop
+	(the default), commits that start or become empty are dropped.
+	With keep (implied by --exec), 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 or
+	commit it.  Also with ask, if the rebase is interactive then
+	commits which start empty will be commented out in the todo
+	action list (giving you a chance to uncomment).
++
+Note that this has no effect on commits which are already upstream (as
+can be checked via `git log --cherry-mark ...`), which are always
+dropped by rebase.
++
+See also INCOMPATIBLE OPTIONS below.
+
 --keep-empty::
-	Keep the commits that do not change anything from its
-	parents in the result.
+	Deprecated alias for what is now known as --empty=keep.
 +
 See also INCOMPATIBLE OPTIONS below.
 
@@ -569,6 +585,7 @@ are incompatible with the following options:
  * --interactive
  * --exec
  * --keep-empty
+ * --empty=
  * --edit-todo
  * --root when used in combination with --onto
 
@@ -580,6 +597,7 @@ In addition, the following pairs of options are incompatible:
  * --preserve-merges and --ignore-whitespace
  * --preserve-merges and --committer-date-is-author-date
  * --preserve-merges and --ignore-date
+ * --preserve-merges and --empty=
  * --keep-base and --onto
  * --keep-base and --root
 
@@ -588,19 +606,6 @@ BEHAVIORAL DIFFERENCES
 
 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 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.
-
 Directory rename detection
 ~~~~~~~~~~~~~~~~~~~~~~~~~~
 
builtin/rebase.c +79 -8

161 lines not on this page · raw message

rebase-interactive.c +2 -2
index aa18ae82b7..ad82bf77df 100644
@@ -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(unsigned no_ask_empty, int command_count,
 		      const char *shortrevisions, const char *shortonto,
 		      struct strbuf *buf)
 {
@@ -81,7 +81,7 @@ void append_todo_help(unsigned keep_empty, int command_count,
 
 	strbuf_add_commented_lines(buf, msg, strlen(msg));
 
-	if (!keep_empty) {
+	if (!no_ask_empty) {
 		msg = _("Note that empty commits are commented out");
 		strbuf_add_commented_lines(buf, msg, strlen(msg));
 	}
rebase-interactive.h +1 -1
index 44dbb06311..f531e00ba7 100644
@@ -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(unsigned no_ask_empty, int command_count,
 		      const char *shortrevisions, const char *shortonto,
 		      struct strbuf *buf);
 int edit_todo_list(struct repository *r, struct todo_list *todo_list,
sequencer.c +59 -15

167 lines not on this page · raw message

sequencer.h +5 -1
index e9a0e03ea2..1c3abb661c 100644
@@ -39,7 +39,9 @@ struct replay_opts {
 	int allow_rerere_auto;
 	int allow_empty;
 	int allow_empty_message;
+	int drop_redundant_commits;
 	int keep_redundant_commits;
+	int ask_on_initially_empty;
 	int verbose;
 	int quiet;
 	int reschedule_failed_exec;
@@ -134,7 +136,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)
@@ -150,6 +152,8 @@ int sequencer_remove_state(struct replay_opts *opts);
  * `--onto`, we do not want to re-generate the root commits.
  */
 #define TODO_LIST_ROOT_WITH_ONTO (1U << 6)
+#define TODO_LIST_DROP_EMPTY (1U << 7)
+#define TODO_LIST_ASK_EMPTY (1U << 8)
 
 
 int sequencer_make_script(struct repository *r, struct strbuf *out, int argc,
t/t3421-rebase-topology-linear.sh +2 -2
index 325072b0a3..d23e0bf778 100755
@@ -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
t/t3424-rebase-empty.sh new file +89 -0

89 lines not on this page · raw message

t/t3427-rebase-subtree.sh +10 -6
index bec48e6a1f..468ebc1bef 100755
@@ -85,23 +85,27 @@ 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 --empty=ask --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 --empty=ask --onto files-master master &&
 	: first pick results in no changes &&
-	git rebase --continue &&
+	test_must_fail git rebase --skip &&
+	: last pick was an empty commit that has no changes, but we want to keep it &&
+	git commit --allow-empty &&
 	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 --empty=ask --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 --empty=ask --rebase-merges --onto files-master --root &&
 	: first pick results in no changes &&
-	git rebase --continue &&
+	test_must_fail git rebase --skip &&
+	: last pick was an empty commit that has no changes, but we want to keep it &&
+	git commit --allow-empty &&
 	verbose test "$(commit_message HEAD~2)" = "master4" &&
 	verbose test "$(commit_message HEAD~)" = "files_subtree/master5" &&
 	verbose test "$(commit_message HEAD)" = "Empty commit"
Signature · 1 line
gitgitgadget
#9 lore raw

PATCH 4/15 rebase: make sure to pass along the quiet flag to the sequencer

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

---
 builtin/rebase.c  | 3 ++-
 t/t3400-rebase.sh | 8 +++++++-
 2 files changed, 9 insertions(+), 2 deletions(-)
builtin/rebase.c +2 -1
index c71d169688..cc8f3f008f 100644
@@ -126,6 +126,7 @@ static struct replay_opts get_replay_opts(const struct rebase_options *opts)
 	replay.keep_redundant_commits = (opts->empty == EMPTY_KEEP);
 	replay.ask_on_initially_empty = (opts->empty == EMPTY_ASK &&
 					 !(opts->flags & REBASE_INTERACTIVE_EXPLICIT));
+	replay.quiet = !(opts->flags & REBASE_NO_QUIET);
 	replay.verbose = opts->flags & REBASE_VERBOSE;
 	replay.reschedule_failed_exec = opts->reschedule_failed_exec;
 	replay.committer_date_is_author_date =
@@ -1506,7 +1507,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),
t/t3400-rebase.sh +7 -1
index 221b35f2df..79762b989a 100755
@@ -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" &&
Signature · 1 line
gitgitgadget
#12 lore raw

PATCH 6/15 t3432: make these tests work with either am or merge backends

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

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.

---
 t/t3432-rebase-fast-forward.sh | 53 ++++++++++++++++------------------
 1 file changed, 25 insertions(+), 28 deletions(-)
t/t3432-rebase-fast-forward.sh +25 -28
index 92f95b57da..3879a43fa0 100755
@@ -44,19 +44,16 @@ 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
+			wc -l .git/logs/HEAD >old &&
+			wc -l .git/logs/HEAD >new &&
+			test_line_count '-gt' $(($old + 2)) .git/logs/HEAD
 		elif test $what = noop
 		then
-			test_i18ngrep 'is up to date' stdout &&
-			test_i18ngrep ! 'rebase forced' stdout
-		elif test $what = noop-force
-		then
-			test_i18ngrep 'is up to date, rebase forced' stdout
+			test_cmp expect .git/logs/HEAD
 		fi &&
 		newhead=\$(git rev-parse HEAD) &&
 		if test $cmp = same
@@ -71,14 +68,14 @@ test_rebase_same_head_ () {
 
 changes='no changes'
 test_rebase_same_head success noop same success work same
-test_rebase_same_head success noop same success noop-force same master
-test_rebase_same_head success noop same success noop-force diff --onto B B
-test_rebase_same_head success noop same success noop-force diff --onto B... B
-test_rebase_same_head success noop same success noop-force same --onto master... master
-test_rebase_same_head success noop same success noop-force same --keep-base master
-test_rebase_same_head success noop same success noop-force same --keep-base
-test_rebase_same_head success noop same success noop-force same --no-fork-point
-test_rebase_same_head success noop same success noop-force same --keep-base --no-fork-point
+test_rebase_same_head success noop same success work same master
+test_rebase_same_head success noop same success work diff --onto B B
+test_rebase_same_head success noop same success work diff --onto B... B
+test_rebase_same_head success noop same success work same --onto master... master
+test_rebase_same_head success noop same success work same --keep-base master
+test_rebase_same_head success noop same success work same --keep-base
+test_rebase_same_head success noop same success work same --no-fork-point
+test_rebase_same_head success noop same success work same --keep-base --no-fork-point
 test_rebase_same_head success noop same success work same --fork-point master
 test_rebase_same_head success noop same success work diff --fork-point --onto B B
 test_rebase_same_head success noop same success work diff --fork-point --onto B... B
@@ -91,14 +88,14 @@ test_expect_success 'add work same to side' '
 
 changes='our changes'
 test_rebase_same_head success noop same success work same
-test_rebase_same_head success noop same success noop-force same master
-test_rebase_same_head success noop same success noop-force diff --onto B B
-test_rebase_same_head success noop same success noop-force diff --onto B... B
-test_rebase_same_head success noop same success noop-force same --onto master... master
-test_rebase_same_head success noop same success noop-force same --keep-base master
-test_rebase_same_head success noop same success noop-force same --keep-base
-test_rebase_same_head success noop same success noop-force same --no-fork-point
-test_rebase_same_head success noop same success noop-force same --keep-base --no-fork-point
+test_rebase_same_head success noop same success work same master
+test_rebase_same_head success noop same success work diff --onto B B
+test_rebase_same_head success noop same success work diff --onto B... B
+test_rebase_same_head success noop same success work same --onto master... master
+test_rebase_same_head success noop same success work same --keep-base master
+test_rebase_same_head success noop same success work same --keep-base
+test_rebase_same_head success noop same success work same --no-fork-point
+test_rebase_same_head success noop same success work same --keep-base --no-fork-point
 test_rebase_same_head success noop same success work same --fork-point master
 test_rebase_same_head success noop same success work diff --fork-point --onto B B
 test_rebase_same_head success noop same success work diff --fork-point --onto B... B
@@ -112,13 +109,13 @@ test_expect_success 'add work same to upstream' '
 '
 
 changes='our and their changes'
-test_rebase_same_head success noop same success noop-force diff --onto B B
-test_rebase_same_head success noop same success noop-force diff --onto B... B
+test_rebase_same_head success noop same success work diff --onto B B
+test_rebase_same_head success noop same success work diff --onto B... B
 test_rebase_same_head success noop same success work diff --onto master... master
 test_rebase_same_head success noop same success work diff --keep-base master
 test_rebase_same_head success noop same success work diff --keep-base
-test_rebase_same_head failure work same success work diff --fork-point --onto B B
-test_rebase_same_head failure work same success work diff --fork-point --onto B... B
+test_rebase_same_head success work same success work diff --fork-point --onto B B
+test_rebase_same_head success work same success work diff --fork-point --onto B... B
 test_rebase_same_head success noop same success work diff --fork-point --onto master... master
 test_rebase_same_head success noop same success work diff --fork-point --keep-base master
 
Signature · 1 line
gitgitgadget
#17 lore raw

PATCH 9/15 rebase: move incompatibility checks between backend options a bit earlier

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

---
 builtin/rebase.c | 22 +++++++++++-----------
 1 file changed, 11 insertions(+), 11 deletions(-)
builtin/rebase.c +11 -11
index 67bccd876f..9e7e88b147 100644
@@ -1910,6 +1910,17 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
 	if (isatty(2) && options.flags & REBASE_NO_QUIET)
 		strbuf_addstr(&options.git_format_patch_opt, " --progress");
 
+	if (options.git_am_opts.argc) {
+		/* all am options except -q are compatible only with --am */
+		for (i = options.git_am_opts.argc - 1; i >= 0; i--)
+			if (strcmp(options.git_am_opts.argv[i], "-q"))
+				break;
+
+		if (is_interactive(&options) && i >= 0)
+			die(_("cannot combine am options with either "
+			      "interactive or merge options"));
+	}
+
 	switch (options.type) {
 	case REBASE_MERGE:
 	case REBASE_INTERACTIVE:
@@ -1940,17 +1951,6 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
 	if (reschedule_failed_exec >= 0)
 		options.reschedule_failed_exec = reschedule_failed_exec;
 
-	if (options.git_am_opts.argc) {
-		/* all am options except -q are compatible only with --am */
-		for (i = options.git_am_opts.argc - 1; i >= 0; i--)
-			if (strcmp(options.git_am_opts.argv[i], "-q"))
-				break;
-
-		if (is_interactive(&options) && i >= 0)
-			die(_("cannot combine am options with either "
-			      "interactive or merge options"));
-	}
-
 	if (options.signoff) {
 		if (options.type == REBASE_PRESERVE_MERGES)
 			die("cannot combine '--signoff' with "
Signature · 1 line
gitgitgadget
#15 lore raw

PATCH 7/15 rebase: allow more types of rebases to fast-forward

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

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.

---
 builtin/rebase.c               | 18 ++++++++++++++----
 t/t3432-rebase-fast-forward.sh |  2 ++
 2 files changed, 16 insertions(+), 4 deletions(-)
builtin/rebase.c +14 -4
index b320bb3a30..67bccd876f 100644
@@ -1497,6 +1497,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"),
@@ -1808,11 +1809,18 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
 	    options.ignore_date)
 		options.flags |= REBASE_FORCE;
 
+	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, "--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++)))
@@ -2148,12 +2156,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)) {
t/t3432-rebase-fast-forward.sh +2 -0
index 3879a43fa0..58c91c6899 100755
@@ -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_ () {
Signature · 1 line
gitgitgadget
#16 lore raw

PATCH 8/15 git-rebase.txt: add more details about behavioral differences of backends

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

---
 Documentation/git-rebase.txt            | 102 +++++++++++++++++++++---
 t/t3433-rebase-options-compatibility.sh |   5 +-
 2 files changed, 94 insertions(+), 13 deletions(-)
Documentation/git-rebase.txt +92 -10
index ff32ca1080..f1ace07c38 100644
@@ -409,13 +409,10 @@ your branch contains commits which were dropped, this option can be used
 with `--keep-base` in order to drop those commits from your branch.
 
 --ignore-whitespace::
-	Behaves differently depending on which backend is selected.
-+
-'am' backend: When applying a patch, ignore changes in whitespace in
-context lines if necessary.
-+
-'interactive' backend: Treat lines with only whitespace changes as
-unchanged for the sake of a three-way merge.
+	Ignore whitespace-only changes in the commits being rebased,
+	which may avoid "unnecessary" conflicts.  (Both backends
+	currently have differing edgecase bugs with this option; see
+	BEHAVIORAL DIFFERENCES.)
 
 --whitespace=<option>::
 	This flag is passed to the 'git apply' program
@@ -609,9 +606,94 @@ There are some subtle differences how the backends behave.
 Directory rename detection
 ~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-Directory rename heuristics are enabled in the merge and interactive
-backends.  Due to the lack of accurate tree information, directory
-rename detection is disabled in the am backend.
+Due to the lack of accurate tree information (arising from
+constructing fake ancestors with the limited information available in
+patches), directory rename detection is disabled in the am backend.
+Disabled directory rename detection means that if one side of history
+renames a directory and the other adds new files to the old directory,
+then the new files will be left behind in the old directory without
+any warning at the time of rebasing that you may want to move these
+files into the new directory.
+
+Directory rename detection works with the merge and interactive
+backends to provide you warnings in such cases.
+
+Context
+~~~~~~~
+
+The am backend works by creating a sequence of patches (by calling
+`format-patch` internally), and then applying the patches in sequence
+(calling `am` internally).  Patches are composed of multiple hunks,
+each with line numbers, a context region, and the actual changes.  The
+line numbers have to be taken with some fuzz, since the other side
+will likely have inserted or deleted lines earlier in the file.  The
+context region is meant to help find how to adjust the line numbers in
+order to apply the changes to the right lines.  However, if multiple
+areas of the code have the same surrounding lines of context, the
+wrong one can be picked.  There are real-world cases where this has
+caused commits to be reapplied incorrectly with no conflicts reported.
+Setting diff.context to a larger value may prevent such types of
+problems, but increases the chance of spurious conflicts (since it
+will require more lines of matching context to apply).
+
+The interactive backend works with a full copy of each relevant file,
+insulating it from these types of problems.
+
+Labelling of conflicts markers
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+When there are content conflicts, the merge machinery tries to
+annotate each side's conflict markers with the commits where the
+content came from.  Since the am backend drops the original
+information about the rebased commits and their parents (and instead
+generates new fake commits based off limited information in the
+generated patches), those commits cannot be identified; instead it has
+to fall back to a commit summary.  Also, when merge.conflictStyle is
+set to diff3, the am backend will use "constructed merge base" to
+label the content from the merge base, and thus provide no information
+about the merge base commit whatsoever.
+
+The interactive backend works with the full commits on both sides of
+history and thus has no such limitations.
+
+--ignore-whitespace
+~~~~~~~~~~~~~~~~~~~
+
+The --ignore-whitespace option is supposed to ignore whitespace-only
+changes if it allows the code to merge cleanly.  Unfortunately, the
+different backends implement this differently, and both have different
+edge case bugs.
++
+'am' backend: When applying a patch, ignore changes in whitespace in
+context lines if necessary.  (Which implies that if the whitespace
+change was not in the context lines but on a line with a real change,
+then the rebase will still fail with "unnecessary" content conflicts.)
++
+'interactive' backend: Treat lines with only whitespace changes as
+unchanged for the sake of a three-way merge.  This means that if one
+side made no changes and the commits being rebased had whitespace-only
+changes, those whitespaces fixups will be discarded despite the fact
+that they present no content conflict.
+
+Miscellaneous differences
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+There are a few more behavioral differences that most folks would
+probably consider inconsequential but which are mentioned for
+completeness:
+
+* Reflog: The two backends will use different wording when describing
+  the changes made in the reflog, though both will make use of the
+  word "rebase".
+
+* Progress, informational, and error messages: The two backends
+  provide slightly different progress and informational messages.
+  Also, the am backend writes error messages (such as "Your files
+  would be overwritten...") to stdout, while the interactive backend
+  writes them to stderr.
+
+* State directories: The two backends keep their state in different
+  directories under .git/
 
 include::merge-strategies.txt[]
 
t/t3433-rebase-options-compatibility.sh +2 -3
index 5166f158dd..bd4d2d2f63 100755
@@ -10,9 +10,8 @@ test_description='tests to ensure compatibility between am and interactive backe
 GIT_AUTHOR_DATE="1999-04-02T08:03:20+05:30"
 export GIT_AUTHOR_DATE
 
-# This is a special case in which both am and interactive backends
-# provide the same output. It was done intentionally because
-# both the backends fall short of optimal behaviour.
+# This is a common case in which both am and interactive backends
+# provide the same output with --ignore-whitespace.
 test_expect_success 'setup' '
 	git checkout -b topic &&
 	q_to_tab >file <<-\EOF &&
Signature · 1 line
gitgitgadget
#22 lore raw

PATCH 12/15 rebase tests: mark tests specific to the am-backend with --am

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

We have many rebase tests in the testsuite, and often the same test is repeated multiple times just testing different backends. For those tests that were specifically trying to test the am backend, add the --am flag.

---
 t/t3400-rebase.sh                       | 10 +++++-----
 t/t3401-rebase-and-am-rename.sh         |  4 ++--
 t/t3404-rebase-interactive.sh           |  2 +-
 t/t3406-rebase-message.sh               | 12 ++++++------
 t/t3407-rebase-abort.sh                 |  6 +++---
 t/t3420-rebase-autostash.sh             |  2 +-
 t/t3425-rebase-topology-merges.sh       |  8 ++++----
 t/t3432-rebase-fast-forward.sh          |  4 ++--
 t/t3433-rebase-options-compatibility.sh |  8 ++++----
 t/t5407-post-rewrite-hook.sh            | 12 ++++++------
 t/t7512-status-help.sh                  | 12 ++++++------
 11 files changed, 40 insertions(+), 40 deletions(-)
t/t3400-rebase.sh +5 -5
index 71fd6396cd..0a491f2363 100755
@@ -183,19 +183,19 @@ test_expect_success 'default to common base in @{upstream}s reflog if no upstrea
 	test_cmp expect actual
 '
 
-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 (--am)' '
 	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 rebase --am &&
 	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 &&
+	git rebase --am &&
 	git rev-parse --verify default-base >expect &&
 	git rev-parse default~1 >actual &&
 	test_cmp expect actual
@@ -226,7 +226,7 @@ test_expect_success 'cherry-picked commits and fork-point work together' '
 
 test_expect_success 'rebase --am -q is quiet' '
 	git checkout -b quiet topic &&
-	git rebase -q master >output.out 2>&1 &&
+	git rebase --am -q master >output.out 2>&1 &&
 	test_must_be_empty output.out
 '
 
@@ -325,7 +325,7 @@ test_expect_success 'rebase --am and --show-current-patch' '
 		echo two >>init.t &&
 		git commit -a -m two &&
 		git tag two &&
-		test_must_fail git rebase -f --onto init HEAD^ &&
+		test_must_fail git rebase --am -f --onto init HEAD^ &&
 		GIT_TRACE=1 git rebase --show-current-patch >/dev/null 2>stderr &&
 		grep "show.*$(git rev-parse two)" stderr
 	)
t/t3401-rebase-and-am-rename.sh +2 -2
index a0b9438b22..50803958fd 100755
@@ -52,13 +52,13 @@ test_expect_success 'rebase --interactive: directory rename detected' '
 	)
 '
 
-test_expect_failure 'rebase (am): directory rename detected' '
+test_expect_failure 'rebase --am: directory rename detected' '
 	(
 		cd dir-rename &&
 
 		git checkout B^0 &&
 
-		git -c merge.directoryRenames=true rebase A &&
+		git -c merge.directoryRenames=true rebase --am A &&
 
 		git ls-files -s >out &&
 		test_line_count = 5 out &&
t/t3404-rebase-interactive.sh +1 -1
index ae6e55ce79..743b7e511a 100755
@@ -1137,7 +1137,7 @@ test_expect_success C_LOCALE_OUTPUT 'rebase --edit-todo does not work on non-int
 	git checkout conflict-branch &&
 	(
 		set_fake_editor &&
-		test_must_fail git rebase -f --onto HEAD~2 HEAD~ &&
+		test_must_fail git rebase -f --am --onto HEAD~2 HEAD~ &&
 		test_must_fail git rebase --edit-todo
 	) &&
 	git rebase --abort
t/t3406-rebase-message.sh +6 -6
index 0c2c569f95..7ce617fc1f 100755
@@ -23,24 +23,24 @@ test_expect_success 'rebase -m' '
 '
 
 test_expect_success 'rebase against master twice' '
-	git rebase master >out &&
+	git rebase --am master >out &&
 	test_i18ngrep "Current branch topic is up to date" out
 '
 
 test_expect_success 'rebase against master twice with --force' '
-	git rebase --force-rebase master >out &&
+	git rebase --force-rebase --am master >out &&
 	test_i18ngrep "Current branch topic is up to date, rebase forced" out
 '
 
 test_expect_success 'rebase against master twice from another branch' '
 	git checkout topic^ &&
-	git rebase master topic >out &&
+	git rebase --am master topic >out &&
 	test_i18ngrep "Current branch topic is up to date" out
 '
 
 test_expect_success 'rebase fast-forward to master' '
 	git checkout topic^ &&
-	git rebase topic >out &&
+	git rebase --am topic >out &&
 	test_i18ngrep "Fast-forwarded HEAD to topic" out
 '
 
@@ -89,7 +89,7 @@ test_expect_success 'GIT_REFLOG_ACTION' '
 	git checkout -b reflog-topic start &&
 	test_commit reflog-to-rebase &&
 
-	git rebase reflog-onto &&
+	git rebase --am reflog-onto &&
 	git log -g --format=%gs -3 >actual &&
 	cat >expect <<-\EOF &&
 	rebase finished: returning to refs/heads/reflog-topic
@@ -99,7 +99,7 @@ test_expect_success 'GIT_REFLOG_ACTION' '
 	test_cmp expect actual &&
 
 	git checkout -b reflog-prefix reflog-to-rebase &&
-	GIT_REFLOG_ACTION=change-the-reflog git rebase reflog-onto &&
+	GIT_REFLOG_ACTION=change-the-reflog git rebase --am reflog-onto &&
 	git log -g --format=%gs -3 >actual &&
 	cat >expect <<-\EOF &&
 	rebase finished: returning to refs/heads/reflog-prefix
t/t3407-rebase-abort.sh +3 -3
index 910f218284..3e31826170 100755
@@ -96,14 +96,14 @@ testrebase() {
 	'
 }
 
-testrebase "" .git/rebase-apply
+testrebase " --am" .git/rebase-apply
 testrebase " --merge" .git/rebase-merge
 
-test_expect_success 'rebase --quit' '
+test_expect_success 'rebase --am --quit' '
 	cd "$work_dir" &&
 	# Clean up the state from the previous one
 	git reset --hard pre-rebase &&
-	test_must_fail git rebase master &&
+	test_must_fail git rebase --am master &&
 	test_path_is_dir .git/rebase-apply &&
 	head_before=$(git rev-parse HEAD) &&
 	git rebase --quit &&
t/t3420-rebase-autostash.sh +1 -1
index 5f7e73cf83..3816159e20 100755
@@ -234,7 +234,7 @@ test_expect_success "rebase: noop rebase" '
 	git checkout feature-branch
 '
 
-testrebase "" .git/rebase-apply
+testrebase " --am" .git/rebase-apply
 testrebase " --merge" .git/rebase-merge
 testrebase " --interactive" .git/rebase-merge
 
t/t3425-rebase-topology-merges.sh +4 -4
index fd8efe84fe..19700b025b 100755
@@ -54,7 +54,7 @@ test_run_rebase () {
 		test_linear_range 'n o' e..
 	"
 }
-test_run_rebase success ''
+test_run_rebase success --am
 test_run_rebase success -m
 test_run_rebase success -i
 
@@ -70,7 +70,7 @@ test_run_rebase () {
 		test_linear_range "\'"$expected"\'" d..
 	"
 }
-test_run_rebase success 'n o e' ''
+test_run_rebase success 'n o e' --am
 test_run_rebase success 'n o e' -m
 test_run_rebase success 'n o e' -i
 
@@ -86,7 +86,7 @@ test_run_rebase () {
 		test_linear_range "\'"$expected"\'" c..
 	"
 }
-test_run_rebase success 'd n o e' ''
+test_run_rebase success 'd n o e' --am
 test_run_rebase success 'd n o e' -m
 test_run_rebase success 'd n o e' -i
 
@@ -102,7 +102,7 @@ test_run_rebase () {
 		test_linear_range "\'"$expected"\'" c..
 	"
 }
-test_run_rebase success 'd n o e' ''
+test_run_rebase success 'd n o e' --am
 test_run_rebase success 'd n o e' -m
 test_run_rebase success 'd n o e' -i
 
t/t3432-rebase-fast-forward.sh +2 -2
index 58c91c6899..b22aa8c7ee 100755
@@ -28,8 +28,8 @@ test_rebase_same_head () {
 	shift &&
 	cmp_f="$1" &&
 	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 " --am" "$*" &&
+	test_rebase_same_head_ $status_f $what_f $cmp_f " --am --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" "$*"
 }
t/t3433-rebase-options-compatibility.sh +4 -4
index bd4d2d2f63..a07e1f276b 100755
@@ -51,9 +51,9 @@ test_expect_success '--ignore-whitespace works with am backend' '
 	new line 2
 	line 3
 	EOF
-	test_must_fail git rebase main side &&
+	test_must_fail git rebase --am main side &&
 	git rebase --abort &&
-	git rebase --ignore-whitespace main side &&
+	git rebase --am --ignore-whitespace main side &&
 	test_cmp expect file
 '
 
@@ -71,7 +71,7 @@ test_expect_success '--ignore-whitespace works with interactive backend' '
 
 test_expect_success '--committer-date-is-author-date works with am backend' '
 	git commit --amend &&
-	git rebase --committer-date-is-author-date HEAD^ &&
+	git rebase --am --committer-date-is-author-date HEAD^ &&
 	git show HEAD --pretty="format:%ai" >authortime &&
 	git show HEAD --pretty="format:%ci" >committertime &&
 	test_cmp authortime committertime
@@ -103,7 +103,7 @@ test_expect_success '--committer-date-is-author-date works with rebase -r' '
 # sets to +0530.
 test_expect_success '--ignore-date works with am backend' '
 	git commit --amend --date="$GIT_AUTHOR_DATE" &&
-	git rebase --ignore-date HEAD^ &&
+	git rebase --am --ignore-date HEAD^ &&
 	git show HEAD --pretty="format:%ai" >authortime &&
 	grep "+0000" authortime
 '
t/t5407-post-rewrite-hook.sh +6 -6
index 7344253bfb..a8a73616e4 100755
@@ -53,10 +53,10 @@ test_expect_success 'git commit --amend --no-post-rewrite' '
 	test ! -f post-rewrite.data
 '
 
-test_expect_success 'git rebase' '
+test_expect_success 'git rebase --am' '
 	git reset --hard D &&
 	clear_hook_input &&
-	test_must_fail git rebase --onto A B &&
+	test_must_fail git rebase --am --onto A B &&
 	echo C > foo &&
 	git add foo &&
 	git rebase --continue &&
@@ -68,10 +68,10 @@ test_expect_success 'git rebase' '
 	verify_hook_input
 '
 
-test_expect_success 'git rebase --skip' '
+test_expect_success 'git rebase --am --skip' '
 	git reset --hard D &&
 	clear_hook_input &&
-	test_must_fail git rebase --onto A B &&
+	test_must_fail git rebase --am --onto A B &&
 	test_must_fail git rebase --skip &&
 	echo D > foo &&
 	git add foo &&
@@ -84,10 +84,10 @@ test_expect_success 'git rebase --skip' '
 	verify_hook_input
 '
 
-test_expect_success 'git rebase --skip the last one' '
+test_expect_success 'git rebase --am --skip the last one' '
 	git reset --hard F &&
 	clear_hook_input &&
-	test_must_fail git rebase --onto D A &&
+	test_must_fail git rebase --am --onto D A &&
 	git rebase --skip &&
 	echo rebase >expected.args &&
 	cat >expected.data <<-EOF &&
t/t7512-status-help.sh +6 -6
index 66d7a62797..d22b0acf2a 100755
@@ -71,10 +71,10 @@ test_expect_success 'prepare for rebase conflicts' '
 '
 
 
-test_expect_success 'status when rebase in progress before resolving conflicts' '
+test_expect_success 'status when rebase --am in progress before resolving conflicts' '
 	test_when_finished "git rebase --abort" &&
 	ONTO=$(git rev-parse --short HEAD^^) &&
-	test_must_fail git rebase HEAD^ --onto HEAD^^ &&
+	test_must_fail git rebase --am HEAD^ --onto HEAD^^ &&
 	cat >expected <<EOF &&
 rebase in progress; onto $ONTO
 You are currently rebasing branch '\''rebase_conflicts'\'' on '\''$ONTO'\''.
@@ -94,11 +94,11 @@ EOF
 '
 
 
-test_expect_success 'status when rebase in progress before rebase --continue' '
+test_expect_success 'status when rebase --am in progress before rebase --continue' '
 	git reset --hard rebase_conflicts &&
 	test_when_finished "git rebase --abort" &&
 	ONTO=$(git rev-parse --short HEAD^^) &&
-	test_must_fail git rebase HEAD^ --onto HEAD^^ &&
+	test_must_fail git rebase --am HEAD^ --onto HEAD^^ &&
 	echo three >main.txt &&
 	git add main.txt &&
 	cat >expected <<EOF &&
@@ -688,7 +688,7 @@ EOF
 '
 
 
-test_expect_success 'status when rebase conflicts with statushints disabled' '
+test_expect_success 'status when rebase --am conflicts with statushints disabled' '
 	git reset --hard master &&
 	git checkout -b statushints_disabled &&
 	test_when_finished "git config --local advice.statushints true" &&
@@ -698,7 +698,7 @@ test_expect_success 'status when rebase conflicts with statushints disabled' '
 	test_commit three_statushints main.txt three &&
 	test_when_finished "git rebase --abort" &&
 	ONTO=$(git rev-parse --short HEAD^^) &&
-	test_must_fail git rebase HEAD^ --onto HEAD^^ &&
+	test_must_fail git rebase --am HEAD^ --onto HEAD^^ &&
 	cat >expected <<EOF &&
 rebase in progress; onto $ONTO
 You are currently rebasing branch '\''statushints_disabled'\'' on '\''$ONTO'\''.
Signature · 1 line
gitgitgadget
#23 lore raw

PATCH 13/15 rebase tests: repeat some tests using the merge backend instead of am

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

In order to ensure the merge/interactive backend gets similar coverage to the am one, add some tests for cases where previously only the am backend was tested.

---
 t/t5520-pull.sh                   | 17 +++++++++++++++--
 t/t6047-diff3-conflict-markers.sh | 13 +++++++++++--
 2 files changed, 26 insertions(+), 4 deletions(-)
t/t5520-pull.sh +15 -2
index 602d996a33..3fff6a06fa 100755
@@ -277,14 +277,27 @@ test_expect_success '--rebase' '
 	test_cmp expect actual
 '
 
-test_expect_success '--rebase fast forward' '
+test_expect_success '--rebase (merge) fast forward' '
 	git reset --hard before-rebase &&
 	git checkout -b ff &&
 	echo another modification >file &&
 	git commit -m third file &&
 
 	git checkout to-rebase &&
-	git pull --rebase . ff &&
+	git -c rebase.backend=merge pull --rebase . ff &&
+	test_cmp_rev HEAD ff &&
+
+	# The above only validates the result.  Did we actually bypass rebase?
+	git reflog -1 >reflog.actual &&
+	sed "s/^[0-9a-f][0-9a-f]*/OBJID/" reflog.actual >reflog.fuzzy &&
+	echo "OBJID HEAD@{0}: pull --rebase . ff: Fast-forward" >reflog.expected &&
+	test_cmp reflog.expected reflog.fuzzy
+'
+
+test_expect_success '--rebase (am) fast forward' '
+	git reset --hard before-rebase &&
+
+	git -c rebase.backend=am pull --rebase . ff &&
 	test_cmp_rev HEAD ff &&
 
 	# The above only validates the result.  Did we actually bypass rebase?
t/t6047-diff3-conflict-markers.sh +11 -2
index 860542aad0..d383ce8130 100755
@@ -186,7 +186,7 @@ test_expect_success 'check multiple merge bases' '
 	)
 '
 
-test_expect_success 'rebase describes fake ancestor base' '
+test_expect_success 'rebase --merge describes parent of commit being picked' '
 	test_create_repo rebase &&
 	(
 		cd rebase &&
@@ -194,7 +194,16 @@ test_expect_success 'rebase describes fake ancestor base' '
 		test_commit master file &&
 		git checkout -b side HEAD^ &&
 		test_commit side file &&
-		test_must_fail git -c merge.conflictstyle=diff3 rebase master &&
+		test_must_fail git -c merge.conflictstyle=diff3 rebase --merge master &&
+		grep "||||||| parent of" file
+	)
+'
+
+test_expect_success 'rebase --am describes fake ancestor base' '
+	(
+		cd rebase &&
+		git rebase --abort &&
+		test_must_fail git -c merge.conflictstyle=diff3 rebase --am master &&
 		grep "||||||| constructed merge base" file
 	)
 '
Signature · 1 line
gitgitgadget
#24 lore raw

PATCH 14/15 rebase: make the backend configurable via config setting

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

---
 Documentation/config/rebase.txt |  8 ++++++++
 builtin/rebase.c                | 31 ++++++++++++++++++++++++-------
 2 files changed, 32 insertions(+), 7 deletions(-)
Documentation/config/rebase.txt +8 -0
index d98e32d812..e6ae30c999 100644
@@ -5,6 +5,14 @@ rebase.useBuiltin::
 	is always used. Setting this will emit a warning, to alert any
 	remaining users that setting this now does nothing.
 
+rebase.backend::
+	Default backend to use for rebasing.  Possible choices are
+	'am' or 'merge' (note that the merge backend is sometimes also
+	refered to as the interactive backend or the interactive
+	machinery elsewhere in the docs).  Also, in the future, if the
+	merge backend gains all remaining capabilities of the am
+	backend, this setting may become unused.
+
 rebase.stat::
 	Whether to show a diffstat of what changed upstream since the last
 	rebase. False by default.
builtin/rebase.c +24 -7
index ab9e16b206..eaa5eac59e 100644
@@ -60,6 +60,7 @@ enum empty_type {
 struct rebase_options {
 	enum rebase_type type;
 	enum empty_type empty;
+	const char *default_backend;
 	const char *state_dir;
 	struct commit *upstream;
 	const char *upstream_name;
@@ -103,6 +104,7 @@ struct rebase_options {
 #define REBASE_OPTIONS_INIT {			  	\
 		.type = REBASE_UNSPECIFIED,	  	\
 		.empty = EMPTY_UNSPECIFIED,	  	\
+		.default_backend = "am",	  	\
 		.flags = REBASE_NO_QUIET, 		\
 		.git_am_opts = ARGV_ARRAY_INIT,		\
 		.git_format_patch_opt = STRBUF_INIT	\
@@ -1298,6 +1300,10 @@ static int rebase_config(const char *var, const char *value, void *data)
 		return 0;
 	}
 
+	if (!strcmp(var, "rebase.backend")) {
+		return git_config_string(&opts->default_backend, var, value);
+	}
+
 	return git_default_config(var, value, data);
 }
 
@@ -1932,9 +1938,23 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
 			if (strcmp(options.git_am_opts.argv[i], "-q"))
 				break;
 
-		if (is_interactive(&options) && i >= 0)
-			die(_("cannot combine am options with either "
-			      "interactive or merge options"));
+		if (i >= 0) {
+			if (is_interactive(&options))
+				die(_("cannot combine am options with either "
+				      "interactive or merge options"));
+			else
+				options.type = REBASE_AM;
+		}
+	}
+
+	if (options.type == REBASE_UNSPECIFIED) {
+		if (!strcmp(options.default_backend, "merge"))
+			options.type = REBASE_MERGE;
+		else if (!strcmp(options.default_backend, "am"))
+			options.type = REBASE_AM;
+		else
+			die(_("Unknown rebase backend: %s"),
+			    options.default_backend);
 	}
 
 	switch (options.type) {
@@ -1947,10 +1967,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
 		options.state_dir = apply_dir();
 		break;
 	default:
-		/* the default rebase backend is `--am` */
-		options.type = REBASE_AM;
-		options.state_dir = apply_dir();
-		break;
+		BUG("options.type was just set above; should be unreachable.");
 	}
 
 	if (options.empty == EMPTY_UNSPECIFIED) {
Signature · 1 line
gitgitgadget
#19 lore raw

PATCH 11/15 contrib: change the prompt for am-based rebases

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

The prompt for am-based rebases was REBASE, while for interactive-based rebases was REBASE-i. A while ago, we switched merge-based rebases from using REBASE-m to REBASE-i via re-implementing the merge backend based on the interactive backend. We will soon be changing the default rebase backend to the interactive one, meaning the default prompt will be REBASE-i rather than REBASE. We have also noted in the documentation that currently am-specific options will be implemented in the interactive backend, and even the --am flag may eventually imply an interactive-based rebase. As such, change the prompt for an am-based rebase from REBASE to REBASE-a.

---
 contrib/completion/git-prompt.sh | 2 +-
 t/t9903-bash-prompt.sh           | 6 +++---
 2 files changed, 4 insertions(+), 4 deletions(-)
contrib/completion/git-prompt.sh +1 -1
index 1d510cd47b..3c81099d60 100644
@@ -440,7 +440,7 @@ __git_ps1 ()
 			__git_eread "$g/rebase-apply/last" total
 			if [ -f "$g/rebase-apply/rebasing" ]; then
 				__git_eread "$g/rebase-apply/head-name" b
-				r="|REBASE"
+				r="|REBASE-a"
 			elif [ -f "$g/rebase-apply/applying" ]; then
 				r="|AM"
 			else
t/t9903-bash-prompt.sh +3 -3
index 88bc733ad6..8da5b1aee2 100755
@@ -189,11 +189,11 @@ test_expect_success 'prompt - rebase merge' '
 	test_cmp expected "$actual"
 '
 
-test_expect_success 'prompt - rebase' '
-	printf " (b2|REBASE 1/3)" >expected &&
+test_expect_success 'prompt - rebase am' '
+	printf " (b2|REBASE-a 1/3)" >expected &&
 	git checkout b2 &&
 	test_when_finished "git checkout master" &&
-	test_must_fail git rebase b1 b2 &&
+	test_must_fail git rebase --am b1 b2 &&
 	test_when_finished "git rebase --abort" &&
 	__git_ps1 >"$actual" &&
 	test_cmp expected "$actual"
Signature · 1 line
gitgitgadget
#10 lore raw

PATCH 5/15 rebase: fix handling of restrict_revision

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

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.

---
 builtin/rebase.c  |  4 ++--
 t/t3400-rebase.sh | 20 +++++++++++++++++++-
 2 files changed, 21 insertions(+), 3 deletions(-)
builtin/rebase.c +2 -2
index cc8f3f008f..b320bb3a30 100644
@@ -364,8 +364,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,
t/t3400-rebase.sh +19 -1
index 79762b989a..71fd6396cd 100755
@@ -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 &&
Signature · 1 line
gitgitgadget
#25 lore raw

PATCH 15/15 rebase: change the default backend from "am" to "merge"

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

The am-backend drops information and thus limits what we can do:

* lack of full tree information from the original commits means we cannot do directory rename detection and warn users that they might want to move some of their new files that they placed in old directories to prevent their becoming orphaned.[1]
* reduction in context from only having a few lines beyond those changed means that when context lines are non-unique we can apply patches incorrectly.[2]
* lack of access to original commits means that conflict marker annotation has less information available.

Also, the merge/interactive backend have far more abilities, appear to currently have a slight performance advantage[3] and have room for more optimizations than the am backend[4] (and work is underway to take advantage of some of those possibilities).

[1] https://lore.kernel.org/git/[email protected]/
[2] https://lore.kernel.org/git/CABPp-BGiu2nVMQY_t-rnFR5GQUz_ipyEE8oDocKeO+h+t4Mn4A@mail.gmail.com/
[3] https://public-inbox.org/git/CABPp-BF=ev03WgODk6TMQmuNoatg2kiEe5DR__gJ0OTVqHSnfQ@mail.gmail.com/
[4] https://lore.kernel.org/git/CABPp-BGh7yW69QwxQb13K0HM38NKmQif3A6C6UULEKYnkEJ5vA@mail.gmail.com/

---
 Documentation/git-rebase.txt           |  2 +-
 builtin/rebase.c                       |  4 ++--
 t/t5520-pull.sh                        | 10 ++++++----
 t/t9106-git-svn-commit-diff-clobber.sh |  3 ++-
 4 files changed, 11 insertions(+), 8 deletions(-)
Documentation/git-rebase.txt +1 -1
index cf1ac2e359..e819889a31 100644
@@ -309,7 +309,7 @@ See also INCOMPATIBLE OPTIONS below.
 --merge::
 	Use merging strategies to rebase.  When the recursive (default) merge
 	strategy is used, this allows rebase to be aware of renames on the
-	upstream side.
+	upstream side.  This is the default.
 +
 Note that a rebase merge works by replaying each commit from the working
 branch on top of the <upstream> branch.  Because of this, when a merge
builtin/rebase.c +2 -2
index eaa5eac59e..1c3d0b97d5 100644
@@ -104,7 +104,7 @@ struct rebase_options {
 #define REBASE_OPTIONS_INIT {			  	\
 		.type = REBASE_UNSPECIFIED,	  	\
 		.empty = EMPTY_UNSPECIFIED,	  	\
-		.default_backend = "am",	  	\
+		.default_backend = "merge",	  	\
 		.flags = REBASE_NO_QUIET, 		\
 		.git_am_opts = ARGV_ARRAY_INIT,		\
 		.git_format_patch_opt = STRBUF_INIT	\
@@ -1949,7 +1949,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
 
 	if (options.type == REBASE_UNSPECIFIED) {
 		if (!strcmp(options.default_backend, "merge"))
-			options.type = REBASE_MERGE;
+			imply_interactive(&options, "--merge");
 		else if (!strcmp(options.default_backend, "am"))
 			options.type = REBASE_AM;
 		else
t/t5520-pull.sh +6 -4
index 3fff6a06fa..4f9e7f7ff6 100755
@@ -340,7 +340,7 @@ test_expect_success '--rebase with conflicts shows advice' '
 	test_tick &&
 	git commit -m "Create conflict" seq.txt &&
 	test_must_fail git pull --rebase . seq 2>err >out &&
-	test_i18ngrep "Resolve all conflicts manually" out
+	test_i18ngrep "Resolve all conflicts manually" err
 '
 
 test_expect_success 'failed --rebase shows advice' '
@@ -354,7 +354,7 @@ test_expect_success 'failed --rebase shows advice' '
 	git checkout -f -b fails-to-rebase HEAD^ &&
 	test_commit v2-without-cr file "2" file2-lf &&
 	test_must_fail git pull --rebase . diverging 2>err >out &&
-	test_i18ngrep "Resolve all conflicts manually" out
+	test_i18ngrep "Resolve all conflicts manually" err
 '
 
 test_expect_success '--rebase fails with multiple branches' '
@@ -774,8 +774,10 @@ test_expect_success 'git pull --rebase does not reapply old patches' '
 	(
 		cd dst &&
 		test_must_fail git pull --rebase &&
-		find .git/rebase-apply -name "000*" >patches &&
-		test_line_count = 1 patches
+		cat .git/rebase-merge/done .git/rebase-merge/git-rebase-todo >work &&
+		grep -v -e \# -e ^$ work >patches &&
+		test_line_count = 1 patches &&
+		rm -f work
 	)
 '
 
t/t9106-git-svn-commit-diff-clobber.sh +2 -1
index dbe8deac0d..aec45bca3b 100755
@@ -92,7 +92,8 @@ test_expect_success 'multiple dcommit from git svn will not clobber svn' "
 
 
 test_expect_success 'check that rebase really failed' '
-	test -d .git/rebase-apply
+	git status >output &&
+	grep currently.rebasing output
 '
 
 test_expect_success 'resolve, continue the rebase and dcommit' "
Signature · 1 line
gitgitgadget
#18 lore raw

PATCH 10/15 rebase: add an --am option

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

Currently, this option doesn't do anything except error out if any options requiring the interactive-backend are also passed. However, when we make the default backend configurable later in this series, this flag will provide a way to override the config setting.

---
 Documentation/git-rebase.txt | 11 ++++++++++-
 builtin/rebase.c             | 18 +++++++++++++++++-
 2 files changed, 27 insertions(+), 2 deletions(-)
Documentation/git-rebase.txt +10 -1
index f1ace07c38..cf1ac2e359 100644
@@ -258,6 +258,13 @@ See also INCOMPATIBLE OPTIONS below.
 	original branch. The index and working tree are also left
 	unchanged as a result.
 
+--am:
+	Use git-am internally to rebase.  This option may become a
+	no-op in the future once the interactive backend handles
+	everything the am one does.
++
+See also INCOMPATIBLE OPTIONS below.
+
 --empty={drop,keep,ask}::
 	How to handle commits that become empty (because they contain a
 	subset of already upstream changes) or start empty.  With drop
@@ -372,7 +379,7 @@ See also INCOMPATIBLE OPTIONS below.
 	Ensure at least <n> lines of surrounding context match before
 	and after each change.  When fewer lines of surrounding
 	context exist they all must match.  By default no context is
-	ever ignored.
+	ever ignored.  Implies --am.
 +
 See also INCOMPATIBLE OPTIONS below.
 
@@ -417,6 +424,7 @@ with `--keep-base` in order to drop those commits from your branch.
 --whitespace=<option>::
 	This flag is passed to the 'git apply' program
 	(see linkgit:git-apply[1]) that applies the patch.
+	Implies --am.
 +
 See also INCOMPATIBLE OPTIONS below.
 
@@ -567,6 +575,7 @@ INCOMPATIBLE OPTIONS
 
 The following options:
 
+ * --am
  * --whitespace
  * -C
 
builtin/rebase.c +17 -1
index 9e7e88b147..ab9e16b206 100644
@@ -1361,6 +1361,18 @@ static int can_fast_forward(struct commit *onto, struct commit *upstream,
 	return res && is_linear_history(onto, head);
 }
 
+static int parse_opt_am(const struct option *opt, const char *arg, int unset)
+{
+	struct rebase_options *opts = opt->value;
+
+	BUG_ON_OPT_NEG(unset);
+	BUG_ON_OPT_ARG(arg);
+
+	opts->type = REBASE_AM;
+
+	return 0;
+}
+
 /* -i followed by -m is still -i */
 static int parse_opt_merge(const struct option *opt, const char *arg, int unset)
 {
@@ -1550,6 +1562,10 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
 		OPT_CMDMODE(0, "show-current-patch", &action,
 			    N_("show the patch file being applied or merged"),
 			    ACTION_SHOW_CURRENT_PATCH),
+		{ OPTION_CALLBACK, 0, "am", &options, NULL,
+			N_("use apply-mail strategies to rebase"),
+			PARSE_OPT_NOARG | PARSE_OPT_NONEG,
+			parse_opt_am },
 		{ OPTION_CALLBACK, 'm', "merge", &options, NULL,
 			N_("use merging strategies to rebase"),
 			PARSE_OPT_NOARG | PARSE_OPT_NONEG,
@@ -1910,7 +1926,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
 	if (isatty(2) && options.flags & REBASE_NO_QUIET)
 		strbuf_addstr(&options.git_format_patch_opt, " --progress");
 
-	if (options.git_am_opts.argc) {
+	if (options.git_am_opts.argc || options.type == REBASE_AM) {
 		/* all am options except -q are compatible only with --am */
 		for (i = options.git_am_opts.argc - 1; i >= 0; i--)
 			if (strcmp(options.git_am_opts.argv[i], "-q"))
Signature · 1 line
gitgitgadget
#26 lore raw
In reply to Elijah Newren via GitGitGadget

Hi Elijah,

Le 20/12/2019 à 18:09, Elijah Newren via GitGitGadget a écrit :

Quoted 103 lines

This series does a lot of work around making the default rebase backend configurable:

* provide configurability of handling empty commits and iron out differences between backends
* increase/fix capabilities of the merge/interactive backend to make it workable as the default
* document the remaining differences in backends more thoroughly
* add an --am option for explicitly requesting the am-backend
* extend merge/interactive backend testing to put it on par with the am backend
* add a 'rebase.backend' config option and:
* switch the rebase.backend default value from 'am' to 'merge'.

Areas I'd like reviewers to focus, in priority order:

* Patch 15 (Are we ready to switch the default backend? Or should we leave this patch out for now? Junio suggested we may be ready or at least are close[1])
* Patch 1 (Does my empty handling make sense? Do others agree I fixed --keep-empty, or do they view it as breaking it?)
* Patch 8 (Do the updates to the documentation of behavioral differences make sense? Is it too long?) * Patch 11 (okay to change the git-completion shell prompt slightly, especially in light of patches 15 & 16? We did a prompt change previously when we merged the merge backend with the interactive one, so I assume so, but just want to make sure people have a chance to chime in.)

If it's too soon to switch from the 'am' to 'merge' backend, we can just drop the last patch and I'll resubmit it later. I generated this series mostly through switching the default first and then watching what broke, but moved the patch to the end to make it easy to drop.

Briefly, reasons for switching the default backend boil down to the fact that the am-backend drops information and thus limits what it can do. This manifests in different ways:

* lack of tree information that would allow us to warn users that new files in old directories might want to move along with the other files that were renamed with those directories[1]
* incorrect application of patches in the presence of non-unique context lines[2], which could be avoided with access to the original files involved.
* less information available to annotate conflict markers (since am creates fake ancestors and commits on top of them, and doesn't have access to the original commits)

[1] https://lore.kernel.org/git/[email protected]/
[2] https://lore.kernel.org/git/[email protected]/
[3]
https://lore.kernel.org/git/CABPp-BGiu2nVMQY_t-rnFR5GQUz_ipyEE8oDocKeO+h+t4Mn4A@mail.gmail.com/

Elijah Newren (15):
rebase: extend the options for handling of empty commits
t3406: simplify an already simple test
rebase, sequencer: remove the broken GIT_QUIET handling
rebase: make sure to pass along the quiet flag to the sequencer
rebase: fix handling of restrict_revision
t3432: make these tests work with either am or merge backends
rebase: allow more types of rebases to fast-forward git-rebase.txt: add more details about behavioral differences of
backends
rebase: move incompatibility checks between backend options a bit
earlier
rebase: add an --am option
contrib: change the prompt for am-based rebases
rebase tests: mark tests specific to the am-backend with --am rebase tests: repeat some tests using the merge backend instead of am
rebase: make the backend configurable via config setting
rebase: change the default backend from "am" to "merge"

 Documentation/config/rebase.txt         |   8 ++
 Documentation/git-rebase.txt            | 150 ++++++++++++++++----
 builtin/rebase.c                        | 181 +++++++++++++++++++-----
 contrib/completion/git-prompt.sh        |   2 +-
 rebase-interactive.c                    |   4 +-
 rebase-interactive.h                    |   2 +-
 sequencer.c                             |  80 ++++++++---
 sequencer.h                             |   6 +-
 t/t3400-rebase.sh                       |  36 ++++-
 t/t3401-rebase-and-am-rename.sh         |   4 +-
 t/t3404-rebase-interactive.sh           |   2 +-
 t/t3406-rebase-message.sh               |  19 ++-
 t/t3407-rebase-abort.sh                 |   6 +-
 t/t3420-rebase-autostash.sh             |   2 +-
 t/t3421-rebase-topology-linear.sh       |   4 +-
 t/t3424-rebase-empty.sh                 |  89 ++++++++++++
 t/t3425-rebase-topology-merges.sh       |   8 +-
 t/t3427-rebase-subtree.sh               |  16 ++-
 t/t3432-rebase-fast-forward.sh          |  59 ++++----
 t/t3433-rebase-options-compatibility.sh |  13 +-
 t/t5407-post-rewrite-hook.sh            |  12 +-
 t/t5520-pull.sh                         |  27 +++-
 t/t6047-diff3-conflict-markers.sh       |  13 +-
 t/t7512-status-help.sh                  |  12 +-
 t/t9106-git-svn-commit-diff-clobber.sh  |   3 +-
 t/t9903-bash-prompt.sh                  |   6 +-
 26 files changed, 582 insertions(+), 182 deletions(-)
 create mode 100755 t/t3424-rebase-empty.sh
Quoted 5 lines

base-commit: 12029dc57db23baef008e77db1909367599210ee
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-679%2Fnewren%2Frebase-fixes-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-679/newren/rebase-fixes-v1
Pull-Request: https://github.com/git/git/pull/679

BTW, I have investigated on the performance regression when split-index is enabled, I’ll give my conclusions next week.

Cheers,
Alban

#27 lore raw

Hi Alban,

Quoted 8 lines · Alban Gruin

Hi Elijah,

Quoted 3 lines · Elijah Newren via GitGitGadget

This series does a lot of work around making the default rebase backend configurable:

Quoted 5 lines

BTW, I have investigated on the performance regression when split-index is enabled, I’ll give my conclusions next week.

Cheers,
Alban

Nice!

#2 lore raw

Re: [PATCH 01/15] rebase: extend the options for handling of empty commits

In reply to Elijah Newren via GitGitGadget
Quoted 8 lines · Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

Extend the interactive machinery with the ability to handle the full spread of options for how to handle commits that either start or become empty (by "become empty" I mean the changes in a commit are a subset of changes that exist upstream, so the net effect of applying the commit is no changes). Introduce a new command line flag for selecting the desired behavior:

Quoted 6 lines
    --empty={drop,keep,ask}

with the definitions:
drop: drop empty commits
keep: keep empty commits
ask: provide the user a chance to interact and pick what to do with
empty commits on a case-by-case basis

This looks like a logical and natural extension of the --keep-empty option.

After seeing the stress on "empty from the beginning and ending up to be empty" in the description, I somehow expected that we may be able to specify what happens to the empty commit separately, but that does not seem to be what the patch is about, which was somewhat disappointing.

builtin/rebase.c static int parse_opt_interactive(const struct option *opt, const char *arg, · line 1403

Quoted 10 lines
builtin/rebase.c +10 -0
+static long parse_empty_value(const char *value)
+{
+	if (!value)
+		return EMPTY_UNSPECIFIED;
+	else if (!strcasecmp(value, "drop"))
+		return EMPTY_DROP;
+	else if (!strcasecmp(value, "keep"))
+		return EMPTY_KEEP;
+	else if (!strcasecmp(value, "ask"))
+		return EMPTY_ASK;

Not an error but just silently ignored?

builtin/rebase.c static int parse_opt_interactive(const struct option *opt, const char *arg, · line 1410

Quoted 8 lines
builtin/rebase.c +7 -0
+	return EMPTY_UNSPECIFIED;
+}
+
 
+static int parse_opt_empty(const struct option *opt, const char *arg, int unset)
+{
+	struct rebase_options *options = opt->value;
+	long value = parse_empty_value(arg);

Ahh, OK.

Wouldn't it be better to make the variable and the parsing helper function of type "enum empty_type", not "long", just like the field in the rebase_options struct?

Quoted 8 lines
builtin/rebase.c +8 -0
+	BUG_ON_OPT_NEG(unset);
+	if (value < 0)
+		return error(_("option empty accepts \"drop\", "
+			       "\"keep\", and \"ask\""));
+
+	options->empty = value;
+	return 0;
+}
#8 lore raw

Re: [PATCH 03/15] rebase, sequencer: remove the broken GIT_QUIET handling

In reply to Elijah Newren via GitGitGadget
Quoted 9 lines · Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

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.

Nice to see a useless code go ;-)

#11 lore raw

Re: [PATCH 05/15] rebase: fix handling of restrict_revision

In reply to Elijah Newren via GitGitGadget
Quoted 11 lines · Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

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.

Thanks.

Quoted 56 lines
---
 builtin/rebase.c  |  4 ++--
 t/t3400-rebase.sh | 20 +++++++++++++++++++-
 2 files changed, 21 insertions(+), 3 deletions(-)
builtin/rebase.c +2 -2
index cc8f3f008f..b320bb3a30 100644
@@ -364,8 +364,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,
t/t3400-rebase.sh +19 -1
index 79762b989a..71fd6396cd 100755
@@ -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 &&
#20 lore raw

Re: [PATCH 11/15] contrib: change the prompt for am-based rebases

In reply to Elijah Newren via GitGitGadget

The subject prefix for this file is most of the time 'bash prompt' or 'git-prompt'.

Quoted 12 lines · Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

The prompt for am-based rebases was REBASE, while for interactive-based rebases was REBASE-i. A while ago, we switched merge-based rebases from using REBASE-m to REBASE-i via re-implementing the merge backend based on the interactive backend. We will soon be changing the default rebase backend to the interactive one, meaning the default prompt will be REBASE-i rather than REBASE. We have also noted in the documentation that currently am-specific options will be implemented in the interactive backend, and even the --am flag may eventually imply an interactive-based rebase. As such, change the prompt for an am-based rebase from REBASE to REBASE-a.

I had a bit of a hard time following which rebase variant is implemented with which backend and when it was changed, and... etc.

Makes me wonder: do we really need those "-i", "-m" or "-a" suffixes? What benefit do they bring? Why don't we just say "REBASE" in the prompt?

Quoted 41 lines
---
 contrib/completion/git-prompt.sh | 2 +-
 t/t9903-bash-prompt.sh           | 6 +++---
 2 files changed, 4 insertions(+), 4 deletions(-)
contrib/completion/git-prompt.sh +1 -1
index 1d510cd47b..3c81099d60 100644
@@ -440,7 +440,7 @@ __git_ps1 ()
 			__git_eread "$g/rebase-apply/last" total
 			if [ -f "$g/rebase-apply/rebasing" ]; then
 				__git_eread "$g/rebase-apply/head-name" b
-				r="|REBASE"
+				r="|REBASE-a"
 			elif [ -f "$g/rebase-apply/applying" ]; then
 				r="|AM"
 			else
t/t9903-bash-prompt.sh +3 -3
index 88bc733ad6..8da5b1aee2 100755
@@ -189,11 +189,11 @@ test_expect_success 'prompt - rebase merge' '
 	test_cmp expected "$actual"
 '
 
-test_expect_success 'prompt - rebase' '
-	printf " (b2|REBASE 1/3)" >expected &&
+test_expect_success 'prompt - rebase am' '
+	printf " (b2|REBASE-a 1/3)" >expected &&
 	git checkout b2 &&
 	test_when_finished "git checkout master" &&
-	test_must_fail git rebase b1 b2 &&
+	test_must_fail git rebase --am b1 b2 &&
 	test_when_finished "git rebase --abort" &&
 	__git_ps1 >"$actual" &&
 	test_cmp expected "$actual"
Signature · 1 line
gitgitgadget
#21 lore raw

Re: [PATCH 11/15] contrib: change the prompt for am-based rebases

Quoted 18 lines · SZEDER Gábor

The subject prefix for this file is most of the time 'bash prompt' or 'git-prompt'.

Quoted 12 lines · Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

The prompt for am-based rebases was REBASE, while for interactive-based rebases was REBASE-i. A while ago, we switched merge-based rebases from using REBASE-m to REBASE-i via re-implementing the merge backend based on the interactive backend. We will soon be changing the default rebase backend to the interactive one, meaning the default prompt will be REBASE-i rather than REBASE. We have also noted in the documentation that currently am-specific options will be implemented in the interactive backend, and even the --am flag may eventually imply an interactive-based rebase. As such, change the prompt for an am-based rebase from REBASE to REBASE-a.

Quoted 2 lines

I had a bit of a hard time following which rebase variant is implemented with which backend and when it was changed, and... etc.

Sorry about that. I could fix it up, but...

Quoted 3 lines

Makes me wonder: do we really need those "-i", "-m" or "-a" suffixes? What benefit do they bring? Why don't we just say "REBASE" in the prompt?

Ooh, I like this idea. A lot easier to explain in the commit message and probably less confusing for users as well.

#3 lore raw

Re: [PATCH 01/15] rebase: extend the options for handling of empty commits

In reply to Junio C Hamano
Quoted 18 lines · Junio C Hamano
Quoted 8 lines · Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

Extend the interactive machinery with the ability to handle the full spread of options for how to handle commits that either start or become empty (by "become empty" I mean the changes in a commit are a subset of changes that exist upstream, so the net effect of applying the commit is no changes). Introduce a new command line flag for selecting the desired behavior:

Quoted 6 lines
    --empty={drop,keep,ask}

with the definitions:
drop: drop empty commits
keep: keep empty commits
ask: provide the user a chance to interact and pick what to do with
empty commits on a case-by-case basis

Quoted 8 lines

This looks like a logical and natural extension of the --keep-empty option.

After seeing the stress on "empty from the beginning and ending up to be empty" in the description, I somehow expected that we may be able to specify what happens to the empty commit separately, but that does not seem to be what the patch is about, which was somewhat disappointing.

My main reason for stressing both was that I've commits and code that talked about "empty" commits, but which often only dealt with one of the two types (sometimes when I would expect both to have been addressed), so I was just trying to be careful in my explanation of what it was about.

If wanted, we could implement finer control, but I don't know what the UI would look like or what the range of options we might want. Do we want to mix all possible permutations and have two flags, e.g. --become-empty=drop --start-empty=keep or --start-empty=ask --become-empty=keep? Would --empty=<whatever> imply both --become-empty=<whatever> and --start-empty=<whatever> or would users need to specify both? Do we think all the permutations make sense, or would we want to limit users to some subset?

The current choice of three covered the things that I had wanted in the past, and which I think made for good defaults for various different flags, so I just implemented it. We could always extend it later.

Quoted 13 lines

builtin/rebase.c static int parse_opt_interactive(const struct option *opt, const char *arg, · line 1403

Quoted 11 lines
builtin/rebase.c +10 -0
+static long parse_empty_value(const char *value)
+{
+     if (!value)
+             return EMPTY_UNSPECIFIED;
+     else if (!strcasecmp(value, "drop"))
+             return EMPTY_DROP;
+     else if (!strcasecmp(value, "keep"))
+             return EMPTY_KEEP;
+     else if (!strcasecmp(value, "ask"))
+             return EMPTY_ASK;

Not an error but just silently ignored?

Oops, I'll switch it to an error.

Quoted 11 lines

builtin/rebase.c static int parse_opt_interactive(const struct option *opt, const char *arg, · line 1410

Quoted 8 lines
builtin/rebase.c +7 -0
+     return EMPTY_UNSPECIFIED;
+}
+
 
+static int parse_opt_empty(const struct option *opt, const char *arg, int unset)
+{
+     struct rebase_options *options = opt->value;
+     long value = parse_empty_value(arg);

Ahh, OK.

Quoted 3 lines

Wouldn't it be better to make the variable and the parsing helper function of type "enum empty_type", not "long", just like the field in the rebase_options struct?

Indeed, I'll fix this up.

#4 lore raw

Re: [PATCH 01/15] rebase: extend the options for handling of empty commits

Quoted 19 lines · Elijah Newren
Quoted 4 lines · Junio C Hamano

"Elijah Newren via GitGitGadget" <[email protected]> writes:

Quoted 12 lines

builtin/rebase.c static int parse_opt_interactive(const struct option *opt, const char *arg, · line 1403

Quoted 10 lines
builtin/rebase.c +10 -0
+static long parse_empty_value(const char *value)
+{
+     if (!value)
+             return EMPTY_UNSPECIFIED;
+     else if (!strcasecmp(value, "drop"))
+             return EMPTY_DROP;
+     else if (!strcasecmp(value, "keep"))
+             return EMPTY_KEEP;
+     else if (!strcasecmp(value, "ask"))
+             return EMPTY_ASK;

Not an error but just silently ignored?

Quoted 1 line

Oops, I'll switch it to an error.

By the way, diff.c's parse_algorithm_value() and diff_opt_diff_algorithm() behave the same way, where parse_algorithm_value() will return a negative value if it's not one of the valid known cases, and diff_opt_diff_algorithm() checks for negative and throws an error. Should I also modify those to put the error in parse_algorithm_value()? (Not part of this series, but as a consistency cleanup since it clearly surprised you and even I forgot that I had the calling function returning the error when I copied that style?)

#5 lore raw

Re: [PATCH 01/15] rebase: extend the options for handling of empty commits

In reply to Elijah Newren
Quoted 13 lines · Elijah Newren
Quoted 4 lines
Quoted 4 lines
builtin/rebase.c +4 -0
+static long parse_empty_value(const char *value)
+{
+     if (!value)
+             return EMPTY_UNSPECIFIED;
Quoted 8 lines

builtin/rebase.c static int parse_opt_interactive(const struct option *opt, const char *arg, · line 1403

Quoted 6 lines
builtin/rebase.c +6 -0
+     else if (!strcasecmp(value, "drop"))
+             return EMPTY_DROP;
+     else if (!strcasecmp(value, "keep"))
+             return EMPTY_KEEP;
+     else if (!strcasecmp(value, "ask"))
+             return EMPTY_ASK;

Not an error but just silently ignored?

Quoted 1 line

Oops, I'll switch it to an error.

Not necessarily (see "Ahh, OK" below).

Quoted 11 lines
Quoted 11 lines

builtin/rebase.c static int parse_opt_interactive(const struct option *opt, const char *arg, · line 1410

Quoted 8 lines
builtin/rebase.c +7 -0
+     return EMPTY_UNSPECIFIED;
+}
+
 
+static int parse_opt_empty(const struct option *opt, const char *arg, int unset)
+{
+     struct rebase_options *options = opt->value;
+     long value = parse_empty_value(arg);

Ahh, OK.

Quoted 5 lines
Quoted 3 lines

Wouldn't it be better to make the variable and the parsing helper function of type "enum empty_type", not "long", just like the field in the rebase_options struct?

Indeed, I'll fix this up.

Thanks. I was wondering if there is something subtle I didn't see going on around here.

#13 lore raw

Re: [PATCH 06/15] t3432: make these tests work with either am or merge backends

In reply to Elijah Newren via GitGitGadget

Hi Elijah,

Quoted 7 lines · Elijah Newren via GitGitGadget
t/t3432-rebase-fast-forward.sh +0 -0
index 92f95b57da..3879a43fa0 100755
@@ -44,19 +44,16 @@ test_rebase_same_head_ () {
 	test_expect_$status "git rebase$flag $* with $changes is $what with $cmp HEAD" "
 		oldhead=\$(git rev-parse HEAD) &&

t/t3432-rebase-fast-forward.sh test_rebase_same_head_ () { · line 52

Quoted 10 lines
t/t3432-rebase-fast-forward.sh +3 -3
 		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
+			wc -l .git/logs/HEAD >old &&
+			wc -l .git/logs/HEAD >new &&

Are the above two lines some yet-to-be cleaned up leftovers? I can't see where old and new are being used elsewhere.

Quoted 12 lines
t/t3432-rebase-fast-forward.sh +2 -5
+			test_line_count '-gt' $(($old + 2)) .git/logs/HEAD
 		elif test $what = noop
 		then
-			test_i18ngrep 'is up to date' stdout &&
-			test_i18ngrep ! 'rebase forced' stdout
-		elif test $what = noop-force
-		then
-			test_i18ngrep 'is up to date, rebase forced' stdout
+			test_cmp expect .git/logs/HEAD
 		fi &&
 		newhead=\$(git rev-parse HEAD) &&
 		if test $cmp = same
#14 lore raw

Re: [PATCH 06/15] t3432: make these tests work with either am or merge backends

Quoted 16 lines · Denton Liu

Hi Elijah,

Quoted 12 lines · Elijah Newren via GitGitGadget
t/t3432-rebase-fast-forward.sh +1 -0
index 92f95b57da..3879a43fa0 100755
@@ -44,19 +44,16 @@ 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
Quoted 8 lines

t/t3432-rebase-fast-forward.sh test_rebase_same_head_ () { · line 52

Quoted 5 lines
t/t3432-rebase-fast-forward.sh +2 -3
-                     # Must check this case first, for 'is up to
-                     # date, rebase forced[...]rewinding head' cases
-                     test_i18ngrep 'rewinding head' stdout
+                     wc -l .git/logs/HEAD >old &&
+                     wc -l .git/logs/HEAD >new &&

Are the above two lines some yet-to-be cleaned up leftovers? I can't see where old and new are being used elsewhere.

Um, this whole section is broken and causes it to pass the test for stupid reasons. Not sure how I bungled that so badly. I'll send a re-roll with a corrected 6/15.

Quoted 13 lines
Quoted 13 lines
t/t3432-rebase-fast-forward.sh +2 -5
+                     test_line_count '-gt' $(($old + 2)) .git/logs/HEAD
              elif test $what = noop
              then
-                     test_i18ngrep 'is up to date' stdout &&
-                     test_i18ngrep ! 'rebase forced' stdout
-             elif test $what = noop-force
-             then
-                     test_i18ngrep 'is up to date, rebase forced' stdout
+                     test_cmp expect .git/logs/HEAD
              fi &&
              newhead=\$(git rev-parse HEAD) &&
              if test $cmp = same
#30 lore raw

PATCH v2 2/15 t3406: simplify an already simple test

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

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.

---
 t/t3406-rebase-message.sh | 7 ++-----
 1 file changed, 2 insertions(+), 5 deletions(-)
t/t3406-rebase-message.sh +2 -5
index b393e1e9fe..0c2c569f95 100755
@@ -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' '
Signature · 1 line
gitgitgadget
#29 lore raw

PATCH v2 1/15 rebase: extend the options for handling of empty commits

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

Extend the interactive machinery with the ability to handle the full spread of options for how to handle commits that either start or become empty (by "become empty" I mean the changes in a commit are a subset of changes that exist upstream, so the net effect of applying the commit is no changes). Introduce a new command line flag for selecting the desired behavior:

    --empty={drop,keep,ask}

with the definitions:
drop: drop empty commits
keep: keep empty commits
ask: provide the user a chance to interact and pick what to do with
empty commits on a case-by-case basis

Note that traditionally, am-based rebases have always dropped commits that either started or became empty, while interactive-based rebases have defaulted to ask (and provided an option to keep commits that started empty). This difference made sense since users of an am-based rebase just wanted to quickly batch apply a sequence of commits, while users editing a todo list will likely want the chance to interact and handle unusual cases on a case-by-case basis. However, not all rebases using the interactive machinery are explicitly interactive anymore. In particular --merge was always meant to behave more like --am: just rebase a batch of commits without popping up a todo list.

If the --empty flag is not specified, pick defaults as follows:
explicitly interactive: ask
--exec: keep (exec is about checking existing commits, and often
used without actually changing the base. Thus the expectation is that the user doesn't necessarily want anything to change; they just want to test).
otherwise: drop

Also, this commit makes --keep-empty just imply --empty=keep, and hides it from help so that we aren't confusing users with different ways to do the same thing. (I could have added a --drop-empty flag, but then that invites users to specify both --keep-empty and --drop-empty and we have to add sanity checking around that; it seems cleaner to have a single multi-valued option.) This actually fixes --keep-empty too; previously, it only meant to sometimes keep empty commits, in particular commits which started empty would be kept. But it would still error out and ask the user what to do with commits that became empty. Now it keeps empty commits, as instructed.

---
 Documentation/git-rebase.txt      | 35 ++++++------
 builtin/rebase.c                  | 83 +++++++++++++++++++++++++---
 rebase-interactive.c              |  4 +-
 rebase-interactive.h              |  2 +-
 sequencer.c                       | 74 +++++++++++++++++++------
 sequencer.h                       |  6 ++-
 t/t3421-rebase-topology-linear.sh |  4 +-
 t/t3424-rebase-empty.sh           | 89 +++++++++++++++++++++++++++++++
 t/t3427-rebase-subtree.sh         | 16 +++---
 9 files changed, 263 insertions(+), 50 deletions(-)
 create mode 100755 t/t3424-rebase-empty.sh
Documentation/git-rebase.txt +20 -15
index 1d0e2d27cc..ff32ca1080 100644
@@ -258,9 +258,25 @@ 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 become empty (because they contain a
+	subset of already upstream changes) or start empty.  With drop
+	(the default), commits that start or become empty are dropped.
+	With keep (implied by --exec), 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 or
+	commit it.  Also with ask, if the rebase is interactive then
+	commits which start empty will be commented out in the todo
+	action list (giving you a chance to uncomment).
++
+Note that this has no effect on commits which are already upstream (as
+can be checked via `git log --cherry-mark ...`), which are always
+dropped by rebase.
++
+See also INCOMPATIBLE OPTIONS below.
+
 --keep-empty::
-	Keep the commits that do not change anything from its
-	parents in the result.
+	Deprecated alias for what is now known as --empty=keep.
 +
 See also INCOMPATIBLE OPTIONS below.
 
@@ -569,6 +585,7 @@ are incompatible with the following options:
  * --interactive
  * --exec
  * --keep-empty
+ * --empty=
  * --edit-todo
  * --root when used in combination with --onto
 
@@ -580,6 +597,7 @@ In addition, the following pairs of options are incompatible:
  * --preserve-merges and --ignore-whitespace
  * --preserve-merges and --committer-date-is-author-date
  * --preserve-merges and --ignore-date
+ * --preserve-merges and --empty=
  * --keep-base and --onto
  * --keep-base and --root
 
@@ -588,19 +606,6 @@ BEHAVIORAL DIFFERENCES
 
 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 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.
-
 Directory rename detection
 ~~~~~~~~~~~~~~~~~~~~~~~~~~
 
builtin/rebase.c +75 -8

157 lines not on this page · raw message

rebase-interactive.c +2 -2
index aa18ae82b7..ad82bf77df 100644
@@ -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(unsigned no_ask_empty, int command_count,
 		      const char *shortrevisions, const char *shortonto,
 		      struct strbuf *buf)
 {
@@ -81,7 +81,7 @@ void append_todo_help(unsigned keep_empty, int command_count,
 
 	strbuf_add_commented_lines(buf, msg, strlen(msg));
 
-	if (!keep_empty) {
+	if (!no_ask_empty) {
 		msg = _("Note that empty commits are commented out");
 		strbuf_add_commented_lines(buf, msg, strlen(msg));
 	}
rebase-interactive.h +1 -1
index 44dbb06311..f531e00ba7 100644
@@ -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(unsigned no_ask_empty, int command_count,
 		      const char *shortrevisions, const char *shortonto,
 		      struct strbuf *buf);
 int edit_todo_list(struct repository *r, struct todo_list *todo_list,
sequencer.c +59 -15

167 lines not on this page · raw message

sequencer.h +5 -1
index e9a0e03ea2..1c3abb661c 100644
@@ -39,7 +39,9 @@ struct replay_opts {
 	int allow_rerere_auto;
 	int allow_empty;
 	int allow_empty_message;
+	int drop_redundant_commits;
 	int keep_redundant_commits;
+	int ask_on_initially_empty;
 	int verbose;
 	int quiet;
 	int reschedule_failed_exec;
@@ -134,7 +136,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)
@@ -150,6 +152,8 @@ int sequencer_remove_state(struct replay_opts *opts);
  * `--onto`, we do not want to re-generate the root commits.
  */
 #define TODO_LIST_ROOT_WITH_ONTO (1U << 6)
+#define TODO_LIST_DROP_EMPTY (1U << 7)
+#define TODO_LIST_ASK_EMPTY (1U << 8)
 
 
 int sequencer_make_script(struct repository *r, struct strbuf *out, int argc,
t/t3421-rebase-topology-linear.sh +2 -2
index 325072b0a3..d23e0bf778 100755
@@ -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
t/t3424-rebase-empty.sh new file +89 -0

89 lines not on this page · raw message

t/t3427-rebase-subtree.sh +10 -6
index bec48e6a1f..468ebc1bef 100755
@@ -85,23 +85,27 @@ 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 --empty=ask --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 --empty=ask --onto files-master master &&
 	: first pick results in no changes &&
-	git rebase --continue &&
+	test_must_fail git rebase --skip &&
+	: last pick was an empty commit that has no changes, but we want to keep it &&
+	git commit --allow-empty &&
 	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 --empty=ask --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 --empty=ask --rebase-merges --onto files-master --root &&
 	: first pick results in no changes &&
-	git rebase --continue &&
+	test_must_fail git rebase --skip &&
+	: last pick was an empty commit that has no changes, but we want to keep it &&
+	git commit --allow-empty &&
 	verbose test "$(commit_message HEAD~2)" = "master4" &&
 	verbose test "$(commit_message HEAD~)" = "files_subtree/master5" &&
 	verbose test "$(commit_message HEAD)" = "Empty commit"
Signature · 1 line
gitgitgadget
#32 lore raw

PATCH v2 4/15 rebase: make sure to pass along the quiet flag to the sequencer

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

---
 builtin/rebase.c  | 3 ++-
 t/t3400-rebase.sh | 8 +++++++-
 2 files changed, 9 insertions(+), 2 deletions(-)
builtin/rebase.c +2 -1
index 32026a62e8..5014c9a437 100644
@@ -126,6 +126,7 @@ static struct replay_opts get_replay_opts(const struct rebase_options *opts)
 	replay.keep_redundant_commits = (opts->empty == EMPTY_KEEP);
 	replay.ask_on_initially_empty = (opts->empty == EMPTY_ASK &&
 					 !(opts->flags & REBASE_INTERACTIVE_EXPLICIT));
+	replay.quiet = !(opts->flags & REBASE_NO_QUIET);
 	replay.verbose = opts->flags & REBASE_VERBOSE;
 	replay.reschedule_failed_exec = opts->reschedule_failed_exec;
 	replay.committer_date_is_author_date =
@@ -1502,7 +1503,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),
t/t3400-rebase.sh +7 -1
index 221b35f2df..79762b989a 100755
@@ -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" &&
Signature · 1 line
gitgitgadget
#28 lore raw

v2 cover rebase: make the default backend configurable

In reply to Elijah Newren via GitGitGadget

This series does a lot of work around making the default rebase backend configurable, and switching the default from the am backend to the merge/interactive one.

See the cover letter for v1[*] for a more detailed motivation for the series, the type of changes being made, and areas I'd like to reviewers to focus on.

Changes since v1:

* Made some cleanups to the empty type parsing, as suggested by Junio (Patch 1)
* Changed the shell prompt for all rebases to just be "REBASE", as suggested by SZEDER (Patch 11)
* Correctly check that work was done (by checking that reflog grew, patch 6); flagged by Denton

[*]
https://lore.kernel.org/git/[email protected]/

Elijah Newren (15):
rebase: extend the options for handling of empty commits
t3406: simplify an already simple test
rebase, sequencer: remove the broken GIT_QUIET handling
rebase: make sure to pass along the quiet flag to the sequencer
rebase: fix handling of restrict_revision
t3432: make these tests work with either am or merge backends
rebase: allow more types of rebases to fast-forward git-rebase.txt: add more details about behavioral differences of
backends
rebase: move incompatibility checks between backend options a bit
earlier
rebase: add an --am option
contrib: change the prompt for interactive-based rebases rebase tests: mark tests specific to the am-backend with --am rebase tests: repeat some tests using the merge backend instead of am
rebase: make the backend configurable via config setting
rebase: change the default backend from "am" to "merge"

 Documentation/config/rebase.txt         |   8 ++
 Documentation/git-rebase.txt            | 150 ++++++++++++++++----
 builtin/rebase.c                        | 177 +++++++++++++++++++-----
 contrib/completion/git-prompt.sh        |   4 +-
 rebase-interactive.c                    |   4 +-
 rebase-interactive.h                    |   2 +-
 sequencer.c                             |  80 ++++++++---
 sequencer.h                             |   6 +-
 t/t3400-rebase.sh                       |  36 ++++-
 t/t3401-rebase-and-am-rename.sh         |   4 +-
 t/t3404-rebase-interactive.sh           |   2 +-
 t/t3406-rebase-message.sh               |  19 ++-
 t/t3407-rebase-abort.sh                 |   6 +-
 t/t3420-rebase-autostash.sh             |   2 +-
 t/t3421-rebase-topology-linear.sh       |   4 +-
 t/t3424-rebase-empty.sh                 |  89 ++++++++++++
 t/t3425-rebase-topology-merges.sh       |   8 +-
 t/t3427-rebase-subtree.sh               |  16 ++-
 t/t3432-rebase-fast-forward.sh          |  54 ++++----
 t/t3433-rebase-options-compatibility.sh |  13 +-
 t/t5407-post-rewrite-hook.sh            |  12 +-
 t/t5520-pull.sh                         |  27 +++-
 t/t6047-diff3-conflict-markers.sh       |  13 +-
 t/t7512-status-help.sh                  |  12 +-
 t/t9106-git-svn-commit-diff-clobber.sh  |   3 +-
 t/t9903-bash-prompt.sh                  |   8 +-
 26 files changed, 577 insertions(+), 182 deletions(-)
 create mode 100755 t/t3424-rebase-empty.sh

base-commit: 12029dc57db23baef008e77db1909367599210ee
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-679%2Fnewren%2Frebase-fixes-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-679/newren/rebase-fixes-v2
Pull-Request: https://github.com/git/git/pull/679

Range-diff vs v1:

1: 13e2056e78 ! 1: 1c2b77e94d rebase: extend the options for handling of empty commits

     @@ -219,28 +219,24 @@
       	return 0;
       }
       
     -+static long parse_empty_value(const char *value)
     ++static enum empty_type parse_empty_value(const char *value)
      +{
     -+	if (!value)
     -+		return EMPTY_UNSPECIFIED;
     -+	else if (!strcasecmp(value, "drop"))
     ++	if (!strcasecmp(value, "drop"))
      +		return EMPTY_DROP;
      +	else if (!strcasecmp(value, "keep"))
      +		return EMPTY_KEEP;
      +	else if (!strcasecmp(value, "ask"))
      +		return EMPTY_ASK;
     -+	return EMPTY_UNSPECIFIED;
     ++
     ++	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;
     -+	long value = parse_empty_value(arg);
     ++	enum empty_type value = parse_empty_value(arg);
      +
      +	BUG_ON_OPT_NEG(unset);
     -+	if (value < 0)
     -+		return error(_("option empty accepts \"drop\", "
     -+			       "\"keep\", and \"ask\""));
      +
      +	options->empty = value;
      +	return 0;
  2:  47ea99fb30 =  2:  bd3c5ec155 t3406: simplify an already simple test
  3:  b99feebae1 =  3:  49388b79fd rebase, sequencer: remove the broken GIT_QUIET handling
  4:  66bee10d1c =  4:  478479358f rebase: make sure to pass along the quiet flag to the sequencer
  5:  b84faa8684 =  5:  ee26f5a161 rebase: fix handling of restrict_revision
  6:  9b1ad46c58 !  6:  34a69def33 t3432: make these tests work with either am or merge backends
     @@ -37,9 +37,8 @@
      -			# Must check this case first, for 'is up to
      -			# date, rebase forced[...]rewinding head' cases
      -			test_i18ngrep 'rewinding head' stdout
     -+			wc -l .git/logs/HEAD >old &&
     -+			wc -l .git/logs/HEAD >new &&
     -+			test_line_count '-gt' $(($old + 2)) .git/logs/HEAD
     ++			old=\$(wc -l <expect) &&
     ++			test_line_count '-gt' \$old .git/logs/HEAD
       		elif test $what = noop
       		then
      -			test_i18ngrep 'is up to date' stdout &&
     @@ -108,10 +107,3 @@
       test_rebase_same_head success noop same success work diff --onto master... master
       test_rebase_same_head success noop same success work diff --keep-base master
       test_rebase_same_head success noop same success work diff --keep-base
     --test_rebase_same_head failure work same success work diff --fork-point --onto B B
     --test_rebase_same_head failure work same success work diff --fork-point --onto B... B
     -+test_rebase_same_head success work same success work diff --fork-point --onto B B
     -+test_rebase_same_head success work same success work diff --fork-point --onto B... B
     - test_rebase_same_head success noop same success work diff --fork-point --onto master... master
     - test_rebase_same_head success noop same success work diff --fork-point --keep-base master
     - 
  7:  a4073df910 =  7:  f2c92853b4 rebase: allow more types of rebases to fast-forward
  8:  90e8927ea0 =  8:  b307340f7c git-rebase.txt: add more details about behavioral differences of backends
  9:  5c31d664f3 =  9:  7c3f2e07f3 rebase: move incompatibility checks between backend options a bit earlier
 10:  340bf71c5c = 10:  1df11f0b51 rebase: add an --am option
 11:  1c3f8ba328 ! 11:  94b5a3051d contrib: change the prompt for am-based rebases

@@ -1,17 +1,38 @@
Author: Elijah Newren <[email protected]>

- contrib: change the prompt for am-based rebases
-
- The prompt for am-based rebases was REBASE, while for interactive-based
- rebases was REBASE-i. A while ago, we switched merge-based rebases from
- using REBASE-m to REBASE-i via re-implementing the merge backend based
- on the interactive backend. We will soon be changing the default rebase
- backend to the interactive one, meaning the default prompt will be
- REBASE-i rather than REBASE. We have also noted in the documentation
- that currently am-specific options will be implemented in the
- interactive backend, and even the --am flag may eventually imply an
- interactive-based rebase. As such, change the prompt for an am-based
- rebase from REBASE to REBASE-a.
+ contrib: change the prompt for interactive-based rebases
+
+ In the past, we had different prompts for different types of rebases:
+ REBASE: for am-based rebases
+ REBASE-m: for merge-based rebases
+ REBASE-i: for interactive-based rebases
+
+ It's not clear why this distinction was necessary or helpful; when the
+ prompt was added in commit e75201963f67 ("Improve bash prompt to detect
+ various states like an unfinished merge", 2007-09-30), it simply added
+ these three different types. Perhaps there was a useful purpose back
+ then, but there have been some changes:
+
+ * The merge backend was deleted after being implemented on top of the
+ interactive backend, causing the prompt for merge-based rebases to
+ change from REBASE-m to REBASE-i.
+ * The interactive backend is used for multiple different types of
+ non-interactive rebases, so the "-i" part of the prompt doesn't
+ really mean what it used to.
+ * Rebase backends have gained more abilities and have a great deal of
+ overlap, sometimes making it hard to distinguish them.
+ * Behavioral differences between the backends have also been ironed
+ out.
+ * We want to change the default backend from am to interactive, which
+ means people would get "REBASE-i" by default if we didn't change
+ the prompt, and only if they specified --am or --whitespace or -C
+ would they get the "REBASE" prompt.
+ * In the future, we plan to have "--whitespace", "-C", and even "--am"
+ run the interactive backend once it can handle everything the
+ am-backend can.
+
+ For all these reasons, make the prompt for any type of rebase just be
+ "REBASE".

@@ -19,26 +40,46 @@
--- a/contrib/completion/git-prompt.sh
+++ b/contrib/completion/git-prompt.sh
@@
- __git_eread "$g/rebase-apply/last" total
- if [ -f "$g/rebase-apply/rebasing" ]; then
- __git_eread "$g/rebase-apply/head-name" b
-- r="|REBASE"
-+ r="|REBASE-a"
- elif [ -f "$g/rebase-apply/applying" ]; then
- r="|AM"
- else
+ __git_eread "$g/rebase-merge/msgnum" step
+ __git_eread "$g/rebase-merge/end" total
+ if [ -f "$g/rebase-merge/interactive" ]; then
+- r="|REBASE-i"
++ r="|REBASE"
+ else
+- r="|REBASE-m"
++ r="|REBASE"
+ fi
+ else
+ if [ -d "$g/rebase-apply" ]; then

diff --git a/t/t9903-bash-prompt.sh b/t/t9903-bash-prompt.sh --- a/t/t9903-bash-prompt.sh
+++ b/t/t9903-bash-prompt.sh
+@@
+ '
+
+ test_expect_success 'prompt - interactive rebase' '
+- printf " (b1|REBASE-i 2/3)" >expected &&
++ printf " (b1|REBASE 2/3)" >expected &&
+ write_script fake_editor.sh <<-\EOF &&
+ echo "exec echo" >"$1"
+ echo "edit $(git log -1 --format="%h")" >>"$1"
+@@
+ '
+
+ test_expect_success 'prompt - rebase merge' '
+- printf " (b2|REBASE-i 1/3)" >expected &&
++ printf " (b2|REBASE 1/3)" >expected &&
+ git checkout b2 &&
+ test_when_finished "git checkout master" &&
+ test_must_fail git rebase --merge b1 b2 &&
@@
test_cmp expected "$actual"
'

-test_expect_success 'prompt - rebase' '
-- printf " (b2|REBASE 1/3)" >expected &&
+test_expect_success 'prompt - rebase am' '
-+ printf " (b2|REBASE-a 1/3)" >expected &&
+ printf " (b2|REBASE 1/3)" >expected &&
git checkout b2 && test_when_finished "git checkout master" &&
- test_must_fail git rebase b1 b2 &&

 12:  bcac3b77e8 = 12:  c905d288bf rebase tests: mark tests specific to the am-backend with --am
 13:  81d0f18b3e = 13:  0287881361 rebase tests: repeat some tests using the merge backend instead of am
 14:  04db4472d3 = 14:  ec782e711c rebase: make the backend configurable via config setting
 15:  999587933b = 15:  7adcbc0bc5 rebase: change the default backend from "am" to "merge"
Signature · 1 line
gitgitgadget
#31 lore raw

PATCH v2 3/15 rebase, sequencer: remove the broken GIT_QUIET handling

From: Elijah Newren <[email protected]>

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.

---
 builtin/rebase.c | 6 ++----
 sequencer.c      | 6 ++----
 2 files changed, 4 insertions(+), 8 deletions(-)
builtin/rebase.c +2 -4
index 6903249307..32026a62e8 100644
@@ -718,8 +718,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)
@@ -1178,8 +1178,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);
sequencer.c +2 -4
index d2c11f34b7..71062212a5 100644
@@ -2688,8 +2688,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)
@@ -2698,8 +2696,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)
Signature · 1 line
gitgitgadget
#36 lore raw

PATCH v2 8/15 git-rebase.txt: add more details about behavioral differences of backends

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

---
 Documentation/git-rebase.txt            | 102 +++++++++++++++++++++---
 t/t3433-rebase-options-compatibility.sh |   5 +-
 2 files changed, 94 insertions(+), 13 deletions(-)
Documentation/git-rebase.txt +92 -10
index ff32ca1080..f1ace07c38 100644
@@ -409,13 +409,10 @@ your branch contains commits which were dropped, this option can be used
 with `--keep-base` in order to drop those commits from your branch.
 
 --ignore-whitespace::
-	Behaves differently depending on which backend is selected.
-+
-'am' backend: When applying a patch, ignore changes in whitespace in
-context lines if necessary.
-+
-'interactive' backend: Treat lines with only whitespace changes as
-unchanged for the sake of a three-way merge.
+	Ignore whitespace-only changes in the commits being rebased,
+	which may avoid "unnecessary" conflicts.  (Both backends
+	currently have differing edgecase bugs with this option; see
+	BEHAVIORAL DIFFERENCES.)
 
 --whitespace=<option>::
 	This flag is passed to the 'git apply' program
@@ -609,9 +606,94 @@ There are some subtle differences how the backends behave.
 Directory rename detection
 ~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-Directory rename heuristics are enabled in the merge and interactive
-backends.  Due to the lack of accurate tree information, directory
-rename detection is disabled in the am backend.
+Due to the lack of accurate tree information (arising from
+constructing fake ancestors with the limited information available in
+patches), directory rename detection is disabled in the am backend.
+Disabled directory rename detection means that if one side of history
+renames a directory and the other adds new files to the old directory,
+then the new files will be left behind in the old directory without
+any warning at the time of rebasing that you may want to move these
+files into the new directory.
+
+Directory rename detection works with the merge and interactive
+backends to provide you warnings in such cases.
+
+Context
+~~~~~~~
+
+The am backend works by creating a sequence of patches (by calling
+`format-patch` internally), and then applying the patches in sequence
+(calling `am` internally).  Patches are composed of multiple hunks,
+each with line numbers, a context region, and the actual changes.  The
+line numbers have to be taken with some fuzz, since the other side
+will likely have inserted or deleted lines earlier in the file.  The
+context region is meant to help find how to adjust the line numbers in
+order to apply the changes to the right lines.  However, if multiple
+areas of the code have the same surrounding lines of context, the
+wrong one can be picked.  There are real-world cases where this has
+caused commits to be reapplied incorrectly with no conflicts reported.
+Setting diff.context to a larger value may prevent such types of
+problems, but increases the chance of spurious conflicts (since it
+will require more lines of matching context to apply).
+
+The interactive backend works with a full copy of each relevant file,
+insulating it from these types of problems.
+
+Labelling of conflicts markers
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+When there are content conflicts, the merge machinery tries to
+annotate each side's conflict markers with the commits where the
+content came from.  Since the am backend drops the original
+information about the rebased commits and their parents (and instead
+generates new fake commits based off limited information in the
+generated patches), those commits cannot be identified; instead it has
+to fall back to a commit summary.  Also, when merge.conflictStyle is
+set to diff3, the am backend will use "constructed merge base" to
+label the content from the merge base, and thus provide no information
+about the merge base commit whatsoever.
+
+The interactive backend works with the full commits on both sides of
+history and thus has no such limitations.
+
+--ignore-whitespace
+~~~~~~~~~~~~~~~~~~~
+
+The --ignore-whitespace option is supposed to ignore whitespace-only
+changes if it allows the code to merge cleanly.  Unfortunately, the
+different backends implement this differently, and both have different
+edge case bugs.
++
+'am' backend: When applying a patch, ignore changes in whitespace in
+context lines if necessary.  (Which implies that if the whitespace
+change was not in the context lines but on a line with a real change,
+then the rebase will still fail with "unnecessary" content conflicts.)
++
+'interactive' backend: Treat lines with only whitespace changes as
+unchanged for the sake of a three-way merge.  This means that if one
+side made no changes and the commits being rebased had whitespace-only
+changes, those whitespaces fixups will be discarded despite the fact
+that they present no content conflict.
+
+Miscellaneous differences
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+There are a few more behavioral differences that most folks would
+probably consider inconsequential but which are mentioned for
+completeness:
+
+* Reflog: The two backends will use different wording when describing
+  the changes made in the reflog, though both will make use of the
+  word "rebase".
+
+* Progress, informational, and error messages: The two backends
+  provide slightly different progress and informational messages.
+  Also, the am backend writes error messages (such as "Your files
+  would be overwritten...") to stdout, while the interactive backend
+  writes them to stderr.
+
+* State directories: The two backends keep their state in different
+  directories under .git/
 
 include::merge-strategies.txt[]
 
t/t3433-rebase-options-compatibility.sh +2 -3
index 5166f158dd..bd4d2d2f63 100755
@@ -10,9 +10,8 @@ test_description='tests to ensure compatibility between am and interactive backe
 GIT_AUTHOR_DATE="1999-04-02T08:03:20+05:30"
 export GIT_AUTHOR_DATE
 
-# This is a special case in which both am and interactive backends
-# provide the same output. It was done intentionally because
-# both the backends fall short of optimal behaviour.
+# This is a common case in which both am and interactive backends
+# provide the same output with --ignore-whitespace.
 test_expect_success 'setup' '
 	git checkout -b topic &&
 	q_to_tab >file <<-\EOF &&
Signature · 1 line
gitgitgadget
#33 lore raw

PATCH v2 5/15 rebase: fix handling of restrict_revision

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

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.

---
 builtin/rebase.c  |  4 ++--
 t/t3400-rebase.sh | 20 +++++++++++++++++++-
 2 files changed, 21 insertions(+), 3 deletions(-)
builtin/rebase.c +2 -2
index 5014c9a437..f1de5c8186 100644
@@ -364,8 +364,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,
t/t3400-rebase.sh +19 -1
index 79762b989a..71fd6396cd 100755
@@ -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 &&
Signature · 1 line
gitgitgadget
#35 lore raw

PATCH v2 7/15 rebase: allow more types of rebases to fast-forward

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

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.

---
 builtin/rebase.c               | 18 ++++++++++++++----
 t/t3432-rebase-fast-forward.sh |  2 ++
 2 files changed, 16 insertions(+), 4 deletions(-)
builtin/rebase.c +14 -4
index f1de5c8186..7027e34567 100644
@@ -1493,6 +1493,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"),
@@ -1804,11 +1805,18 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
 	    options.ignore_date)
 		options.flags |= REBASE_FORCE;
 
+	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, "--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++)))
@@ -2144,12 +2152,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)) {
t/t3432-rebase-fast-forward.sh +2 -0
index 7432c0e241..40388ccf9f 100755
@@ -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_ () {
Signature · 1 line
gitgitgadget
#34 lore raw

PATCH v2 6/15 t3432: make these tests work with either am or merge backends

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

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.

---
 t/t3432-rebase-fast-forward.sh | 48 ++++++++++++++++------------------
 1 file changed, 22 insertions(+), 26 deletions(-)
t/t3432-rebase-fast-forward.sh +22 -26
index 92f95b57da..7432c0e241 100755
@@ -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 <expect) &&
+			test_line_count '-gt' \$old .git/logs/HEAD
 		elif test $what = noop
 		then
-			test_i18ngrep 'is up to date' stdout &&
-			test_i18ngrep ! 'rebase forced' stdout
-		elif test $what = noop-force
-		then
-			test_i18ngrep 'is up to date, rebase forced' stdout
+			test_cmp expect .git/logs/HEAD
 		fi &&
 		newhead=\$(git rev-parse HEAD) &&
 		if test $cmp = same
@@ -71,14 +67,14 @@ test_rebase_same_head_ () {
 
 changes='no changes'
 test_rebase_same_head success noop same success work same
-test_rebase_same_head success noop same success noop-force same master
-test_rebase_same_head success noop same success noop-force diff --onto B B
-test_rebase_same_head success noop same success noop-force diff --onto B... B
-test_rebase_same_head success noop same success noop-force same --onto master... master
-test_rebase_same_head success noop same success noop-force same --keep-base master
-test_rebase_same_head success noop same success noop-force same --keep-base
-test_rebase_same_head success noop same success noop-force same --no-fork-point
-test_rebase_same_head success noop same success noop-force same --keep-base --no-fork-point
+test_rebase_same_head success noop same success work same master
+test_rebase_same_head success noop same success work diff --onto B B
+test_rebase_same_head success noop same success work diff --onto B... B
+test_rebase_same_head success noop same success work same --onto master... master
+test_rebase_same_head success noop same success work same --keep-base master
+test_rebase_same_head success noop same success work same --keep-base
+test_rebase_same_head success noop same success work same --no-fork-point
+test_rebase_same_head success noop same success work same --keep-base --no-fork-point
 test_rebase_same_head success noop same success work same --fork-point master
 test_rebase_same_head success noop same success work diff --fork-point --onto B B
 test_rebase_same_head success noop same success work diff --fork-point --onto B... B
@@ -91,14 +87,14 @@ test_expect_success 'add work same to side' '
 
 changes='our changes'
 test_rebase_same_head success noop same success work same
-test_rebase_same_head success noop same success noop-force same master
-test_rebase_same_head success noop same success noop-force diff --onto B B
-test_rebase_same_head success noop same success noop-force diff --onto B... B
-test_rebase_same_head success noop same success noop-force same --onto master... master
-test_rebase_same_head success noop same success noop-force same --keep-base master
-test_rebase_same_head success noop same success noop-force same --keep-base
-test_rebase_same_head success noop same success noop-force same --no-fork-point
-test_rebase_same_head success noop same success noop-force same --keep-base --no-fork-point
+test_rebase_same_head success noop same success work same master
+test_rebase_same_head success noop same success work diff --onto B B
+test_rebase_same_head success noop same success work diff --onto B... B
+test_rebase_same_head success noop same success work same --onto master... master
+test_rebase_same_head success noop same success work same --keep-base master
+test_rebase_same_head success noop same success work same --keep-base
+test_rebase_same_head success noop same success work same --no-fork-point
+test_rebase_same_head success noop same success work same --keep-base --no-fork-point
 test_rebase_same_head success noop same success work same --fork-point master
 test_rebase_same_head success noop same success work diff --fork-point --onto B B
 test_rebase_same_head success noop same success work diff --fork-point --onto B... B
@@ -112,8 +108,8 @@ test_expect_success 'add work same to upstream' '
 '
 
 changes='our and their changes'
-test_rebase_same_head success noop same success noop-force diff --onto B B
-test_rebase_same_head success noop same success noop-force diff --onto B... B
+test_rebase_same_head success noop same success work diff --onto B B
+test_rebase_same_head success noop same success work diff --onto B... B
 test_rebase_same_head success noop same success work diff --onto master... master
 test_rebase_same_head success noop same success work diff --keep-base master
 test_rebase_same_head success noop same success work diff --keep-base
Signature · 1 line
gitgitgadget
#43 lore raw

PATCH v2 14/15 rebase: make the backend configurable via config setting

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

---
 Documentation/config/rebase.txt |  8 ++++++++
 builtin/rebase.c                | 31 ++++++++++++++++++++++++-------
 2 files changed, 32 insertions(+), 7 deletions(-)
Documentation/config/rebase.txt +8 -0
index d98e32d812..e6ae30c999 100644
@@ -5,6 +5,14 @@ rebase.useBuiltin::
 	is always used. Setting this will emit a warning, to alert any
 	remaining users that setting this now does nothing.
 
+rebase.backend::
+	Default backend to use for rebasing.  Possible choices are
+	'am' or 'merge' (note that the merge backend is sometimes also
+	refered to as the interactive backend or the interactive
+	machinery elsewhere in the docs).  Also, in the future, if the
+	merge backend gains all remaining capabilities of the am
+	backend, this setting may become unused.
+
 rebase.stat::
 	Whether to show a diffstat of what changed upstream since the last
 	rebase. False by default.
builtin/rebase.c +24 -7
index b7915fc0cb..d602b2da4c 100644
@@ -60,6 +60,7 @@ enum empty_type {
 struct rebase_options {
 	enum rebase_type type;
 	enum empty_type empty;
+	const char *default_backend;
 	const char *state_dir;
 	struct commit *upstream;
 	const char *upstream_name;
@@ -103,6 +104,7 @@ struct rebase_options {
 #define REBASE_OPTIONS_INIT {			  	\
 		.type = REBASE_UNSPECIFIED,	  	\
 		.empty = EMPTY_UNSPECIFIED,	  	\
+		.default_backend = "am",	  	\
 		.flags = REBASE_NO_QUIET, 		\
 		.git_am_opts = ARGV_ARRAY_INIT,		\
 		.git_format_patch_opt = STRBUF_INIT	\
@@ -1298,6 +1300,10 @@ static int rebase_config(const char *var, const char *value, void *data)
 		return 0;
 	}
 
+	if (!strcmp(var, "rebase.backend")) {
+		return git_config_string(&opts->default_backend, var, value);
+	}
+
 	return git_default_config(var, value, data);
 }
 
@@ -1928,9 +1934,23 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
 			if (strcmp(options.git_am_opts.argv[i], "-q"))
 				break;
 
-		if (is_interactive(&options) && i >= 0)
-			die(_("cannot combine am options with either "
-			      "interactive or merge options"));
+		if (i >= 0) {
+			if (is_interactive(&options))
+				die(_("cannot combine am options with either "
+				      "interactive or merge options"));
+			else
+				options.type = REBASE_AM;
+		}
+	}
+
+	if (options.type == REBASE_UNSPECIFIED) {
+		if (!strcmp(options.default_backend, "merge"))
+			options.type = REBASE_MERGE;
+		else if (!strcmp(options.default_backend, "am"))
+			options.type = REBASE_AM;
+		else
+			die(_("Unknown rebase backend: %s"),
+			    options.default_backend);
 	}
 
 	switch (options.type) {
@@ -1943,10 +1963,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
 		options.state_dir = apply_dir();
 		break;
 	default:
-		/* the default rebase backend is `--am` */
-		options.type = REBASE_AM;
-		options.state_dir = apply_dir();
-		break;
+		BUG("options.type was just set above; should be unreachable.");
 	}
 
 	if (options.empty == EMPTY_UNSPECIFIED) {
Signature · 1 line
gitgitgadget
#42 lore raw

PATCH v2 13/15 rebase tests: repeat some tests using the merge backend instead of am

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

In order to ensure the merge/interactive backend gets similar coverage to the am one, add some tests for cases where previously only the am backend was tested.

---
 t/t5520-pull.sh                   | 17 +++++++++++++++--
 t/t6047-diff3-conflict-markers.sh | 13 +++++++++++--
 2 files changed, 26 insertions(+), 4 deletions(-)
t/t5520-pull.sh +15 -2
index 602d996a33..3fff6a06fa 100755
@@ -277,14 +277,27 @@ test_expect_success '--rebase' '
 	test_cmp expect actual
 '
 
-test_expect_success '--rebase fast forward' '
+test_expect_success '--rebase (merge) fast forward' '
 	git reset --hard before-rebase &&
 	git checkout -b ff &&
 	echo another modification >file &&
 	git commit -m third file &&
 
 	git checkout to-rebase &&
-	git pull --rebase . ff &&
+	git -c rebase.backend=merge pull --rebase . ff &&
+	test_cmp_rev HEAD ff &&
+
+	# The above only validates the result.  Did we actually bypass rebase?
+	git reflog -1 >reflog.actual &&
+	sed "s/^[0-9a-f][0-9a-f]*/OBJID/" reflog.actual >reflog.fuzzy &&
+	echo "OBJID HEAD@{0}: pull --rebase . ff: Fast-forward" >reflog.expected &&
+	test_cmp reflog.expected reflog.fuzzy
+'
+
+test_expect_success '--rebase (am) fast forward' '
+	git reset --hard before-rebase &&
+
+	git -c rebase.backend=am pull --rebase . ff &&
 	test_cmp_rev HEAD ff &&
 
 	# The above only validates the result.  Did we actually bypass rebase?
t/t6047-diff3-conflict-markers.sh +11 -2
index 860542aad0..d383ce8130 100755
@@ -186,7 +186,7 @@ test_expect_success 'check multiple merge bases' '
 	)
 '
 
-test_expect_success 'rebase describes fake ancestor base' '
+test_expect_success 'rebase --merge describes parent of commit being picked' '
 	test_create_repo rebase &&
 	(
 		cd rebase &&
@@ -194,7 +194,16 @@ test_expect_success 'rebase describes fake ancestor base' '
 		test_commit master file &&
 		git checkout -b side HEAD^ &&
 		test_commit side file &&
-		test_must_fail git -c merge.conflictstyle=diff3 rebase master &&
+		test_must_fail git -c merge.conflictstyle=diff3 rebase --merge master &&
+		grep "||||||| parent of" file
+	)
+'
+
+test_expect_success 'rebase --am describes fake ancestor base' '
+	(
+		cd rebase &&
+		git rebase --abort &&
+		test_must_fail git -c merge.conflictstyle=diff3 rebase --am master &&
 		grep "||||||| constructed merge base" file
 	)
 '
Signature · 1 line
gitgitgadget
#41 lore raw

PATCH v2 12/15 rebase tests: mark tests specific to the am-backend with --am

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

We have many rebase tests in the testsuite, and often the same test is repeated multiple times just testing different backends. For those tests that were specifically trying to test the am backend, add the --am flag.

---
 t/t3400-rebase.sh                       | 10 +++++-----
 t/t3401-rebase-and-am-rename.sh         |  4 ++--
 t/t3404-rebase-interactive.sh           |  2 +-
 t/t3406-rebase-message.sh               | 12 ++++++------
 t/t3407-rebase-abort.sh                 |  6 +++---
 t/t3420-rebase-autostash.sh             |  2 +-
 t/t3425-rebase-topology-merges.sh       |  8 ++++----
 t/t3432-rebase-fast-forward.sh          |  4 ++--
 t/t3433-rebase-options-compatibility.sh |  8 ++++----
 t/t5407-post-rewrite-hook.sh            | 12 ++++++------
 t/t7512-status-help.sh                  | 12 ++++++------
 11 files changed, 40 insertions(+), 40 deletions(-)
t/t3400-rebase.sh +5 -5
index 71fd6396cd..0a491f2363 100755
@@ -183,19 +183,19 @@ test_expect_success 'default to common base in @{upstream}s reflog if no upstrea
 	test_cmp expect actual
 '
 
-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 (--am)' '
 	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 rebase --am &&
 	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 &&
+	git rebase --am &&
 	git rev-parse --verify default-base >expect &&
 	git rev-parse default~1 >actual &&
 	test_cmp expect actual
@@ -226,7 +226,7 @@ test_expect_success 'cherry-picked commits and fork-point work together' '
 
 test_expect_success 'rebase --am -q is quiet' '
 	git checkout -b quiet topic &&
-	git rebase -q master >output.out 2>&1 &&
+	git rebase --am -q master >output.out 2>&1 &&
 	test_must_be_empty output.out
 '
 
@@ -325,7 +325,7 @@ test_expect_success 'rebase --am and --show-current-patch' '
 		echo two >>init.t &&
 		git commit -a -m two &&
 		git tag two &&
-		test_must_fail git rebase -f --onto init HEAD^ &&
+		test_must_fail git rebase --am -f --onto init HEAD^ &&
 		GIT_TRACE=1 git rebase --show-current-patch >/dev/null 2>stderr &&
 		grep "show.*$(git rev-parse two)" stderr
 	)
t/t3401-rebase-and-am-rename.sh +2 -2
index a0b9438b22..50803958fd 100755
@@ -52,13 +52,13 @@ test_expect_success 'rebase --interactive: directory rename detected' '
 	)
 '
 
-test_expect_failure 'rebase (am): directory rename detected' '
+test_expect_failure 'rebase --am: directory rename detected' '
 	(
 		cd dir-rename &&
 
 		git checkout B^0 &&
 
-		git -c merge.directoryRenames=true rebase A &&
+		git -c merge.directoryRenames=true rebase --am A &&
 
 		git ls-files -s >out &&
 		test_line_count = 5 out &&
t/t3404-rebase-interactive.sh +1 -1
index ae6e55ce79..743b7e511a 100755
@@ -1137,7 +1137,7 @@ test_expect_success C_LOCALE_OUTPUT 'rebase --edit-todo does not work on non-int
 	git checkout conflict-branch &&
 	(
 		set_fake_editor &&
-		test_must_fail git rebase -f --onto HEAD~2 HEAD~ &&
+		test_must_fail git rebase -f --am --onto HEAD~2 HEAD~ &&
 		test_must_fail git rebase --edit-todo
 	) &&
 	git rebase --abort
t/t3406-rebase-message.sh +6 -6
index 0c2c569f95..7ce617fc1f 100755
@@ -23,24 +23,24 @@ test_expect_success 'rebase -m' '
 '
 
 test_expect_success 'rebase against master twice' '
-	git rebase master >out &&
+	git rebase --am master >out &&
 	test_i18ngrep "Current branch topic is up to date" out
 '
 
 test_expect_success 'rebase against master twice with --force' '
-	git rebase --force-rebase master >out &&
+	git rebase --force-rebase --am master >out &&
 	test_i18ngrep "Current branch topic is up to date, rebase forced" out
 '
 
 test_expect_success 'rebase against master twice from another branch' '
 	git checkout topic^ &&
-	git rebase master topic >out &&
+	git rebase --am master topic >out &&
 	test_i18ngrep "Current branch topic is up to date" out
 '
 
 test_expect_success 'rebase fast-forward to master' '
 	git checkout topic^ &&
-	git rebase topic >out &&
+	git rebase --am topic >out &&
 	test_i18ngrep "Fast-forwarded HEAD to topic" out
 '
 
@@ -89,7 +89,7 @@ test_expect_success 'GIT_REFLOG_ACTION' '
 	git checkout -b reflog-topic start &&
 	test_commit reflog-to-rebase &&
 
-	git rebase reflog-onto &&
+	git rebase --am reflog-onto &&
 	git log -g --format=%gs -3 >actual &&
 	cat >expect <<-\EOF &&
 	rebase finished: returning to refs/heads/reflog-topic
@@ -99,7 +99,7 @@ test_expect_success 'GIT_REFLOG_ACTION' '
 	test_cmp expect actual &&
 
 	git checkout -b reflog-prefix reflog-to-rebase &&
-	GIT_REFLOG_ACTION=change-the-reflog git rebase reflog-onto &&
+	GIT_REFLOG_ACTION=change-the-reflog git rebase --am reflog-onto &&
 	git log -g --format=%gs -3 >actual &&
 	cat >expect <<-\EOF &&
 	rebase finished: returning to refs/heads/reflog-prefix
t/t3407-rebase-abort.sh +3 -3
index 910f218284..3e31826170 100755
@@ -96,14 +96,14 @@ testrebase() {
 	'
 }
 
-testrebase "" .git/rebase-apply
+testrebase " --am" .git/rebase-apply
 testrebase " --merge" .git/rebase-merge
 
-test_expect_success 'rebase --quit' '
+test_expect_success 'rebase --am --quit' '
 	cd "$work_dir" &&
 	# Clean up the state from the previous one
 	git reset --hard pre-rebase &&
-	test_must_fail git rebase master &&
+	test_must_fail git rebase --am master &&
 	test_path_is_dir .git/rebase-apply &&
 	head_before=$(git rev-parse HEAD) &&
 	git rebase --quit &&
t/t3420-rebase-autostash.sh +1 -1
index 5f7e73cf83..3816159e20 100755
@@ -234,7 +234,7 @@ test_expect_success "rebase: noop rebase" '
 	git checkout feature-branch
 '
 
-testrebase "" .git/rebase-apply
+testrebase " --am" .git/rebase-apply
 testrebase " --merge" .git/rebase-merge
 testrebase " --interactive" .git/rebase-merge
 
t/t3425-rebase-topology-merges.sh +4 -4
index fd8efe84fe..19700b025b 100755
@@ -54,7 +54,7 @@ test_run_rebase () {
 		test_linear_range 'n o' e..
 	"
 }
-test_run_rebase success ''
+test_run_rebase success --am
 test_run_rebase success -m
 test_run_rebase success -i
 
@@ -70,7 +70,7 @@ test_run_rebase () {
 		test_linear_range "\'"$expected"\'" d..
 	"
 }
-test_run_rebase success 'n o e' ''
+test_run_rebase success 'n o e' --am
 test_run_rebase success 'n o e' -m
 test_run_rebase success 'n o e' -i
 
@@ -86,7 +86,7 @@ test_run_rebase () {
 		test_linear_range "\'"$expected"\'" c..
 	"
 }
-test_run_rebase success 'd n o e' ''
+test_run_rebase success 'd n o e' --am
 test_run_rebase success 'd n o e' -m
 test_run_rebase success 'd n o e' -i
 
@@ -102,7 +102,7 @@ test_run_rebase () {
 		test_linear_range "\'"$expected"\'" c..
 	"
 }
-test_run_rebase success 'd n o e' ''
+test_run_rebase success 'd n o e' --am
 test_run_rebase success 'd n o e' -m
 test_run_rebase success 'd n o e' -i
 
t/t3432-rebase-fast-forward.sh +2 -2
index 40388ccf9f..4b3cecce56 100755
@@ -28,8 +28,8 @@ test_rebase_same_head () {
 	shift &&
 	cmp_f="$1" &&
 	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 " --am" "$*" &&
+	test_rebase_same_head_ $status_f $what_f $cmp_f " --am --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" "$*"
 }
t/t3433-rebase-options-compatibility.sh +4 -4
index bd4d2d2f63..a07e1f276b 100755
@@ -51,9 +51,9 @@ test_expect_success '--ignore-whitespace works with am backend' '
 	new line 2
 	line 3
 	EOF
-	test_must_fail git rebase main side &&
+	test_must_fail git rebase --am main side &&
 	git rebase --abort &&
-	git rebase --ignore-whitespace main side &&
+	git rebase --am --ignore-whitespace main side &&
 	test_cmp expect file
 '
 
@@ -71,7 +71,7 @@ test_expect_success '--ignore-whitespace works with interactive backend' '
 
 test_expect_success '--committer-date-is-author-date works with am backend' '
 	git commit --amend &&
-	git rebase --committer-date-is-author-date HEAD^ &&
+	git rebase --am --committer-date-is-author-date HEAD^ &&
 	git show HEAD --pretty="format:%ai" >authortime &&
 	git show HEAD --pretty="format:%ci" >committertime &&
 	test_cmp authortime committertime
@@ -103,7 +103,7 @@ test_expect_success '--committer-date-is-author-date works with rebase -r' '
 # sets to +0530.
 test_expect_success '--ignore-date works with am backend' '
 	git commit --amend --date="$GIT_AUTHOR_DATE" &&
-	git rebase --ignore-date HEAD^ &&
+	git rebase --am --ignore-date HEAD^ &&
 	git show HEAD --pretty="format:%ai" >authortime &&
 	grep "+0000" authortime
 '
t/t5407-post-rewrite-hook.sh +6 -6
index 7344253bfb..a8a73616e4 100755
@@ -53,10 +53,10 @@ test_expect_success 'git commit --amend --no-post-rewrite' '
 	test ! -f post-rewrite.data
 '
 
-test_expect_success 'git rebase' '
+test_expect_success 'git rebase --am' '
 	git reset --hard D &&
 	clear_hook_input &&
-	test_must_fail git rebase --onto A B &&
+	test_must_fail git rebase --am --onto A B &&
 	echo C > foo &&
 	git add foo &&
 	git rebase --continue &&
@@ -68,10 +68,10 @@ test_expect_success 'git rebase' '
 	verify_hook_input
 '
 
-test_expect_success 'git rebase --skip' '
+test_expect_success 'git rebase --am --skip' '
 	git reset --hard D &&
 	clear_hook_input &&
-	test_must_fail git rebase --onto A B &&
+	test_must_fail git rebase --am --onto A B &&
 	test_must_fail git rebase --skip &&
 	echo D > foo &&
 	git add foo &&
@@ -84,10 +84,10 @@ test_expect_success 'git rebase --skip' '
 	verify_hook_input
 '
 
-test_expect_success 'git rebase --skip the last one' '
+test_expect_success 'git rebase --am --skip the last one' '
 	git reset --hard F &&
 	clear_hook_input &&
-	test_must_fail git rebase --onto D A &&
+	test_must_fail git rebase --am --onto D A &&
 	git rebase --skip &&
 	echo rebase >expected.args &&
 	cat >expected.data <<-EOF &&
t/t7512-status-help.sh +6 -6
index 66d7a62797..d22b0acf2a 100755
@@ -71,10 +71,10 @@ test_expect_success 'prepare for rebase conflicts' '
 '
 
 
-test_expect_success 'status when rebase in progress before resolving conflicts' '
+test_expect_success 'status when rebase --am in progress before resolving conflicts' '
 	test_when_finished "git rebase --abort" &&
 	ONTO=$(git rev-parse --short HEAD^^) &&
-	test_must_fail git rebase HEAD^ --onto HEAD^^ &&
+	test_must_fail git rebase --am HEAD^ --onto HEAD^^ &&
 	cat >expected <<EOF &&
 rebase in progress; onto $ONTO
 You are currently rebasing branch '\''rebase_conflicts'\'' on '\''$ONTO'\''.
@@ -94,11 +94,11 @@ EOF
 '
 
 
-test_expect_success 'status when rebase in progress before rebase --continue' '
+test_expect_success 'status when rebase --am in progress before rebase --continue' '
 	git reset --hard rebase_conflicts &&
 	test_when_finished "git rebase --abort" &&
 	ONTO=$(git rev-parse --short HEAD^^) &&
-	test_must_fail git rebase HEAD^ --onto HEAD^^ &&
+	test_must_fail git rebase --am HEAD^ --onto HEAD^^ &&
 	echo three >main.txt &&
 	git add main.txt &&
 	cat >expected <<EOF &&
@@ -688,7 +688,7 @@ EOF
 '
 
 
-test_expect_success 'status when rebase conflicts with statushints disabled' '
+test_expect_success 'status when rebase --am conflicts with statushints disabled' '
 	git reset --hard master &&
 	git checkout -b statushints_disabled &&
 	test_when_finished "git config --local advice.statushints true" &&
@@ -698,7 +698,7 @@ test_expect_success 'status when rebase conflicts with statushints disabled' '
 	test_commit three_statushints main.txt three &&
 	test_when_finished "git rebase --abort" &&
 	ONTO=$(git rev-parse --short HEAD^^) &&
-	test_must_fail git rebase HEAD^ --onto HEAD^^ &&
+	test_must_fail git rebase --am HEAD^ --onto HEAD^^ &&
 	cat >expected <<EOF &&
 rebase in progress; onto $ONTO
 You are currently rebasing branch '\''statushints_disabled'\'' on '\''$ONTO'\''.
Signature · 1 line
gitgitgadget
#37 lore raw

PATCH v2 9/15 rebase: move incompatibility checks between backend options a bit earlier

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

---
 builtin/rebase.c | 22 +++++++++++-----------
 1 file changed, 11 insertions(+), 11 deletions(-)
builtin/rebase.c +11 -11
index 7027e34567..d2b99e9908 100644
@@ -1906,6 +1906,17 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
 	if (isatty(2) && options.flags & REBASE_NO_QUIET)
 		strbuf_addstr(&options.git_format_patch_opt, " --progress");
 
+	if (options.git_am_opts.argc) {
+		/* all am options except -q are compatible only with --am */
+		for (i = options.git_am_opts.argc - 1; i >= 0; i--)
+			if (strcmp(options.git_am_opts.argv[i], "-q"))
+				break;
+
+		if (is_interactive(&options) && i >= 0)
+			die(_("cannot combine am options with either "
+			      "interactive or merge options"));
+	}
+
 	switch (options.type) {
 	case REBASE_MERGE:
 	case REBASE_INTERACTIVE:
@@ -1936,17 +1947,6 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
 	if (reschedule_failed_exec >= 0)
 		options.reschedule_failed_exec = reschedule_failed_exec;
 
-	if (options.git_am_opts.argc) {
-		/* all am options except -q are compatible only with --am */
-		for (i = options.git_am_opts.argc - 1; i >= 0; i--)
-			if (strcmp(options.git_am_opts.argv[i], "-q"))
-				break;
-
-		if (is_interactive(&options) && i >= 0)
-			die(_("cannot combine am options with either "
-			      "interactive or merge options"));
-	}
-
 	if (options.signoff) {
 		if (options.type == REBASE_PRESERVE_MERGES)
 			die("cannot combine '--signoff' with "
Signature · 1 line
gitgitgadget
#44 lore raw

PATCH v2 15/15 rebase: change the default backend from "am" to "merge"

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

The am-backend drops information and thus limits what we can do:

* lack of full tree information from the original commits means we cannot do directory rename detection and warn users that they might want to move some of their new files that they placed in old directories to prevent their becoming orphaned.[1]
* reduction in context from only having a few lines beyond those changed means that when context lines are non-unique we can apply patches incorrectly.[2]
* lack of access to original commits means that conflict marker annotation has less information available.

Also, the merge/interactive backend have far more abilities, appear to currently have a slight performance advantage[3] and have room for more optimizations than the am backend[4] (and work is underway to take advantage of some of those possibilities).

[1] https://lore.kernel.org/git/[email protected]/
[2] https://lore.kernel.org/git/CABPp-BGiu2nVMQY_t-rnFR5GQUz_ipyEE8oDocKeO+h+t4Mn4A@mail.gmail.com/
[3] https://public-inbox.org/git/CABPp-BF=ev03WgODk6TMQmuNoatg2kiEe5DR__gJ0OTVqHSnfQ@mail.gmail.com/
[4] https://lore.kernel.org/git/CABPp-BGh7yW69QwxQb13K0HM38NKmQif3A6C6UULEKYnkEJ5vA@mail.gmail.com/

---
 Documentation/git-rebase.txt           |  2 +-
 builtin/rebase.c                       |  4 ++--
 t/t5520-pull.sh                        | 10 ++++++----
 t/t9106-git-svn-commit-diff-clobber.sh |  3 ++-
 4 files changed, 11 insertions(+), 8 deletions(-)
Documentation/git-rebase.txt +1 -1
index cf1ac2e359..e819889a31 100644
@@ -309,7 +309,7 @@ See also INCOMPATIBLE OPTIONS below.
 --merge::
 	Use merging strategies to rebase.  When the recursive (default) merge
 	strategy is used, this allows rebase to be aware of renames on the
-	upstream side.
+	upstream side.  This is the default.
 +
 Note that a rebase merge works by replaying each commit from the working
 branch on top of the <upstream> branch.  Because of this, when a merge
builtin/rebase.c +2 -2
index d602b2da4c..938a8840b1 100644
@@ -104,7 +104,7 @@ struct rebase_options {
 #define REBASE_OPTIONS_INIT {			  	\
 		.type = REBASE_UNSPECIFIED,	  	\
 		.empty = EMPTY_UNSPECIFIED,	  	\
-		.default_backend = "am",	  	\
+		.default_backend = "merge",	  	\
 		.flags = REBASE_NO_QUIET, 		\
 		.git_am_opts = ARGV_ARRAY_INIT,		\
 		.git_format_patch_opt = STRBUF_INIT	\
@@ -1945,7 +1945,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
 
 	if (options.type == REBASE_UNSPECIFIED) {
 		if (!strcmp(options.default_backend, "merge"))
-			options.type = REBASE_MERGE;
+			imply_interactive(&options, "--merge");
 		else if (!strcmp(options.default_backend, "am"))
 			options.type = REBASE_AM;
 		else
t/t5520-pull.sh +6 -4
index 3fff6a06fa..4f9e7f7ff6 100755
@@ -340,7 +340,7 @@ test_expect_success '--rebase with conflicts shows advice' '
 	test_tick &&
 	git commit -m "Create conflict" seq.txt &&
 	test_must_fail git pull --rebase . seq 2>err >out &&
-	test_i18ngrep "Resolve all conflicts manually" out
+	test_i18ngrep "Resolve all conflicts manually" err
 '
 
 test_expect_success 'failed --rebase shows advice' '
@@ -354,7 +354,7 @@ test_expect_success 'failed --rebase shows advice' '
 	git checkout -f -b fails-to-rebase HEAD^ &&
 	test_commit v2-without-cr file "2" file2-lf &&
 	test_must_fail git pull --rebase . diverging 2>err >out &&
-	test_i18ngrep "Resolve all conflicts manually" out
+	test_i18ngrep "Resolve all conflicts manually" err
 '
 
 test_expect_success '--rebase fails with multiple branches' '
@@ -774,8 +774,10 @@ test_expect_success 'git pull --rebase does not reapply old patches' '
 	(
 		cd dst &&
 		test_must_fail git pull --rebase &&
-		find .git/rebase-apply -name "000*" >patches &&
-		test_line_count = 1 patches
+		cat .git/rebase-merge/done .git/rebase-merge/git-rebase-todo >work &&
+		grep -v -e \# -e ^$ work >patches &&
+		test_line_count = 1 patches &&
+		rm -f work
 	)
 '
 
t/t9106-git-svn-commit-diff-clobber.sh +2 -1
index dbe8deac0d..aec45bca3b 100755
@@ -92,7 +92,8 @@ test_expect_success 'multiple dcommit from git svn will not clobber svn' "
 
 
 test_expect_success 'check that rebase really failed' '
-	test -d .git/rebase-apply
+	git status >output &&
+	grep currently.rebasing output
 '
 
 test_expect_success 'resolve, continue the rebase and dcommit' "
Signature · 1 line
gitgitgadget
#38 lore raw

PATCH v2 10/15 rebase: add an --am option

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

Currently, this option doesn't do anything except error out if any options requiring the interactive-backend are also passed. However, when we make the default backend configurable later in this series, this flag will provide a way to override the config setting.

---
 Documentation/git-rebase.txt | 11 ++++++++++-
 builtin/rebase.c             | 18 +++++++++++++++++-
 2 files changed, 27 insertions(+), 2 deletions(-)
Documentation/git-rebase.txt +10 -1
index f1ace07c38..cf1ac2e359 100644
@@ -258,6 +258,13 @@ See also INCOMPATIBLE OPTIONS below.
 	original branch. The index and working tree are also left
 	unchanged as a result.
 
+--am:
+	Use git-am internally to rebase.  This option may become a
+	no-op in the future once the interactive backend handles
+	everything the am one does.
++
+See also INCOMPATIBLE OPTIONS below.
+
 --empty={drop,keep,ask}::
 	How to handle commits that become empty (because they contain a
 	subset of already upstream changes) or start empty.  With drop
@@ -372,7 +379,7 @@ See also INCOMPATIBLE OPTIONS below.
 	Ensure at least <n> lines of surrounding context match before
 	and after each change.  When fewer lines of surrounding
 	context exist they all must match.  By default no context is
-	ever ignored.
+	ever ignored.  Implies --am.
 +
 See also INCOMPATIBLE OPTIONS below.
 
@@ -417,6 +424,7 @@ with `--keep-base` in order to drop those commits from your branch.
 --whitespace=<option>::
 	This flag is passed to the 'git apply' program
 	(see linkgit:git-apply[1]) that applies the patch.
+	Implies --am.
 +
 See also INCOMPATIBLE OPTIONS below.
 
@@ -567,6 +575,7 @@ INCOMPATIBLE OPTIONS
 
 The following options:
 
+ * --am
  * --whitespace
  * -C
 
builtin/rebase.c +17 -1
index d2b99e9908..b7915fc0cb 100644
@@ -1361,6 +1361,18 @@ static int can_fast_forward(struct commit *onto, struct commit *upstream,
 	return res && is_linear_history(onto, head);
 }
 
+static int parse_opt_am(const struct option *opt, const char *arg, int unset)
+{
+	struct rebase_options *opts = opt->value;
+
+	BUG_ON_OPT_NEG(unset);
+	BUG_ON_OPT_ARG(arg);
+
+	opts->type = REBASE_AM;
+
+	return 0;
+}
+
 /* -i followed by -m is still -i */
 static int parse_opt_merge(const struct option *opt, const char *arg, int unset)
 {
@@ -1546,6 +1558,10 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
 		OPT_CMDMODE(0, "show-current-patch", &action,
 			    N_("show the patch file being applied or merged"),
 			    ACTION_SHOW_CURRENT_PATCH),
+		{ OPTION_CALLBACK, 0, "am", &options, NULL,
+			N_("use apply-mail strategies to rebase"),
+			PARSE_OPT_NOARG | PARSE_OPT_NONEG,
+			parse_opt_am },
 		{ OPTION_CALLBACK, 'm', "merge", &options, NULL,
 			N_("use merging strategies to rebase"),
 			PARSE_OPT_NOARG | PARSE_OPT_NONEG,
@@ -1906,7 +1922,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
 	if (isatty(2) && options.flags & REBASE_NO_QUIET)
 		strbuf_addstr(&options.git_format_patch_opt, " --progress");
 
-	if (options.git_am_opts.argc) {
+	if (options.git_am_opts.argc || options.type == REBASE_AM) {
 		/* all am options except -q are compatible only with --am */
 		for (i = options.git_am_opts.argc - 1; i >= 0; i--)
 			if (strcmp(options.git_am_opts.argv[i], "-q"))
Signature · 1 line
gitgitgadget
#39 lore raw

PATCH v2 11/15 contrib: change the prompt for interactive-based rebases

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

In the past, we had different prompts for different types of rebases:
REBASE: for am-based rebases
REBASE-m: for merge-based rebases
REBASE-i: for interactive-based rebases

It's not clear why this distinction was necessary or helpful; when the prompt was added in commit e75201963f67 ("Improve bash prompt to detect various states like an unfinished merge", 2007-09-30), it simply added these three different types. Perhaps there was a useful purpose back then, but there have been some changes:

* The merge backend was deleted after being implemented on top of the interactive backend, causing the prompt for merge-based rebases to change from REBASE-m to REBASE-i.
* The interactive backend is used for multiple different types of non-interactive rebases, so the "-i" part of the prompt doesn't really mean what it used to.
* Rebase backends have gained more abilities and have a great deal of overlap, sometimes making it hard to distinguish them.
* Behavioral differences between the backends have also been ironed out.
* We want to change the default backend from am to interactive, which means people would get "REBASE-i" by default if we didn't change the prompt, and only if they specified --am or --whitespace or -C would they get the "REBASE" prompt.
* In the future, we plan to have "--whitespace", "-C", and even "--am" run the interactive backend once it can handle everything the am-backend can.

For all these reasons, make the prompt for any type of rebase just be "REBASE".

---
 contrib/completion/git-prompt.sh | 4 ++--
 t/t9903-bash-prompt.sh           | 8 ++++----
 2 files changed, 6 insertions(+), 6 deletions(-)
contrib/completion/git-prompt.sh +2 -2
index 1d510cd47b..8f8a22ba60 100644
@@ -430,9 +430,9 @@ __git_ps1 ()
 		__git_eread "$g/rebase-merge/msgnum" step
 		__git_eread "$g/rebase-merge/end" total
 		if [ -f "$g/rebase-merge/interactive" ]; then
-			r="|REBASE-i"
+			r="|REBASE"
 		else
-			r="|REBASE-m"
+			r="|REBASE"
 		fi
 	else
 		if [ -d "$g/rebase-apply" ]; then
t/t9903-bash-prompt.sh +4 -4
index 88bc733ad6..7ca35d358d 100755
@@ -163,7 +163,7 @@ test_expect_success 'prompt - inside bare repository' '
 '
 
 test_expect_success 'prompt - interactive rebase' '
-	printf " (b1|REBASE-i 2/3)" >expected &&
+	printf " (b1|REBASE 2/3)" >expected &&
 	write_script fake_editor.sh <<-\EOF &&
 		echo "exec echo" >"$1"
 		echo "edit $(git log -1 --format="%h")" >>"$1"
@@ -180,7 +180,7 @@ test_expect_success 'prompt - interactive rebase' '
 '
 
 test_expect_success 'prompt - rebase merge' '
-	printf " (b2|REBASE-i 1/3)" >expected &&
+	printf " (b2|REBASE 1/3)" >expected &&
 	git checkout b2 &&
 	test_when_finished "git checkout master" &&
 	test_must_fail git rebase --merge b1 b2 &&
@@ -189,11 +189,11 @@ test_expect_success 'prompt - rebase merge' '
 	test_cmp expected "$actual"
 '
 
-test_expect_success 'prompt - rebase' '
+test_expect_success 'prompt - rebase am' '
 	printf " (b2|REBASE 1/3)" >expected &&
 	git checkout b2 &&
 	test_when_finished "git checkout master" &&
-	test_must_fail git rebase b1 b2 &&
+	test_must_fail git rebase --am b1 b2 &&
 	test_when_finished "git rebase --abort" &&
 	__git_ps1 >"$actual" &&
 	test_cmp expected "$actual"
Signature · 1 line
gitgitgadget
#40 lore raw

Re: [PATCH v2 11/15] contrib: change the prompt for interactive-based rebases

Hi Elijah,

Quoted 1 line

Subject: contrib: change the prompt for interactive-based rebases

I'll also echo Gábor's comments and suggest that you use "git-prompt" for the change area.

Quoted 45 lines · Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

In the past, we had different prompts for different types of rebases:
REBASE: for am-based rebases
REBASE-m: for merge-based rebases
REBASE-i: for interactive-based rebases

It's not clear why this distinction was necessary or helpful; when the prompt was added in commit e75201963f67 ("Improve bash prompt to detect various states like an unfinished merge", 2007-09-30), it simply added these three different types. Perhaps there was a useful purpose back then, but there have been some changes:

* The merge backend was deleted after being implemented on top of the interactive backend, causing the prompt for merge-based rebases to change from REBASE-m to REBASE-i.
* The interactive backend is used for multiple different types of non-interactive rebases, so the "-i" part of the prompt doesn't really mean what it used to.
* Rebase backends have gained more abilities and have a great deal of overlap, sometimes making it hard to distinguish them.
* Behavioral differences between the backends have also been ironed out.
* We want to change the default backend from am to interactive, which means people would get "REBASE-i" by default if we didn't change the prompt, and only if they specified --am or --whitespace or -C would they get the "REBASE" prompt.
* In the future, we plan to have "--whitespace", "-C", and even "--am" run the interactive backend once it can handle everything the am-backend can.

For all these reasons, make the prompt for any type of rebase just be "REBASE".

---
 contrib/completion/git-prompt.sh | 4 ++--
 t/t9903-bash-prompt.sh           | 8 ++++----
 2 files changed, 6 insertions(+), 6 deletions(-)
contrib/completion/git-prompt.sh +0 -0
index 1d510cd47b..8f8a22ba60 100644

contrib/completion/git-prompt.sh __git_ps1 () · line 435

Quoted 8 lines
contrib/completion/git-prompt.sh +2 -2
@@ -430,9 +430,9 @@ __git_ps1 ()
 		__git_eread "$g/rebase-merge/msgnum" step
 		__git_eread "$g/rebase-merge/end" total
 		if [ -f "$g/rebase-merge/interactive" ]; then
-			r="|REBASE-i"
+			r="|REBASE"
 		else
-			r="|REBASE-m"
+			r="|REBASE"

We should just drop the if here since both arms are the same..

Thanks,

Denton

Quoted 42 lines
contrib/completion/git-prompt.sh +0 -0
 		fi
 	else
 		if [ -d "$g/rebase-apply" ]; then
t/t9903-bash-prompt.sh +4 -4
index 88bc733ad6..7ca35d358d 100755
@@ -163,7 +163,7 @@ test_expect_success 'prompt - inside bare repository' '
 '
 
 test_expect_success 'prompt - interactive rebase' '
-	printf " (b1|REBASE-i 2/3)" >expected &&
+	printf " (b1|REBASE 2/3)" >expected &&
 	write_script fake_editor.sh <<-\EOF &&
 		echo "exec echo" >"$1"
 		echo "edit $(git log -1 --format="%h")" >>"$1"
@@ -180,7 +180,7 @@ test_expect_success 'prompt - interactive rebase' '
 '
 
 test_expect_success 'prompt - rebase merge' '
-	printf " (b2|REBASE-i 1/3)" >expected &&
+	printf " (b2|REBASE 1/3)" >expected &&
 	git checkout b2 &&
 	test_when_finished "git checkout master" &&
 	test_must_fail git rebase --merge b1 b2 &&
@@ -189,11 +189,11 @@ test_expect_success 'prompt - rebase merge' '
 	test_cmp expected "$actual"
 '
 
-test_expect_success 'prompt - rebase' '
+test_expect_success 'prompt - rebase am' '
 	printf " (b2|REBASE 1/3)" >expected &&
 	git checkout b2 &&
 	test_when_finished "git checkout master" &&
-	test_must_fail git rebase b1 b2 &&
+	test_must_fail git rebase --am b1 b2 &&
 	test_when_finished "git rebase --abort" &&
 	__git_ps1 >"$actual" &&
 	test_cmp expected "$actual"
Signature · 1 line
gitgitgadget
#45 lore raw

v3 cover rebase: make the default backend configurable

In reply to Elijah Newren via GitGitGadget

This series does a lot of work around making the default rebase backend configurable, and switching the default from the am backend to the merge/interactive one.

See the cover letter for v1[*] for a more detailed motivation for the series, the type of changes being made, and areas I'd like to reviewers to focus on.

Changes since v2:

* Tweaked patch 11 (commit message, and condensing if-else block that was same on both sides), as suggested by Denton

[*]
https://lore.kernel.org/git/[email protected]/

Elijah Newren (15):
rebase: extend the options for handling of empty commits
t3406: simplify an already simple test
rebase, sequencer: remove the broken GIT_QUIET handling
rebase: make sure to pass along the quiet flag to the sequencer
rebase: fix handling of restrict_revision
t3432: make these tests work with either am or merge backends
rebase: allow more types of rebases to fast-forward git-rebase.txt: add more details about behavioral differences of
backends
rebase: move incompatibility checks between backend options a bit
earlier
rebase: add an --am option
git-prompt: change the prompt for interactive-based rebases rebase tests: mark tests specific to the am-backend with --am rebase tests: repeat some tests using the merge backend instead of am
rebase: make the backend configurable via config setting
rebase: change the default backend from "am" to "merge"

 Documentation/config/rebase.txt         |   8 ++
 Documentation/git-rebase.txt            | 150 ++++++++++++++++----
 builtin/rebase.c                        | 177 +++++++++++++++++++-----
 contrib/completion/git-prompt.sh        |   6 +-
 rebase-interactive.c                    |   4 +-
 rebase-interactive.h                    |   2 +-
 sequencer.c                             |  80 ++++++++---
 sequencer.h                             |   6 +-
 t/t3400-rebase.sh                       |  36 ++++-
 t/t3401-rebase-and-am-rename.sh         |   4 +-
 t/t3404-rebase-interactive.sh           |   2 +-
 t/t3406-rebase-message.sh               |  19 ++-
 t/t3407-rebase-abort.sh                 |   6 +-
 t/t3420-rebase-autostash.sh             |   2 +-
 t/t3421-rebase-topology-linear.sh       |   4 +-
 t/t3424-rebase-empty.sh                 |  89 ++++++++++++
 t/t3425-rebase-topology-merges.sh       |   8 +-
 t/t3427-rebase-subtree.sh               |  16 ++-
 t/t3432-rebase-fast-forward.sh          |  54 ++++----
 t/t3433-rebase-options-compatibility.sh |  13 +-
 t/t5407-post-rewrite-hook.sh            |  12 +-
 t/t5520-pull.sh                         |  27 +++-
 t/t6047-diff3-conflict-markers.sh       |  13 +-
 t/t7512-status-help.sh                  |  12 +-
 t/t9106-git-svn-commit-diff-clobber.sh  |   3 +-
 t/t9903-bash-prompt.sh                  |   8 +-
 26 files changed, 576 insertions(+), 185 deletions(-)
 create mode 100755 t/t3424-rebase-empty.sh

base-commit: 12029dc57db23baef008e77db1909367599210ee
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-679%2Fnewren%2Frebase-fixes-v3
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-679/newren/rebase-fixes-v3
Pull-Request: https://github.com/git/git/pull/679

Range-diff vs v2:

  1:  1c2b77e94d =  1:  1c2b77e94d rebase: extend the options for handling of empty commits
  2:  bd3c5ec155 =  2:  bd3c5ec155 t3406: simplify an already simple test
  3:  49388b79fd =  3:  49388b79fd rebase, sequencer: remove the broken GIT_QUIET handling
  4:  478479358f =  4:  478479358f rebase: make sure to pass along the quiet flag to the sequencer
  5:  ee26f5a161 =  5:  ee26f5a161 rebase: fix handling of restrict_revision
  6:  34a69def33 =  6:  34a69def33 t3432: make these tests work with either am or merge backends
  7:  f2c92853b4 =  7:  f2c92853b4 rebase: allow more types of rebases to fast-forward
  8:  b307340f7c =  8:  b307340f7c git-rebase.txt: add more details about behavioral differences of backends
  9:  7c3f2e07f3 =  9:  7c3f2e07f3 rebase: move incompatibility checks between backend options a bit earlier
 10:  1df11f0b51 = 10:  1df11f0b51 rebase: add an --am option
 11:  94b5a3051d ! 11:  ff43593211 contrib: change the prompt for interactive-based rebases
     @@ -1,6 +1,6 @@
      Author: Elijah Newren <[email protected]>
      
     -    contrib: change the prompt for interactive-based rebases
     +    git-prompt: change the prompt for interactive-based rebases
      
          In the past, we had different prompts for different types of rebases:
             REBASE: for am-based rebases
     @@ -40,17 +40,18 @@
       --- a/contrib/completion/git-prompt.sh
       +++ b/contrib/completion/git-prompt.sh
      @@
     + 		__git_eread "$g/rebase-merge/head-name" b
       		__git_eread "$g/rebase-merge/msgnum" step
       		__git_eread "$g/rebase-merge/end" total
     - 		if [ -f "$g/rebase-merge/interactive" ]; then
     +-		if [ -f "$g/rebase-merge/interactive" ]; then
      -			r="|REBASE-i"
     -+			r="|REBASE"
     - 		else
     +-		else
      -			r="|REBASE-m"
     -+			r="|REBASE"
     - 		fi
     +-		fi
     ++		r="|REBASE"
       	else
       		if [ -d "$g/rebase-apply" ]; then
     + 			__git_eread "$g/rebase-apply/next" step
      
       diff --git a/t/t9903-bash-prompt.sh b/t/t9903-bash-prompt.sh
       --- a/t/t9903-bash-prompt.sh
 12:  c905d288bf = 12:  99388f24e5 rebase tests: mark tests specific to the am-backend with --am
 13:  0287881361 = 13:  c2ba6317bf rebase tests: repeat some tests using the merge backend instead of am
 14:  ec782e711c = 14:  8bec6df51a rebase: make the backend configurable via config setting
 15:  7adcbc0bc5 = 15:  044853fd61 rebase: change the default backend from "am" to "merge"
Signature · 1 line
gitgitgadget
#46 lore raw

PATCH v3 1/15 rebase: extend the options for handling of empty commits

From: Elijah Newren <[email protected]>

Extend the interactive machinery with the ability to handle the full spread of options for how to handle commits that either start or become empty (by "become empty" I mean the changes in a commit are a subset of changes that exist upstream, so the net effect of applying the commit is no changes). Introduce a new command line flag for selecting the desired behavior:

    --empty={drop,keep,ask}

with the definitions:
drop: drop empty commits
keep: keep empty commits
ask: provide the user a chance to interact and pick what to do with
empty commits on a case-by-case basis

Note that traditionally, am-based rebases have always dropped commits that either started or became empty, while interactive-based rebases have defaulted to ask (and provided an option to keep commits that started empty). This difference made sense since users of an am-based rebase just wanted to quickly batch apply a sequence of commits, while users editing a todo list will likely want the chance to interact and handle unusual cases on a case-by-case basis. However, not all rebases using the interactive machinery are explicitly interactive anymore. In particular --merge was always meant to behave more like --am: just rebase a batch of commits without popping up a todo list.

If the --empty flag is not specified, pick defaults as follows:
explicitly interactive: ask
--exec: keep (exec is about checking existing commits, and often
used without actually changing the base. Thus the expectation is that the user doesn't necessarily want anything to change; they just want to test).
otherwise: drop

Also, this commit makes --keep-empty just imply --empty=keep, and hides it from help so that we aren't confusing users with different ways to do the same thing. (I could have added a --drop-empty flag, but then that invites users to specify both --keep-empty and --drop-empty and we have to add sanity checking around that; it seems cleaner to have a single multi-valued option.) This actually fixes --keep-empty too; previously, it only meant to sometimes keep empty commits, in particular commits which started empty would be kept. But it would still error out and ask the user what to do with commits that became empty. Now it keeps empty commits, as instructed.

---
 Documentation/git-rebase.txt      | 35 ++++++------
 builtin/rebase.c                  | 83 +++++++++++++++++++++++++---
 rebase-interactive.c              |  4 +-
 rebase-interactive.h              |  2 +-
 sequencer.c                       | 74 +++++++++++++++++++------
 sequencer.h                       |  6 ++-
 t/t3421-rebase-topology-linear.sh |  4 +-
 t/t3424-rebase-empty.sh           | 89 +++++++++++++++++++++++++++++++
 t/t3427-rebase-subtree.sh         | 16 +++---
 9 files changed, 263 insertions(+), 50 deletions(-)
 create mode 100755 t/t3424-rebase-empty.sh
Documentation/git-rebase.txt +20 -15
index 1d0e2d27cc..ff32ca1080 100644
@@ -258,9 +258,25 @@ 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 become empty (because they contain a
+	subset of already upstream changes) or start empty.  With drop
+	(the default), commits that start or become empty are dropped.
+	With keep (implied by --exec), 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 or
+	commit it.  Also with ask, if the rebase is interactive then
+	commits which start empty will be commented out in the todo
+	action list (giving you a chance to uncomment).
++
+Note that this has no effect on commits which are already upstream (as
+can be checked via `git log --cherry-mark ...`), which are always
+dropped by rebase.
++
+See also INCOMPATIBLE OPTIONS below.
+
 --keep-empty::
-	Keep the commits that do not change anything from its
-	parents in the result.
+	Deprecated alias for what is now known as --empty=keep.
 +
 See also INCOMPATIBLE OPTIONS below.
 
@@ -569,6 +585,7 @@ are incompatible with the following options:
  * --interactive
  * --exec
  * --keep-empty
+ * --empty=
  * --edit-todo
  * --root when used in combination with --onto
 
@@ -580,6 +597,7 @@ In addition, the following pairs of options are incompatible:
  * --preserve-merges and --ignore-whitespace
  * --preserve-merges and --committer-date-is-author-date
  * --preserve-merges and --ignore-date
+ * --preserve-merges and --empty=
  * --keep-base and --onto
  * --keep-base and --root
 
@@ -588,19 +606,6 @@ BEHAVIORAL DIFFERENCES
 
 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 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.
-
 Directory rename detection
 ~~~~~~~~~~~~~~~~~~~~~~~~~~
 
builtin/rebase.c +75 -8

157 lines not on this page · raw message

rebase-interactive.c +2 -2
index aa18ae82b7..ad82bf77df 100644
@@ -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(unsigned no_ask_empty, int command_count,
 		      const char *shortrevisions, const char *shortonto,
 		      struct strbuf *buf)
 {
@@ -81,7 +81,7 @@ void append_todo_help(unsigned keep_empty, int command_count,
 
 	strbuf_add_commented_lines(buf, msg, strlen(msg));
 
-	if (!keep_empty) {
+	if (!no_ask_empty) {
 		msg = _("Note that empty commits are commented out");
 		strbuf_add_commented_lines(buf, msg, strlen(msg));
 	}
rebase-interactive.h +1 -1
index 44dbb06311..f531e00ba7 100644
@@ -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(unsigned no_ask_empty, int command_count,
 		      const char *shortrevisions, const char *shortonto,
 		      struct strbuf *buf);
 int edit_todo_list(struct repository *r, struct todo_list *todo_list,
sequencer.c +59 -15

167 lines not on this page · raw message

sequencer.h +5 -1
index e9a0e03ea2..1c3abb661c 100644
@@ -39,7 +39,9 @@ struct replay_opts {
 	int allow_rerere_auto;
 	int allow_empty;
 	int allow_empty_message;
+	int drop_redundant_commits;
 	int keep_redundant_commits;
+	int ask_on_initially_empty;
 	int verbose;
 	int quiet;
 	int reschedule_failed_exec;
@@ -134,7 +136,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)
@@ -150,6 +152,8 @@ int sequencer_remove_state(struct replay_opts *opts);
  * `--onto`, we do not want to re-generate the root commits.
  */
 #define TODO_LIST_ROOT_WITH_ONTO (1U << 6)
+#define TODO_LIST_DROP_EMPTY (1U << 7)
+#define TODO_LIST_ASK_EMPTY (1U << 8)
 
 
 int sequencer_make_script(struct repository *r, struct strbuf *out, int argc,
t/t3421-rebase-topology-linear.sh +2 -2
index 325072b0a3..d23e0bf778 100755
@@ -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
t/t3424-rebase-empty.sh new file +89 -0

89 lines not on this page · raw message

t/t3427-rebase-subtree.sh +10 -6
index bec48e6a1f..468ebc1bef 100755
@@ -85,23 +85,27 @@ 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 --empty=ask --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 --empty=ask --onto files-master master &&
 	: first pick results in no changes &&
-	git rebase --continue &&
+	test_must_fail git rebase --skip &&
+	: last pick was an empty commit that has no changes, but we want to keep it &&
+	git commit --allow-empty &&
 	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 --empty=ask --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 --empty=ask --rebase-merges --onto files-master --root &&
 	: first pick results in no changes &&
-	git rebase --continue &&
+	test_must_fail git rebase --skip &&
+	: last pick was an empty commit that has no changes, but we want to keep it &&
+	git commit --allow-empty &&
 	verbose test "$(commit_message HEAD~2)" = "master4" &&
 	verbose test "$(commit_message HEAD~)" = "files_subtree/master5" &&
 	verbose test "$(commit_message HEAD)" = "Empty commit"
Signature · 1 line
gitgitgadget
#51 lore raw

PATCH v3 2/15 t3406: simplify an already simple test

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

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.

---
 t/t3406-rebase-message.sh | 7 ++-----
 1 file changed, 2 insertions(+), 5 deletions(-)
t/t3406-rebase-message.sh +2 -5
index b393e1e9fe..0c2c569f95 100755
@@ -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' '
Signature · 1 line
gitgitgadget
#52 lore raw

PATCH v3 3/15 rebase, sequencer: remove the broken GIT_QUIET handling

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

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.

---
 builtin/rebase.c | 6 ++----
 sequencer.c      | 6 ++----
 2 files changed, 4 insertions(+), 8 deletions(-)
builtin/rebase.c +2 -4
index 6903249307..32026a62e8 100644
@@ -718,8 +718,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)
@@ -1178,8 +1178,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);
sequencer.c +2 -4
index d2c11f34b7..71062212a5 100644
@@ -2688,8 +2688,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)
@@ -2698,8 +2696,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)
Signature · 1 line
gitgitgadget
#53 lore raw

PATCH v3 4/15 rebase: make sure to pass along the quiet flag to the sequencer

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

---
 builtin/rebase.c  | 3 ++-
 t/t3400-rebase.sh | 8 +++++++-
 2 files changed, 9 insertions(+), 2 deletions(-)
builtin/rebase.c +2 -1
index 32026a62e8..5014c9a437 100644
@@ -126,6 +126,7 @@ static struct replay_opts get_replay_opts(const struct rebase_options *opts)
 	replay.keep_redundant_commits = (opts->empty == EMPTY_KEEP);
 	replay.ask_on_initially_empty = (opts->empty == EMPTY_ASK &&
 					 !(opts->flags & REBASE_INTERACTIVE_EXPLICIT));
+	replay.quiet = !(opts->flags & REBASE_NO_QUIET);
 	replay.verbose = opts->flags & REBASE_VERBOSE;
 	replay.reschedule_failed_exec = opts->reschedule_failed_exec;
 	replay.committer_date_is_author_date =
@@ -1502,7 +1503,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),
t/t3400-rebase.sh +7 -1
index 221b35f2df..79762b989a 100755
@@ -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" &&
Signature · 1 line
gitgitgadget
#54 lore raw

PATCH v3 5/15 rebase: fix handling of restrict_revision

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

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.

---
 builtin/rebase.c  |  4 ++--
 t/t3400-rebase.sh | 20 +++++++++++++++++++-
 2 files changed, 21 insertions(+), 3 deletions(-)
builtin/rebase.c +2 -2
index 5014c9a437..f1de5c8186 100644
@@ -364,8 +364,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,
t/t3400-rebase.sh +19 -1
index 79762b989a..71fd6396cd 100755
@@ -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 &&
Signature · 1 line
gitgitgadget
#55 lore raw

PATCH v3 6/15 t3432: make these tests work with either am or merge backends

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

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.

---
 t/t3432-rebase-fast-forward.sh | 48 ++++++++++++++++------------------
 1 file changed, 22 insertions(+), 26 deletions(-)
t/t3432-rebase-fast-forward.sh +22 -26
index 92f95b57da..7432c0e241 100755
@@ -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 <expect) &&
+			test_line_count '-gt' \$old .git/logs/HEAD
 		elif test $what = noop
 		then
-			test_i18ngrep 'is up to date' stdout &&
-			test_i18ngrep ! 'rebase forced' stdout
-		elif test $what = noop-force
-		then
-			test_i18ngrep 'is up to date, rebase forced' stdout
+			test_cmp expect .git/logs/HEAD
 		fi &&
 		newhead=\$(git rev-parse HEAD) &&
 		if test $cmp = same
@@ -71,14 +67,14 @@ test_rebase_same_head_ () {
 
 changes='no changes'
 test_rebase_same_head success noop same success work same
-test_rebase_same_head success noop same success noop-force same master
-test_rebase_same_head success noop same success noop-force diff --onto B B
-test_rebase_same_head success noop same success noop-force diff --onto B... B
-test_rebase_same_head success noop same success noop-force same --onto master... master
-test_rebase_same_head success noop same success noop-force same --keep-base master
-test_rebase_same_head success noop same success noop-force same --keep-base
-test_rebase_same_head success noop same success noop-force same --no-fork-point
-test_rebase_same_head success noop same success noop-force same --keep-base --no-fork-point
+test_rebase_same_head success noop same success work same master
+test_rebase_same_head success noop same success work diff --onto B B
+test_rebase_same_head success noop same success work diff --onto B... B
+test_rebase_same_head success noop same success work same --onto master... master
+test_rebase_same_head success noop same success work same --keep-base master
+test_rebase_same_head success noop same success work same --keep-base
+test_rebase_same_head success noop same success work same --no-fork-point
+test_rebase_same_head success noop same success work same --keep-base --no-fork-point
 test_rebase_same_head success noop same success work same --fork-point master
 test_rebase_same_head success noop same success work diff --fork-point --onto B B
 test_rebase_same_head success noop same success work diff --fork-point --onto B... B
@@ -91,14 +87,14 @@ test_expect_success 'add work same to side' '
 
 changes='our changes'
 test_rebase_same_head success noop same success work same
-test_rebase_same_head success noop same success noop-force same master
-test_rebase_same_head success noop same success noop-force diff --onto B B
-test_rebase_same_head success noop same success noop-force diff --onto B... B
-test_rebase_same_head success noop same success noop-force same --onto master... master
-test_rebase_same_head success noop same success noop-force same --keep-base master
-test_rebase_same_head success noop same success noop-force same --keep-base
-test_rebase_same_head success noop same success noop-force same --no-fork-point
-test_rebase_same_head success noop same success noop-force same --keep-base --no-fork-point
+test_rebase_same_head success noop same success work same master
+test_rebase_same_head success noop same success work diff --onto B B
+test_rebase_same_head success noop same success work diff --onto B... B
+test_rebase_same_head success noop same success work same --onto master... master
+test_rebase_same_head success noop same success work same --keep-base master
+test_rebase_same_head success noop same success work same --keep-base
+test_rebase_same_head success noop same success work same --no-fork-point
+test_rebase_same_head success noop same success work same --keep-base --no-fork-point
 test_rebase_same_head success noop same success work same --fork-point master
 test_rebase_same_head success noop same success work diff --fork-point --onto B B
 test_rebase_same_head success noop same success work diff --fork-point --onto B... B
@@ -112,8 +108,8 @@ test_expect_success 'add work same to upstream' '
 '
 
 changes='our and their changes'
-test_rebase_same_head success noop same success noop-force diff --onto B B
-test_rebase_same_head success noop same success noop-force diff --onto B... B
+test_rebase_same_head success noop same success work diff --onto B B
+test_rebase_same_head success noop same success work diff --onto B... B
 test_rebase_same_head success noop same success work diff --onto master... master
 test_rebase_same_head success noop same success work diff --keep-base master
 test_rebase_same_head success noop same success work diff --keep-base
Signature · 1 line
gitgitgadget
#56 lore raw

PATCH v3 7/15 rebase: allow more types of rebases to fast-forward

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

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.

---
 builtin/rebase.c               | 18 ++++++++++++++----
 t/t3432-rebase-fast-forward.sh |  2 ++
 2 files changed, 16 insertions(+), 4 deletions(-)
builtin/rebase.c +14 -4
index f1de5c8186..7027e34567 100644
@@ -1493,6 +1493,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"),
@@ -1804,11 +1805,18 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
 	    options.ignore_date)
 		options.flags |= REBASE_FORCE;
 
+	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, "--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++)))
@@ -2144,12 +2152,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)) {
t/t3432-rebase-fast-forward.sh +2 -0
index 7432c0e241..40388ccf9f 100755
@@ -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_ () {
Signature · 1 line
gitgitgadget
#57 lore raw

PATCH v3 8/15 git-rebase.txt: add more details about behavioral differences of backends

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

---
 Documentation/git-rebase.txt            | 102 +++++++++++++++++++++---
 t/t3433-rebase-options-compatibility.sh |   5 +-
 2 files changed, 94 insertions(+), 13 deletions(-)
Documentation/git-rebase.txt +92 -10
index ff32ca1080..f1ace07c38 100644
@@ -409,13 +409,10 @@ your branch contains commits which were dropped, this option can be used
 with `--keep-base` in order to drop those commits from your branch.
 
 --ignore-whitespace::
-	Behaves differently depending on which backend is selected.
-+
-'am' backend: When applying a patch, ignore changes in whitespace in
-context lines if necessary.
-+
-'interactive' backend: Treat lines with only whitespace changes as
-unchanged for the sake of a three-way merge.
+	Ignore whitespace-only changes in the commits being rebased,
+	which may avoid "unnecessary" conflicts.  (Both backends
+	currently have differing edgecase bugs with this option; see
+	BEHAVIORAL DIFFERENCES.)
 
 --whitespace=<option>::
 	This flag is passed to the 'git apply' program
@@ -609,9 +606,94 @@ There are some subtle differences how the backends behave.
 Directory rename detection
 ~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-Directory rename heuristics are enabled in the merge and interactive
-backends.  Due to the lack of accurate tree information, directory
-rename detection is disabled in the am backend.
+Due to the lack of accurate tree information (arising from
+constructing fake ancestors with the limited information available in
+patches), directory rename detection is disabled in the am backend.
+Disabled directory rename detection means that if one side of history
+renames a directory and the other adds new files to the old directory,
+then the new files will be left behind in the old directory without
+any warning at the time of rebasing that you may want to move these
+files into the new directory.
+
+Directory rename detection works with the merge and interactive
+backends to provide you warnings in such cases.
+
+Context
+~~~~~~~
+
+The am backend works by creating a sequence of patches (by calling
+`format-patch` internally), and then applying the patches in sequence
+(calling `am` internally).  Patches are composed of multiple hunks,
+each with line numbers, a context region, and the actual changes.  The
+line numbers have to be taken with some fuzz, since the other side
+will likely have inserted or deleted lines earlier in the file.  The
+context region is meant to help find how to adjust the line numbers in
+order to apply the changes to the right lines.  However, if multiple
+areas of the code have the same surrounding lines of context, the
+wrong one can be picked.  There are real-world cases where this has
+caused commits to be reapplied incorrectly with no conflicts reported.
+Setting diff.context to a larger value may prevent such types of
+problems, but increases the chance of spurious conflicts (since it
+will require more lines of matching context to apply).
+
+The interactive backend works with a full copy of each relevant file,
+insulating it from these types of problems.
+
+Labelling of conflicts markers
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+When there are content conflicts, the merge machinery tries to
+annotate each side's conflict markers with the commits where the
+content came from.  Since the am backend drops the original
+information about the rebased commits and their parents (and instead
+generates new fake commits based off limited information in the
+generated patches), those commits cannot be identified; instead it has
+to fall back to a commit summary.  Also, when merge.conflictStyle is
+set to diff3, the am backend will use "constructed merge base" to
+label the content from the merge base, and thus provide no information
+about the merge base commit whatsoever.
+
+The interactive backend works with the full commits on both sides of
+history and thus has no such limitations.
+
+--ignore-whitespace
+~~~~~~~~~~~~~~~~~~~
+
+The --ignore-whitespace option is supposed to ignore whitespace-only
+changes if it allows the code to merge cleanly.  Unfortunately, the
+different backends implement this differently, and both have different
+edge case bugs.
++
+'am' backend: When applying a patch, ignore changes in whitespace in
+context lines if necessary.  (Which implies that if the whitespace
+change was not in the context lines but on a line with a real change,
+then the rebase will still fail with "unnecessary" content conflicts.)
++
+'interactive' backend: Treat lines with only whitespace changes as
+unchanged for the sake of a three-way merge.  This means that if one
+side made no changes and the commits being rebased had whitespace-only
+changes, those whitespaces fixups will be discarded despite the fact
+that they present no content conflict.
+
+Miscellaneous differences
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+There are a few more behavioral differences that most folks would
+probably consider inconsequential but which are mentioned for
+completeness:
+
+* Reflog: The two backends will use different wording when describing
+  the changes made in the reflog, though both will make use of the
+  word "rebase".
+
+* Progress, informational, and error messages: The two backends
+  provide slightly different progress and informational messages.
+  Also, the am backend writes error messages (such as "Your files
+  would be overwritten...") to stdout, while the interactive backend
+  writes them to stderr.
+
+* State directories: The two backends keep their state in different
+  directories under .git/
 
 include::merge-strategies.txt[]
 
t/t3433-rebase-options-compatibility.sh +2 -3
index 5166f158dd..bd4d2d2f63 100755
@@ -10,9 +10,8 @@ test_description='tests to ensure compatibility between am and interactive backe
 GIT_AUTHOR_DATE="1999-04-02T08:03:20+05:30"
 export GIT_AUTHOR_DATE
 
-# This is a special case in which both am and interactive backends
-# provide the same output. It was done intentionally because
-# both the backends fall short of optimal behaviour.
+# This is a common case in which both am and interactive backends
+# provide the same output with --ignore-whitespace.
 test_expect_success 'setup' '
 	git checkout -b topic &&
 	q_to_tab >file <<-\EOF &&
Signature · 1 line
gitgitgadget
#66 lore raw

PATCH v3 11/15 git-prompt: change the prompt for interactive-based rebases

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

In the past, we had different prompts for different types of rebases:
REBASE: for am-based rebases
REBASE-m: for merge-based rebases
REBASE-i: for interactive-based rebases

It's not clear why this distinction was necessary or helpful; when the prompt was added in commit e75201963f67 ("Improve bash prompt to detect various states like an unfinished merge", 2007-09-30), it simply added these three different types. Perhaps there was a useful purpose back then, but there have been some changes:

* The merge backend was deleted after being implemented on top of the interactive backend, causing the prompt for merge-based rebases to change from REBASE-m to REBASE-i.
* The interactive backend is used for multiple different types of non-interactive rebases, so the "-i" part of the prompt doesn't really mean what it used to.
* Rebase backends have gained more abilities and have a great deal of overlap, sometimes making it hard to distinguish them.
* Behavioral differences between the backends have also been ironed out.
* We want to change the default backend from am to interactive, which means people would get "REBASE-i" by default if we didn't change the prompt, and only if they specified --am or --whitespace or -C would they get the "REBASE" prompt.
* In the future, we plan to have "--whitespace", "-C", and even "--am" run the interactive backend once it can handle everything the am-backend can.

For all these reasons, make the prompt for any type of rebase just be "REBASE".

---
 contrib/completion/git-prompt.sh | 6 +-----
 t/t9903-bash-prompt.sh           | 8 ++++----
 2 files changed, 5 insertions(+), 9 deletions(-)
contrib/completion/git-prompt.sh +1 -5
index 1d510cd47b..014cd7c3cf 100644
@@ -429,11 +429,7 @@ __git_ps1 ()
 		__git_eread "$g/rebase-merge/head-name" b
 		__git_eread "$g/rebase-merge/msgnum" step
 		__git_eread "$g/rebase-merge/end" total
-		if [ -f "$g/rebase-merge/interactive" ]; then
-			r="|REBASE-i"
-		else
-			r="|REBASE-m"
-		fi
+		r="|REBASE"
 	else
 		if [ -d "$g/rebase-apply" ]; then
 			__git_eread "$g/rebase-apply/next" step
t/t9903-bash-prompt.sh +4 -4
index 88bc733ad6..7ca35d358d 100755
@@ -163,7 +163,7 @@ test_expect_success 'prompt - inside bare repository' '
 '
 
 test_expect_success 'prompt - interactive rebase' '
-	printf " (b1|REBASE-i 2/3)" >expected &&
+	printf " (b1|REBASE 2/3)" >expected &&
 	write_script fake_editor.sh <<-\EOF &&
 		echo "exec echo" >"$1"
 		echo "edit $(git log -1 --format="%h")" >>"$1"
@@ -180,7 +180,7 @@ test_expect_success 'prompt - interactive rebase' '
 '
 
 test_expect_success 'prompt - rebase merge' '
-	printf " (b2|REBASE-i 1/3)" >expected &&
+	printf " (b2|REBASE 1/3)" >expected &&
 	git checkout b2 &&
 	test_when_finished "git checkout master" &&
 	test_must_fail git rebase --merge b1 b2 &&
@@ -189,11 +189,11 @@ test_expect_success 'prompt - rebase merge' '
 	test_cmp expected "$actual"
 '
 
-test_expect_success 'prompt - rebase' '
+test_expect_success 'prompt - rebase am' '
 	printf " (b2|REBASE 1/3)" >expected &&
 	git checkout b2 &&
 	test_when_finished "git checkout master" &&
-	test_must_fail git rebase b1 b2 &&
+	test_must_fail git rebase --am b1 b2 &&
 	test_when_finished "git rebase --abort" &&
 	__git_ps1 >"$actual" &&
 	test_cmp expected "$actual"
Signature · 1 line
gitgitgadget
#59 lore raw

PATCH v3 10/15 rebase: add an --am option

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

Currently, this option doesn't do anything except error out if any options requiring the interactive-backend are also passed. However, when we make the default backend configurable later in this series, this flag will provide a way to override the config setting.

---
 Documentation/git-rebase.txt | 11 ++++++++++-
 builtin/rebase.c             | 18 +++++++++++++++++-
 2 files changed, 27 insertions(+), 2 deletions(-)
Documentation/git-rebase.txt +10 -1
index f1ace07c38..cf1ac2e359 100644
@@ -258,6 +258,13 @@ See also INCOMPATIBLE OPTIONS below.
 	original branch. The index and working tree are also left
 	unchanged as a result.
 
+--am:
+	Use git-am internally to rebase.  This option may become a
+	no-op in the future once the interactive backend handles
+	everything the am one does.
++
+See also INCOMPATIBLE OPTIONS below.
+
 --empty={drop,keep,ask}::
 	How to handle commits that become empty (because they contain a
 	subset of already upstream changes) or start empty.  With drop
@@ -372,7 +379,7 @@ See also INCOMPATIBLE OPTIONS below.
 	Ensure at least <n> lines of surrounding context match before
 	and after each change.  When fewer lines of surrounding
 	context exist they all must match.  By default no context is
-	ever ignored.
+	ever ignored.  Implies --am.
 +
 See also INCOMPATIBLE OPTIONS below.
 
@@ -417,6 +424,7 @@ with `--keep-base` in order to drop those commits from your branch.
 --whitespace=<option>::
 	This flag is passed to the 'git apply' program
 	(see linkgit:git-apply[1]) that applies the patch.
+	Implies --am.
 +
 See also INCOMPATIBLE OPTIONS below.
 
@@ -567,6 +575,7 @@ INCOMPATIBLE OPTIONS
 
 The following options:
 
+ * --am
  * --whitespace
  * -C
 
builtin/rebase.c +17 -1
index d2b99e9908..b7915fc0cb 100644
@@ -1361,6 +1361,18 @@ static int can_fast_forward(struct commit *onto, struct commit *upstream,
 	return res && is_linear_history(onto, head);
 }
 
+static int parse_opt_am(const struct option *opt, const char *arg, int unset)
+{
+	struct rebase_options *opts = opt->value;
+
+	BUG_ON_OPT_NEG(unset);
+	BUG_ON_OPT_ARG(arg);
+
+	opts->type = REBASE_AM;
+
+	return 0;
+}
+
 /* -i followed by -m is still -i */
 static int parse_opt_merge(const struct option *opt, const char *arg, int unset)
 {
@@ -1546,6 +1558,10 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
 		OPT_CMDMODE(0, "show-current-patch", &action,
 			    N_("show the patch file being applied or merged"),
 			    ACTION_SHOW_CURRENT_PATCH),
+		{ OPTION_CALLBACK, 0, "am", &options, NULL,
+			N_("use apply-mail strategies to rebase"),
+			PARSE_OPT_NOARG | PARSE_OPT_NONEG,
+			parse_opt_am },
 		{ OPTION_CALLBACK, 'm', "merge", &options, NULL,
 			N_("use merging strategies to rebase"),
 			PARSE_OPT_NOARG | PARSE_OPT_NONEG,
@@ -1906,7 +1922,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
 	if (isatty(2) && options.flags & REBASE_NO_QUIET)
 		strbuf_addstr(&options.git_format_patch_opt, " --progress");
 
-	if (options.git_am_opts.argc) {
+	if (options.git_am_opts.argc || options.type == REBASE_AM) {
 		/* all am options except -q are compatible only with --am */
 		for (i = options.git_am_opts.argc - 1; i >= 0; i--)
 			if (strcmp(options.git_am_opts.argv[i], "-q"))
Signature · 1 line
gitgitgadget
#67 lore raw

PATCH v3 12/15 rebase tests: mark tests specific to the am-backend with --am

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

We have many rebase tests in the testsuite, and often the same test is repeated multiple times just testing different backends. For those tests that were specifically trying to test the am backend, add the --am flag.

---
 t/t3400-rebase.sh                       | 10 +++++-----
 t/t3401-rebase-and-am-rename.sh         |  4 ++--
 t/t3404-rebase-interactive.sh           |  2 +-
 t/t3406-rebase-message.sh               | 12 ++++++------
 t/t3407-rebase-abort.sh                 |  6 +++---
 t/t3420-rebase-autostash.sh             |  2 +-
 t/t3425-rebase-topology-merges.sh       |  8 ++++----
 t/t3432-rebase-fast-forward.sh          |  4 ++--
 t/t3433-rebase-options-compatibility.sh |  8 ++++----
 t/t5407-post-rewrite-hook.sh            | 12 ++++++------
 t/t7512-status-help.sh                  | 12 ++++++------
 11 files changed, 40 insertions(+), 40 deletions(-)
t/t3400-rebase.sh +5 -5
index 71fd6396cd..0a491f2363 100755
@@ -183,19 +183,19 @@ test_expect_success 'default to common base in @{upstream}s reflog if no upstrea
 	test_cmp expect actual
 '
 
-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 (--am)' '
 	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 rebase --am &&
 	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 &&
+	git rebase --am &&
 	git rev-parse --verify default-base >expect &&
 	git rev-parse default~1 >actual &&
 	test_cmp expect actual
@@ -226,7 +226,7 @@ test_expect_success 'cherry-picked commits and fork-point work together' '
 
 test_expect_success 'rebase --am -q is quiet' '
 	git checkout -b quiet topic &&
-	git rebase -q master >output.out 2>&1 &&
+	git rebase --am -q master >output.out 2>&1 &&
 	test_must_be_empty output.out
 '
 
@@ -325,7 +325,7 @@ test_expect_success 'rebase --am and --show-current-patch' '
 		echo two >>init.t &&
 		git commit -a -m two &&
 		git tag two &&
-		test_must_fail git rebase -f --onto init HEAD^ &&
+		test_must_fail git rebase --am -f --onto init HEAD^ &&
 		GIT_TRACE=1 git rebase --show-current-patch >/dev/null 2>stderr &&
 		grep "show.*$(git rev-parse two)" stderr
 	)
t/t3401-rebase-and-am-rename.sh +2 -2
index a0b9438b22..50803958fd 100755
@@ -52,13 +52,13 @@ test_expect_success 'rebase --interactive: directory rename detected' '
 	)
 '
 
-test_expect_failure 'rebase (am): directory rename detected' '
+test_expect_failure 'rebase --am: directory rename detected' '
 	(
 		cd dir-rename &&
 
 		git checkout B^0 &&
 
-		git -c merge.directoryRenames=true rebase A &&
+		git -c merge.directoryRenames=true rebase --am A &&
 
 		git ls-files -s >out &&
 		test_line_count = 5 out &&
t/t3404-rebase-interactive.sh +1 -1
index ae6e55ce79..743b7e511a 100755
@@ -1137,7 +1137,7 @@ test_expect_success C_LOCALE_OUTPUT 'rebase --edit-todo does not work on non-int
 	git checkout conflict-branch &&
 	(
 		set_fake_editor &&
-		test_must_fail git rebase -f --onto HEAD~2 HEAD~ &&
+		test_must_fail git rebase -f --am --onto HEAD~2 HEAD~ &&
 		test_must_fail git rebase --edit-todo
 	) &&
 	git rebase --abort
t/t3406-rebase-message.sh +6 -6
index 0c2c569f95..7ce617fc1f 100755
@@ -23,24 +23,24 @@ test_expect_success 'rebase -m' '
 '
 
 test_expect_success 'rebase against master twice' '
-	git rebase master >out &&
+	git rebase --am master >out &&
 	test_i18ngrep "Current branch topic is up to date" out
 '
 
 test_expect_success 'rebase against master twice with --force' '
-	git rebase --force-rebase master >out &&
+	git rebase --force-rebase --am master >out &&
 	test_i18ngrep "Current branch topic is up to date, rebase forced" out
 '
 
 test_expect_success 'rebase against master twice from another branch' '
 	git checkout topic^ &&
-	git rebase master topic >out &&
+	git rebase --am master topic >out &&
 	test_i18ngrep "Current branch topic is up to date" out
 '
 
 test_expect_success 'rebase fast-forward to master' '
 	git checkout topic^ &&
-	git rebase topic >out &&
+	git rebase --am topic >out &&
 	test_i18ngrep "Fast-forwarded HEAD to topic" out
 '
 
@@ -89,7 +89,7 @@ test_expect_success 'GIT_REFLOG_ACTION' '
 	git checkout -b reflog-topic start &&
 	test_commit reflog-to-rebase &&
 
-	git rebase reflog-onto &&
+	git rebase --am reflog-onto &&
 	git log -g --format=%gs -3 >actual &&
 	cat >expect <<-\EOF &&
 	rebase finished: returning to refs/heads/reflog-topic
@@ -99,7 +99,7 @@ test_expect_success 'GIT_REFLOG_ACTION' '
 	test_cmp expect actual &&
 
 	git checkout -b reflog-prefix reflog-to-rebase &&
-	GIT_REFLOG_ACTION=change-the-reflog git rebase reflog-onto &&
+	GIT_REFLOG_ACTION=change-the-reflog git rebase --am reflog-onto &&
 	git log -g --format=%gs -3 >actual &&
 	cat >expect <<-\EOF &&
 	rebase finished: returning to refs/heads/reflog-prefix
t/t3407-rebase-abort.sh +3 -3
index 910f218284..3e31826170 100755
@@ -96,14 +96,14 @@ testrebase() {
 	'
 }
 
-testrebase "" .git/rebase-apply
+testrebase " --am" .git/rebase-apply
 testrebase " --merge" .git/rebase-merge
 
-test_expect_success 'rebase --quit' '
+test_expect_success 'rebase --am --quit' '
 	cd "$work_dir" &&
 	# Clean up the state from the previous one
 	git reset --hard pre-rebase &&
-	test_must_fail git rebase master &&
+	test_must_fail git rebase --am master &&
 	test_path_is_dir .git/rebase-apply &&
 	head_before=$(git rev-parse HEAD) &&
 	git rebase --quit &&
t/t3420-rebase-autostash.sh +1 -1
index 5f7e73cf83..3816159e20 100755
@@ -234,7 +234,7 @@ test_expect_success "rebase: noop rebase" '
 	git checkout feature-branch
 '
 
-testrebase "" .git/rebase-apply
+testrebase " --am" .git/rebase-apply
 testrebase " --merge" .git/rebase-merge
 testrebase " --interactive" .git/rebase-merge
 
t/t3425-rebase-topology-merges.sh +4 -4
index fd8efe84fe..19700b025b 100755
@@ -54,7 +54,7 @@ test_run_rebase () {
 		test_linear_range 'n o' e..
 	"
 }
-test_run_rebase success ''
+test_run_rebase success --am
 test_run_rebase success -m
 test_run_rebase success -i
 
@@ -70,7 +70,7 @@ test_run_rebase () {
 		test_linear_range "\'"$expected"\'" d..
 	"
 }
-test_run_rebase success 'n o e' ''
+test_run_rebase success 'n o e' --am
 test_run_rebase success 'n o e' -m
 test_run_rebase success 'n o e' -i
 
@@ -86,7 +86,7 @@ test_run_rebase () {
 		test_linear_range "\'"$expected"\'" c..
 	"
 }
-test_run_rebase success 'd n o e' ''
+test_run_rebase success 'd n o e' --am
 test_run_rebase success 'd n o e' -m
 test_run_rebase success 'd n o e' -i
 
@@ -102,7 +102,7 @@ test_run_rebase () {
 		test_linear_range "\'"$expected"\'" c..
 	"
 }
-test_run_rebase success 'd n o e' ''
+test_run_rebase success 'd n o e' --am
 test_run_rebase success 'd n o e' -m
 test_run_rebase success 'd n o e' -i
 
t/t3432-rebase-fast-forward.sh +2 -2
index 40388ccf9f..4b3cecce56 100755
@@ -28,8 +28,8 @@ test_rebase_same_head () {
 	shift &&
 	cmp_f="$1" &&
 	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 " --am" "$*" &&
+	test_rebase_same_head_ $status_f $what_f $cmp_f " --am --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" "$*"
 }
t/t3433-rebase-options-compatibility.sh +4 -4
index bd4d2d2f63..a07e1f276b 100755
@@ -51,9 +51,9 @@ test_expect_success '--ignore-whitespace works with am backend' '
 	new line 2
 	line 3
 	EOF
-	test_must_fail git rebase main side &&
+	test_must_fail git rebase --am main side &&
 	git rebase --abort &&
-	git rebase --ignore-whitespace main side &&
+	git rebase --am --ignore-whitespace main side &&
 	test_cmp expect file
 '
 
@@ -71,7 +71,7 @@ test_expect_success '--ignore-whitespace works with interactive backend' '
 
 test_expect_success '--committer-date-is-author-date works with am backend' '
 	git commit --amend &&
-	git rebase --committer-date-is-author-date HEAD^ &&
+	git rebase --am --committer-date-is-author-date HEAD^ &&
 	git show HEAD --pretty="format:%ai" >authortime &&
 	git show HEAD --pretty="format:%ci" >committertime &&
 	test_cmp authortime committertime
@@ -103,7 +103,7 @@ test_expect_success '--committer-date-is-author-date works with rebase -r' '
 # sets to +0530.
 test_expect_success '--ignore-date works with am backend' '
 	git commit --amend --date="$GIT_AUTHOR_DATE" &&
-	git rebase --ignore-date HEAD^ &&
+	git rebase --am --ignore-date HEAD^ &&
 	git show HEAD --pretty="format:%ai" >authortime &&
 	grep "+0000" authortime
 '
t/t5407-post-rewrite-hook.sh +6 -6
index 7344253bfb..a8a73616e4 100755
@@ -53,10 +53,10 @@ test_expect_success 'git commit --amend --no-post-rewrite' '
 	test ! -f post-rewrite.data
 '
 
-test_expect_success 'git rebase' '
+test_expect_success 'git rebase --am' '
 	git reset --hard D &&
 	clear_hook_input &&
-	test_must_fail git rebase --onto A B &&
+	test_must_fail git rebase --am --onto A B &&
 	echo C > foo &&
 	git add foo &&
 	git rebase --continue &&
@@ -68,10 +68,10 @@ test_expect_success 'git rebase' '
 	verify_hook_input
 '
 
-test_expect_success 'git rebase --skip' '
+test_expect_success 'git rebase --am --skip' '
 	git reset --hard D &&
 	clear_hook_input &&
-	test_must_fail git rebase --onto A B &&
+	test_must_fail git rebase --am --onto A B &&
 	test_must_fail git rebase --skip &&
 	echo D > foo &&
 	git add foo &&
@@ -84,10 +84,10 @@ test_expect_success 'git rebase --skip' '
 	verify_hook_input
 '
 
-test_expect_success 'git rebase --skip the last one' '
+test_expect_success 'git rebase --am --skip the last one' '
 	git reset --hard F &&
 	clear_hook_input &&
-	test_must_fail git rebase --onto D A &&
+	test_must_fail git rebase --am --onto D A &&
 	git rebase --skip &&
 	echo rebase >expected.args &&
 	cat >expected.data <<-EOF &&
t/t7512-status-help.sh +6 -6
index 66d7a62797..d22b0acf2a 100755
@@ -71,10 +71,10 @@ test_expect_success 'prepare for rebase conflicts' '
 '
 
 
-test_expect_success 'status when rebase in progress before resolving conflicts' '
+test_expect_success 'status when rebase --am in progress before resolving conflicts' '
 	test_when_finished "git rebase --abort" &&
 	ONTO=$(git rev-parse --short HEAD^^) &&
-	test_must_fail git rebase HEAD^ --onto HEAD^^ &&
+	test_must_fail git rebase --am HEAD^ --onto HEAD^^ &&
 	cat >expected <<EOF &&
 rebase in progress; onto $ONTO
 You are currently rebasing branch '\''rebase_conflicts'\'' on '\''$ONTO'\''.
@@ -94,11 +94,11 @@ EOF
 '
 
 
-test_expect_success 'status when rebase in progress before rebase --continue' '
+test_expect_success 'status when rebase --am in progress before rebase --continue' '
 	git reset --hard rebase_conflicts &&
 	test_when_finished "git rebase --abort" &&
 	ONTO=$(git rev-parse --short HEAD^^) &&
-	test_must_fail git rebase HEAD^ --onto HEAD^^ &&
+	test_must_fail git rebase --am HEAD^ --onto HEAD^^ &&
 	echo three >main.txt &&
 	git add main.txt &&
 	cat >expected <<EOF &&
@@ -688,7 +688,7 @@ EOF
 '
 
 
-test_expect_success 'status when rebase conflicts with statushints disabled' '
+test_expect_success 'status when rebase --am conflicts with statushints disabled' '
 	git reset --hard master &&
 	git checkout -b statushints_disabled &&
 	test_when_finished "git config --local advice.statushints true" &&
@@ -698,7 +698,7 @@ test_expect_success 'status when rebase conflicts with statushints disabled' '
 	test_commit three_statushints main.txt three &&
 	test_when_finished "git rebase --abort" &&
 	ONTO=$(git rev-parse --short HEAD^^) &&
-	test_must_fail git rebase HEAD^ --onto HEAD^^ &&
+	test_must_fail git rebase --am HEAD^ --onto HEAD^^ &&
 	cat >expected <<EOF &&
 rebase in progress; onto $ONTO
 You are currently rebasing branch '\''statushints_disabled'\'' on '\''$ONTO'\''.
Signature · 1 line
gitgitgadget
#69 lore raw

PATCH v3 14/15 rebase: make the backend configurable via config setting

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

---
 Documentation/config/rebase.txt |  8 ++++++++
 builtin/rebase.c                | 31 ++++++++++++++++++++++++-------
 2 files changed, 32 insertions(+), 7 deletions(-)
Documentation/config/rebase.txt +8 -0
index d98e32d812..e6ae30c999 100644
@@ -5,6 +5,14 @@ rebase.useBuiltin::
 	is always used. Setting this will emit a warning, to alert any
 	remaining users that setting this now does nothing.
 
+rebase.backend::
+	Default backend to use for rebasing.  Possible choices are
+	'am' or 'merge' (note that the merge backend is sometimes also
+	refered to as the interactive backend or the interactive
+	machinery elsewhere in the docs).  Also, in the future, if the
+	merge backend gains all remaining capabilities of the am
+	backend, this setting may become unused.
+
 rebase.stat::
 	Whether to show a diffstat of what changed upstream since the last
 	rebase. False by default.
builtin/rebase.c +24 -7
index b7915fc0cb..d602b2da4c 100644
@@ -60,6 +60,7 @@ enum empty_type {
 struct rebase_options {
 	enum rebase_type type;
 	enum empty_type empty;
+	const char *default_backend;
 	const char *state_dir;
 	struct commit *upstream;
 	const char *upstream_name;
@@ -103,6 +104,7 @@ struct rebase_options {
 #define REBASE_OPTIONS_INIT {			  	\
 		.type = REBASE_UNSPECIFIED,	  	\
 		.empty = EMPTY_UNSPECIFIED,	  	\
+		.default_backend = "am",	  	\
 		.flags = REBASE_NO_QUIET, 		\
 		.git_am_opts = ARGV_ARRAY_INIT,		\
 		.git_format_patch_opt = STRBUF_INIT	\
@@ -1298,6 +1300,10 @@ static int rebase_config(const char *var, const char *value, void *data)
 		return 0;
 	}
 
+	if (!strcmp(var, "rebase.backend")) {
+		return git_config_string(&opts->default_backend, var, value);
+	}
+
 	return git_default_config(var, value, data);
 }
 
@@ -1928,9 +1934,23 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
 			if (strcmp(options.git_am_opts.argv[i], "-q"))
 				break;
 
-		if (is_interactive(&options) && i >= 0)
-			die(_("cannot combine am options with either "
-			      "interactive or merge options"));
+		if (i >= 0) {
+			if (is_interactive(&options))
+				die(_("cannot combine am options with either "
+				      "interactive or merge options"));
+			else
+				options.type = REBASE_AM;
+		}
+	}
+
+	if (options.type == REBASE_UNSPECIFIED) {
+		if (!strcmp(options.default_backend, "merge"))
+			options.type = REBASE_MERGE;
+		else if (!strcmp(options.default_backend, "am"))
+			options.type = REBASE_AM;
+		else
+			die(_("Unknown rebase backend: %s"),
+			    options.default_backend);
 	}
 
 	switch (options.type) {
@@ -1943,10 +1963,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
 		options.state_dir = apply_dir();
 		break;
 	default:
-		/* the default rebase backend is `--am` */
-		options.type = REBASE_AM;
-		options.state_dir = apply_dir();
-		break;
+		BUG("options.type was just set above; should be unreachable.");
 	}
 
 	if (options.empty == EMPTY_UNSPECIFIED) {
Signature · 1 line
gitgitgadget
#58 lore raw

PATCH v3 9/15 rebase: move incompatibility checks between backend options a bit earlier

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

---
 builtin/rebase.c | 22 +++++++++++-----------
 1 file changed, 11 insertions(+), 11 deletions(-)
builtin/rebase.c +11 -11
index 7027e34567..d2b99e9908 100644
@@ -1906,6 +1906,17 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
 	if (isatty(2) && options.flags & REBASE_NO_QUIET)
 		strbuf_addstr(&options.git_format_patch_opt, " --progress");
 
+	if (options.git_am_opts.argc) {
+		/* all am options except -q are compatible only with --am */
+		for (i = options.git_am_opts.argc - 1; i >= 0; i--)
+			if (strcmp(options.git_am_opts.argv[i], "-q"))
+				break;
+
+		if (is_interactive(&options) && i >= 0)
+			die(_("cannot combine am options with either "
+			      "interactive or merge options"));
+	}
+
 	switch (options.type) {
 	case REBASE_MERGE:
 	case REBASE_INTERACTIVE:
@@ -1936,17 +1947,6 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
 	if (reschedule_failed_exec >= 0)
 		options.reschedule_failed_exec = reschedule_failed_exec;
 
-	if (options.git_am_opts.argc) {
-		/* all am options except -q are compatible only with --am */
-		for (i = options.git_am_opts.argc - 1; i >= 0; i--)
-			if (strcmp(options.git_am_opts.argv[i], "-q"))
-				break;
-
-		if (is_interactive(&options) && i >= 0)
-			die(_("cannot combine am options with either "
-			      "interactive or merge options"));
-	}
-
 	if (options.signoff) {
 		if (options.type == REBASE_PRESERVE_MERGES)
 			die("cannot combine '--signoff' with "
Signature · 1 line
gitgitgadget
#70 lore raw

PATCH v3 15/15 rebase: change the default backend from "am" to "merge"

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

The am-backend drops information and thus limits what we can do:

* lack of full tree information from the original commits means we cannot do directory rename detection and warn users that they might want to move some of their new files that they placed in old directories to prevent their becoming orphaned.[1]
* reduction in context from only having a few lines beyond those changed means that when context lines are non-unique we can apply patches incorrectly.[2]
* lack of access to original commits means that conflict marker annotation has less information available.

Also, the merge/interactive backend have far more abilities, appear to currently have a slight performance advantage[3] and have room for more optimizations than the am backend[4] (and work is underway to take advantage of some of those possibilities).

[1] https://lore.kernel.org/git/[email protected]/
[2] https://lore.kernel.org/git/CABPp-BGiu2nVMQY_t-rnFR5GQUz_ipyEE8oDocKeO+h+t4Mn4A@mail.gmail.com/
[3] https://public-inbox.org/git/CABPp-BF=ev03WgODk6TMQmuNoatg2kiEe5DR__gJ0OTVqHSnfQ@mail.gmail.com/
[4] https://lore.kernel.org/git/CABPp-BGh7yW69QwxQb13K0HM38NKmQif3A6C6UULEKYnkEJ5vA@mail.gmail.com/

---
 Documentation/git-rebase.txt           |  2 +-
 builtin/rebase.c                       |  4 ++--
 t/t5520-pull.sh                        | 10 ++++++----
 t/t9106-git-svn-commit-diff-clobber.sh |  3 ++-
 4 files changed, 11 insertions(+), 8 deletions(-)
Documentation/git-rebase.txt +1 -1
index cf1ac2e359..e819889a31 100644
@@ -309,7 +309,7 @@ See also INCOMPATIBLE OPTIONS below.
 --merge::
 	Use merging strategies to rebase.  When the recursive (default) merge
 	strategy is used, this allows rebase to be aware of renames on the
-	upstream side.
+	upstream side.  This is the default.
 +
 Note that a rebase merge works by replaying each commit from the working
 branch on top of the <upstream> branch.  Because of this, when a merge
builtin/rebase.c +2 -2
index d602b2da4c..938a8840b1 100644
@@ -104,7 +104,7 @@ struct rebase_options {
 #define REBASE_OPTIONS_INIT {			  	\
 		.type = REBASE_UNSPECIFIED,	  	\
 		.empty = EMPTY_UNSPECIFIED,	  	\
-		.default_backend = "am",	  	\
+		.default_backend = "merge",	  	\
 		.flags = REBASE_NO_QUIET, 		\
 		.git_am_opts = ARGV_ARRAY_INIT,		\
 		.git_format_patch_opt = STRBUF_INIT	\
@@ -1945,7 +1945,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
 
 	if (options.type == REBASE_UNSPECIFIED) {
 		if (!strcmp(options.default_backend, "merge"))
-			options.type = REBASE_MERGE;
+			imply_interactive(&options, "--merge");
 		else if (!strcmp(options.default_backend, "am"))
 			options.type = REBASE_AM;
 		else
t/t5520-pull.sh +6 -4
index 3fff6a06fa..4f9e7f7ff6 100755
@@ -340,7 +340,7 @@ test_expect_success '--rebase with conflicts shows advice' '
 	test_tick &&
 	git commit -m "Create conflict" seq.txt &&
 	test_must_fail git pull --rebase . seq 2>err >out &&
-	test_i18ngrep "Resolve all conflicts manually" out
+	test_i18ngrep "Resolve all conflicts manually" err
 '
 
 test_expect_success 'failed --rebase shows advice' '
@@ -354,7 +354,7 @@ test_expect_success 'failed --rebase shows advice' '
 	git checkout -f -b fails-to-rebase HEAD^ &&
 	test_commit v2-without-cr file "2" file2-lf &&
 	test_must_fail git pull --rebase . diverging 2>err >out &&
-	test_i18ngrep "Resolve all conflicts manually" out
+	test_i18ngrep "Resolve all conflicts manually" err
 '
 
 test_expect_success '--rebase fails with multiple branches' '
@@ -774,8 +774,10 @@ test_expect_success 'git pull --rebase does not reapply old patches' '
 	(
 		cd dst &&
 		test_must_fail git pull --rebase &&
-		find .git/rebase-apply -name "000*" >patches &&
-		test_line_count = 1 patches
+		cat .git/rebase-merge/done .git/rebase-merge/git-rebase-todo >work &&
+		grep -v -e \# -e ^$ work >patches &&
+		test_line_count = 1 patches &&
+		rm -f work
 	)
 '
 
t/t9106-git-svn-commit-diff-clobber.sh +2 -1
index dbe8deac0d..aec45bca3b 100755
@@ -92,7 +92,8 @@ test_expect_success 'multiple dcommit from git svn will not clobber svn' "
 
 
 test_expect_success 'check that rebase really failed' '
-	test -d .git/rebase-apply
+	git status >output &&
+	grep currently.rebasing output
 '
 
 test_expect_success 'resolve, continue the rebase and dcommit' "
Signature · 1 line
gitgitgadget
#68 lore raw

PATCH v3 13/15 rebase tests: repeat some tests using the merge backend instead of am

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

In order to ensure the merge/interactive backend gets similar coverage to the am one, add some tests for cases where previously only the am backend was tested.

---
 t/t5520-pull.sh                   | 17 +++++++++++++++--
 t/t6047-diff3-conflict-markers.sh | 13 +++++++++++--
 2 files changed, 26 insertions(+), 4 deletions(-)
t/t5520-pull.sh +15 -2
index 602d996a33..3fff6a06fa 100755
@@ -277,14 +277,27 @@ test_expect_success '--rebase' '
 	test_cmp expect actual
 '
 
-test_expect_success '--rebase fast forward' '
+test_expect_success '--rebase (merge) fast forward' '
 	git reset --hard before-rebase &&
 	git checkout -b ff &&
 	echo another modification >file &&
 	git commit -m third file &&
 
 	git checkout to-rebase &&
-	git pull --rebase . ff &&
+	git -c rebase.backend=merge pull --rebase . ff &&
+	test_cmp_rev HEAD ff &&
+
+	# The above only validates the result.  Did we actually bypass rebase?
+	git reflog -1 >reflog.actual &&
+	sed "s/^[0-9a-f][0-9a-f]*/OBJID/" reflog.actual >reflog.fuzzy &&
+	echo "OBJID HEAD@{0}: pull --rebase . ff: Fast-forward" >reflog.expected &&
+	test_cmp reflog.expected reflog.fuzzy
+'
+
+test_expect_success '--rebase (am) fast forward' '
+	git reset --hard before-rebase &&
+
+	git -c rebase.backend=am pull --rebase . ff &&
 	test_cmp_rev HEAD ff &&
 
 	# The above only validates the result.  Did we actually bypass rebase?
t/t6047-diff3-conflict-markers.sh +11 -2
index 860542aad0..d383ce8130 100755
@@ -186,7 +186,7 @@ test_expect_success 'check multiple merge bases' '
 	)
 '
 
-test_expect_success 'rebase describes fake ancestor base' '
+test_expect_success 'rebase --merge describes parent of commit being picked' '
 	test_create_repo rebase &&
 	(
 		cd rebase &&
@@ -194,7 +194,16 @@ test_expect_success 'rebase describes fake ancestor base' '
 		test_commit master file &&
 		git checkout -b side HEAD^ &&
 		test_commit side file &&
-		test_must_fail git -c merge.conflictstyle=diff3 rebase master &&
+		test_must_fail git -c merge.conflictstyle=diff3 rebase --merge master &&
+		grep "||||||| parent of" file
+	)
+'
+
+test_expect_success 'rebase --am describes fake ancestor base' '
+	(
+		cd rebase &&
+		git rebase --abort &&
+		test_must_fail git -c merge.conflictstyle=diff3 rebase --am master &&
 		grep "||||||| constructed merge base" file
 	)
 '
Signature · 1 line
gitgitgadget
#47 lore raw

Re: [PATCH v3 01/15] rebase: extend the options for handling of empty commits

In reply to Elijah Newren via GitGitGadget

Hi Elijah

Thanks for working on this series, I think making the sequencer the default backend is a good idea. I have a few reservations about this path though...

Quoted 8 lines · Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

Extend the interactive machinery with the ability to handle the full spread of options for how to handle commits that either start or become empty (by "become empty" I mean the changes in a commit are a subset of changes that exist upstream, so the net effect of applying the commit is no changes). Introduce a new command line flag for selecting the desired behavior:

Quoted 6 lines
    --empty={drop,keep,ask}

with the definitions:
drop: drop empty commits
keep: keep empty commits
ask: provide the user a chance to interact and pick what to do with
empty commits on a case-by-case basis

I think we want to distinguish between commits that are empty before
rebasing and those that become empty when they are rebased. --keep-empty explicily only applies to commits that are already empty. Cherry-pick
has distinct options for those two cases. If I've explicitly created an empty commit then I want to keep it but I don't want to keep commits that become empty because the changes they contain are already upstream.

If we want an option that keeps commits that become empty (Off hand I don't know why we would though) we should consider if that option should disable --cherry-mark when we create the todo list so that it keeps all commits that become empty when they're rebased.

Quoted 7 lines

Note that traditionally, am-based rebases have always dropped commits that either started or became empty, while interactive-based rebases have defaulted to ask (and provided an option to keep commits that started empty). This difference made sense since users of an am-based rebase just wanted to quickly batch apply a sequence of commits, while users editing a todo list will likely want the chance to interact and handle unusual cases on a case-by-case basis.

I don't see why it makes sense to drop an empty commit that I've made just because it is being rebased. I'm pretty sure the behavor of the am-based rebase is a function of git am not being able to create empty commits.

Quoted 5 lines

However, not all rebases
using the interactive machinery are explicitly interactive anymore. In particular --merge was always meant to behave more like --am: just rebase a batch of commits without popping up a todo list.

Quoted 7 lines

If the --empty flag is not specified, pick defaults as follows:
explicitly interactive: ask
--exec: keep (exec is about checking existing commits, and often
used without actually changing the base. Thus the expectation is that the user doesn't necessarily want anything to change; they just want to test).
otherwise: drop

I'm not sure I like changing the behavior based on --exec, I see what you're getting at but it has the potential to be confusing. What if I want to rearrange the commits without changing the base - why must I specify --empty=keep there but not if I add --exec to the command line?

Quoted 10 lines

Also, this commit makes --keep-empty just imply --empty=keep, and hides it from help so that we aren't confusing users with different ways to do the same thing. (I could have added a --drop-empty flag, but then that invites users to specify both --keep-empty and --drop-empty and we have to add sanity checking around that; it seems cleaner to have a single multi-valued option.) This actually fixes --keep-empty too; previously, it only meant to sometimes keep empty commits, in particular commits which started empty would be kept. But it would still error out and ask the user what to do with commits that became empty. Now it keeps empty commits, as instructed.

It certainly changes the behavior of --keep-empty but I'm not sure it "fixes" it. If I have some empty commits I want to keep as placeholders then that's different from wanting to keep commits that become empty because their changes are upstream but --cherry-mark didn't detect them.

In summary I'm in favor of making it easier to drop commits that become empty but not tying that to the handling of commits that are empty before they are rebased.

I'm also not happy that the deprecation of --keep-empty suddenly makes --no-keep-empty an error.

Quoted 164 lines
---
 Documentation/git-rebase.txt      | 35 ++++++------
 builtin/rebase.c                  | 83 +++++++++++++++++++++++++---
 rebase-interactive.c              |  4 +-
 rebase-interactive.h              |  2 +-
 sequencer.c                       | 74 +++++++++++++++++++------
 sequencer.h                       |  6 ++-
 t/t3421-rebase-topology-linear.sh |  4 +-
 t/t3424-rebase-empty.sh           | 89 +++++++++++++++++++++++++++++++
 t/t3427-rebase-subtree.sh         | 16 +++---
 9 files changed, 263 insertions(+), 50 deletions(-)
 create mode 100755 t/t3424-rebase-empty.sh
Documentation/git-rebase.txt +20 -15
index 1d0e2d27cc..ff32ca1080 100644
@@ -258,9 +258,25 @@ 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 become empty (because they contain a
+	subset of already upstream changes) or start empty.  With drop
+	(the default), commits that start or become empty are dropped.
+	With keep (implied by --exec), 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 or
+	commit it.  Also with ask, if the rebase is interactive then
+	commits which start empty will be commented out in the todo
+	action list (giving you a chance to uncomment).
++
+Note that this has no effect on commits which are already upstream (as
+can be checked via `git log --cherry-mark ...`), which are always
+dropped by rebase.
++
+See also INCOMPATIBLE OPTIONS below.
+
 --keep-empty::
-	Keep the commits that do not change anything from its
-	parents in the result.
+	Deprecated alias for what is now known as --empty=keep.
 +
 See also INCOMPATIBLE OPTIONS below.
 
@@ -569,6 +585,7 @@ are incompatible with the following options:
  * --interactive
  * --exec
  * --keep-empty
+ * --empty=
  * --edit-todo
  * --root when used in combination with --onto
 
@@ -580,6 +597,7 @@ In addition, the following pairs of options are incompatible:
  * --preserve-merges and --ignore-whitespace
  * --preserve-merges and --committer-date-is-author-date
  * --preserve-merges and --ignore-date
+ * --preserve-merges and --empty=
  * --keep-base and --onto
  * --keep-base and --root
 
@@ -588,19 +606,6 @@ BEHAVIORAL DIFFERENCES
 
 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 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.
-
 Directory rename detection
 ~~~~~~~~~~~~~~~~~~~~~~~~~~
 
builtin/rebase.c +30 -2
index ddf33bc9d4..6903249307 100644
@@ -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;
@@ -77,7 +85,6 @@ struct rebase_options {
 	const char *action;
 	int signoff;
 	int allow_rerere_autoupdate;
-	int keep_empty;
 	int autosquash;
 	int ignore_whitespace;
 	char *gpg_sign_opt;
@@ -95,6 +102,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	\
@@ -114,6 +122,10 @@ 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.ask_on_initially_empty = (opts->empty == EMPTY_ASK &&
+					 !(opts->flags & REBASE_INTERACTIVE_EXPLICIT));
 	replay.verbose = opts->flags & REBASE_VERBOSE;
 	replay.reschedule_failed_exec = opts->reschedule_failed_exec;
 	replay.committer_date_is_author_date =
@@ -389,7 +401,10 @@ 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 |= (opts->empty == EMPTY_DROP) ? TODO_LIST_DROP_EMPTY : 0;
+	flags |= (opts->empty == EMPTY_ASK &&
+		  opts->flags & REBASE_INTERACTIVE_EXPLICIT) ?
+			TODO_LIST_ASK_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;
@@ -453,6 +468,19 @@ 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_NEG(unset);
+	BUG_ON_OPT_ARG(arg);
+
+	opts->empty = EMPTY_KEEP;
+	opts->type = REBASE_INTERACTIVE;
+	return 0;
+}
+
 static const char * const builtin_rebase_interactive_usage[] = {
 	N_("git rebase--interactive [<options>]"),
 	NULL

builtin/rebase.c int cmd_rebase__interactive(int argc, const char **argv, const char *prefix) · line 499

Quoted 7 lines
builtin/rebase.c +3 -1
@@ -466,7 +494,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_NONEG | PARSE_OPT_HIDDEN,

It is all very well deprecating --keep-empty but suddenly making '--no-keep-empty' an error goes beyond deprecation. Also I'm not sure it's worth changing these options as I think the only user is git-rebase--preserve-merges.sh

Best Wishes

Phillip

sequencer.c static int populate_opts_cb(const char *key, const char *value, void *data)

Quoted 181 lines
builtin/rebase.c +42 -5
+			parse_opt_keep_empty },
 		OPT_BOOL(0, "allow-empty-message", &opts.allow_empty_message,
 			 N_("allow commits with empty messages")),
 		OPT_BOOL(0, "rebase-merges", &opts.rebase_merges, N_("rebase merge commits")),
@@ -1166,7 +1197,7 @@ 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, "empty", opts->empty == EMPTY_KEEP ? "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);
@@ -1360,6 +1391,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);
@@ -1505,8 +1559,13 @@ 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")),
+		OPT_CALLBACK_F(0, "empty", &options, N_("{drop,keep,ask}"),
+			       N_("how to handle empty commits"),
+			       PARSE_OPT_NONEG, parse_opt_empty),
+		{ OPTION_CALLBACK, 'k', "keep-empty", &options, NULL,
+			N_("(DEPRECATED) keep empty commits"),
+			PARSE_OPT_NOARG | PARSE_OPT_NONEG | PARSE_OPT_HIDDEN,
+			parse_opt_keep_empty },
 		OPT_BOOL(0, "autosquash", &options.autosquash,
 			 N_("move commits that begin with "
 			    "squash!/fixup! under -i")),
@@ -1770,8 +1829,8 @@ 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 (options.empty != EMPTY_UNSPECIFIED)
+		imply_interactive(&options, "--empty");
 
 	if (gpg_sign) {
 		free(options.gpg_sign_opt);
@@ -1856,6 +1915,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"));
rebase-interactive.c +2 -2
index aa18ae82b7..ad82bf77df 100644
@@ -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(unsigned no_ask_empty, int command_count,
 		      const char *shortrevisions, const char *shortonto,
 		      struct strbuf *buf)
 {
@@ -81,7 +81,7 @@ void append_todo_help(unsigned keep_empty, int command_count,
 
 	strbuf_add_commented_lines(buf, msg, strlen(msg));
 
-	if (!keep_empty) {
+	if (!no_ask_empty) {
 		msg = _("Note that empty commits are commented out");
 		strbuf_add_commented_lines(buf, msg, strlen(msg));
 	}
rebase-interactive.h +1 -1
index 44dbb06311..f531e00ba7 100644
@@ -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(unsigned no_ask_empty, int command_count,
 		      const char *shortrevisions, const char *shortonto,
 		      struct strbuf *buf);
 int edit_todo_list(struct repository *r, struct todo_list *todo_list,
sequencer.c +19 -9
index 763ccbbc45..d2c11f34b7 100644
@@ -160,6 +160,9 @@ 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 GIT_PATH_FUNC(rebase_path_ask_on_initially_empty, "rebase-merge/ask_on_initially_empty")
 
 static int git_sequencer_config(const char *k, const char *v, void *cb)
 {
@@ -1623,7 +1626,7 @@ static int allow_empty(struct repository *r,
 	empty_commit = is_original_commit_empty(commit);
 	if (empty_commit < 0)
 		return empty_commit;
-	if (!empty_commit)
+	if (!empty_commit || opts->ask_on_initially_empty)
 		return 0;
 	else
 		return 1;
@@ -1837,7 +1840,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) {
 		/*
@@ -2042,13 +2045,20 @@ static int do_pick_commit(struct repository *r,
 		goto leave;
 	}
 
-	allow = allow_empty(r, opts, commit);
-	if (allow < 0) {
-		res = allow;
-		goto leave;
-	} else if (allow)
-		flags |= ALLOW_EMPTY;
-	if (!opts->no_commit) {
+	drop_commit = 0;
+	if (opts->drop_redundant_commits && is_index_unchanged(r)) {
+		drop_commit = 1;
+		fprintf(stderr, _("No changes -- Patch already applied."));
+	} else {
+		allow = allow_empty(r, opts, commit);
+		if (allow < 0) {
+			res = allow;
+			goto leave;
+		} else if (allow) {
+			flags |= ALLOW_EMPTY;
+		}
+	}
+	if (!opts->no_commit && !drop_commit) {
 		if (author || command == TODO_REVERT || (flags & AMEND_MSG))
 			res = do_commit(r, msg_file, author, opts, flags);
 		else
@@ -2501,9 +2511,15 @@ static int populate_opts_cb(const char *key, const char *value, void *data)

This function is used by cherry-pick/revert not rebase do we need to change it?

sequencer.c static int save_opts(struct replay_opts *opts)

Quoted 44 lines
sequencer.c +21 -0
 	else if (!strcmp(key, "options.allow-empty-message"))
 		opts->allow_empty_message =
 			git_config_bool_or_int(key, value, &error_flag);
+	else if (!strcmp(key, "options.drop-redundant-commits"))
+		opts->drop_redundant_commits =
+			git_config_bool_or_int(key, value, &error_flag);
 	else if (!strcmp(key, "options.keep-redundant-commits"))
 		opts->keep_redundant_commits =
 			git_config_bool_or_int(key, value, &error_flag);
+	else if (!strcmp(key, "options.ask_on_initially_empty"))
+		opts->ask_on_initially_empty =
+			git_config_bool_or_int(key, value, &error_flag);>  	else if (!strcmp(key, "options.signoff"))
 		opts->signoff = git_config_bool_or_int(key, value, &error_flag);
 	else if (!strcmp(key, "options.record-origin"))
@@ -2612,6 +2628,15 @@ 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;
+
+		if (file_exists(rebase_path_ask_on_initially_empty()))
+			opts->ask_on_initially_empty = 1;
+
 		read_strategy_opts(opts, &buf);
 		strbuf_release(&buf);
 
@@ -2695,6 +2720,12 @@ int write_basic_state(struct replay_opts *opts, const char *head_name,
 		write_file(rebase_path_cdate_is_adate(), "%s", "");
 	if (opts->ignore_date)
 		write_file(rebase_path_ignore_date(), "%s", "");
+	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->ask_on_initially_empty)
+		write_file(rebase_path_ask_on_initially_empty(), "%s", "");
 	if (opts->reschedule_failed_exec)
 		write_file(rebase_path_reschedule_failed_exec(), "%s", "");
 
@@ -3033,9 +3064,15 @@ static int save_opts(struct replay_opts *opts)

again this is for cherry-pick/revert

Quoted 267 lines
sequencer.c +19 -6
 	if (opts->allow_empty_message)
 		res |= git_config_set_in_file_gently(opts_file,
 				"options.allow-empty-message", "true");
+	if (opts->drop_redundant_commits)
+		res |= git_config_set_in_file_gently(opts_file,
+				"options.drop-redundant-commits", "true");
 	if (opts->keep_redundant_commits)
 		res |= git_config_set_in_file_gently(opts_file,
 				"options.keep-redundant-commits", "true");
+	if (opts->ask_on_initially_empty)
+		res |= git_config_set_in_file_gently(opts_file,
+				"options.ask_on_initially_empty", "true");
 	if (opts->signoff)
 		res |= git_config_set_in_file_gently(opts_file,
 					"options.signoff", "true");
@@ -4691,7 +4728,8 @@ 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 drop_empty = flags & TODO_LIST_DROP_EMPTY;
+	int ask_empty = flags & TODO_LIST_ASK_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;
@@ -4746,6 +4784,8 @@ static int make_script_with_merges(struct pretty_print_context *pp,
 		is_empty = is_original_commit_empty(commit);
 		if (!is_empty && (commit->object.flags & PATCHSAME))
 			continue;
+		if (is_empty && drop_empty)
+			continue;
 
 		strbuf_reset(&oneline);
 		pretty_print_commit(pp, commit, &oneline);
@@ -4754,7 +4794,7 @@ 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)
+			if (is_empty && ask_empty)
 				strbuf_addf(&buf, "%c ", comment_line_char);
 			strbuf_addf(&buf, "%s %s %s", cmd_pick,
 				    oid_to_hex(&commit->object.oid),
@@ -4922,7 +4962,8 @@ 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;
+	int drop_empty = flags & TODO_LIST_DROP_EMPTY;
+	int ask_empty = flags & TODO_LIST_ASK_EMPTY;
 	const char *insn = flags & TODO_LIST_ABBREVIATE_CMDS ? "p" : "pick";
 	int rebase_merges = flags & TODO_LIST_REBASE_MERGES;
 
@@ -4958,11 +4999,13 @@ 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)
+		if (is_empty && drop_empty)
+			continue;
+		if (is_empty && ask_empty)
 			strbuf_addf(out, "%c ", comment_line_char);
 		strbuf_addf(out, "%s %s ", insn,
 			    oid_to_hex(&commit->object.oid));
@@ -5100,7 +5143,8 @@ 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(!(flags & TODO_LIST_ASK_EMPTY),
+				 count_commands(todo_list),
 				 shortrevisions, shortonto, &buf);
 
 	res = write_message(buf.buf, buf.len, file, 0);
sequencer.h +5 -1
index e9a0e03ea2..1c3abb661c 100644
@@ -39,7 +39,9 @@ struct replay_opts {
 	int allow_rerere_auto;
 	int allow_empty;
 	int allow_empty_message;
+	int drop_redundant_commits;
 	int keep_redundant_commits;
+	int ask_on_initially_empty;
 	int verbose;
 	int quiet;
 	int reschedule_failed_exec;
@@ -134,7 +136,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)
@@ -150,6 +152,8 @@ int sequencer_remove_state(struct replay_opts *opts);
  * `--onto`, we do not want to re-generate the root commits.
  */
 #define TODO_LIST_ROOT_WITH_ONTO (1U << 6)
+#define TODO_LIST_DROP_EMPTY (1U << 7)
+#define TODO_LIST_ASK_EMPTY (1U << 8)
 
 
 int sequencer_make_script(struct repository *r, struct strbuf *out, int argc,
t/t3421-rebase-topology-linear.sh +2 -2
index 325072b0a3..d23e0bf778 100755
@@ -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
t/t3424-rebase-empty.sh new file +89 -0
new file mode 100755
index 0000000000..9d52e1417f
@@ -0,0 +1,89 @@
+#!/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_success 'rebase --merge --empty=drop' '
+	git checkout -B testing localmods &&
+	git rebase --merge --empty=drop upstream &&
+
+	test_write_lines C B A >expect &&
+	git log --format=%s >actual &&
+	test_cmp expect actual
+'
+
+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 &&
+
+	test_must_fail git rebase --skip &&
+	git commit --allow-empty &&
+	git rebase --continue &&
+
+	test_write_lines D C B A >expect &&
+	git log --format=%s >actual &&
+	test_cmp expect actual
+'
+
+GIT_SEQUENCE_EDITOR=: && export GIT_SEQUENCE_EDITOR
+
+test_expect_success 'rebase --interactive --empty=drop' '
+	git checkout -B testing localmods &&
+	git rebase --interactive --empty=drop upstream &&
+
+	test_write_lines 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_done
t/t3427-rebase-subtree.sh +10 -6
index bec48e6a1f..468ebc1bef 100755
@@ -85,23 +85,27 @@ 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 --empty=ask --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 --empty=ask --onto files-master master &&
 	: first pick results in no changes &&
-	git rebase --continue &&
+	test_must_fail git rebase --skip &&
+	: last pick was an empty commit that has no changes, but we want to keep it &&
+	git commit --allow-empty &&
 	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 --empty=ask --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 --empty=ask --rebase-merges --onto files-master --root &&
 	: first pick results in no changes &&
-	git rebase --continue &&
+	test_must_fail git rebase --skip &&
+	: last pick was an empty commit that has no changes, but we want to keep it &&
+	git commit --allow-empty &&
 	verbose test "$(commit_message HEAD~2)" = "master4" &&
 	verbose test "$(commit_message HEAD~)" = "files_subtree/master5" &&
 	verbose test "$(commit_message HEAD)" = "Empty commit"
#60 lore raw

Re: [PATCH v3 10/15] rebase: add an --am option

In reply to Elijah Newren via GitGitGadget

Hi Elijah

Quoted 6 lines · Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

Currently, this option doesn't do anything except error out if any options requiring the interactive-backend are also passed. However, when we make the default backend configurable later in this series, this flag will provide a way to override the config setting.

I wonder if we really want to add a new option that we know we don't want to keep in the long term. Could we not just default to the interactive backend and fallback to the am backend if the user passes any of the am-based options on the commandline? i.e. reverse the current situation of defaulting to the am backend and selecting the interactive backend when the user passes certain options.

Best Wishes

Phillip

Quoted 93 lines
---
  Documentation/git-rebase.txt | 11 ++++++++++-
  builtin/rebase.c             | 18 +++++++++++++++++-
  2 files changed, 27 insertions(+), 2 deletions(-)
Documentation/git-rebase.txt +10 -1
index f1ace07c38..cf1ac2e359 100644
@@ -258,6 +258,13 @@ See also INCOMPATIBLE OPTIONS below.
  	original branch. The index and working tree are also left
  	unchanged as a result.
 
+--am:
+	Use git-am internally to rebase.  This option may become a
+	no-op in the future once the interactive backend handles
+	everything the am one does.
++
+See also INCOMPATIBLE OPTIONS below.
+
  --empty={drop,keep,ask}::
  	How to handle commits that become empty (because they contain a
  	subset of already upstream changes) or start empty.  With drop
@@ -372,7 +379,7 @@ See also INCOMPATIBLE OPTIONS below.
  	Ensure at least <n> lines of surrounding context match before
  	and after each change.  When fewer lines of surrounding
  	context exist they all must match.  By default no context is
-	ever ignored.
+	ever ignored.  Implies --am.
  +
  See also INCOMPATIBLE OPTIONS below.
 
@@ -417,6 +424,7 @@ with `--keep-base` in order to drop those commits from your branch.
  --whitespace=<option>::
  	This flag is passed to the 'git apply' program
  	(see linkgit:git-apply[1]) that applies the patch.
+	Implies --am.
  +
  See also INCOMPATIBLE OPTIONS below.
 
@@ -567,6 +575,7 @@ INCOMPATIBLE OPTIONS
 
  The following options:
 
+ * --am
   * --whitespace
   * -C
 
builtin/rebase.c +17 -1
index d2b99e9908..b7915fc0cb 100644
@@ -1361,6 +1361,18 @@ static int can_fast_forward(struct commit *onto, struct commit *upstream,
  	return res && is_linear_history(onto, head);
  }
 
+static int parse_opt_am(const struct option *opt, const char *arg, int unset)
+{
+	struct rebase_options *opts = opt->value;
+
+	BUG_ON_OPT_NEG(unset);
+	BUG_ON_OPT_ARG(arg);
+
+	opts->type = REBASE_AM;
+
+	return 0;
+}
+
  /* -i followed by -m is still -i */
  static int parse_opt_merge(const struct option *opt, const char *arg, int unset)
  {
@@ -1546,6 +1558,10 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
  		OPT_CMDMODE(0, "show-current-patch", &action,
  			    N_("show the patch file being applied or merged"),
  			    ACTION_SHOW_CURRENT_PATCH),
+		{ OPTION_CALLBACK, 0, "am", &options, NULL,
+			N_("use apply-mail strategies to rebase"),
+			PARSE_OPT_NOARG | PARSE_OPT_NONEG,
+			parse_opt_am },
  		{ OPTION_CALLBACK, 'm', "merge", &options, NULL,
  			N_("use merging strategies to rebase"),
  			PARSE_OPT_NOARG | PARSE_OPT_NONEG,
@@ -1906,7 +1922,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
  	if (isatty(2) && options.flags & REBASE_NO_QUIET)
  		strbuf_addstr(&options.git_format_patch_opt, " --progress");
 
-	if (options.git_am_opts.argc) {
+	if (options.git_am_opts.argc || options.type == REBASE_AM) {
  		/* all am options except -q are compatible only with --am */
  		for (i = options.git_am_opts.argc - 1; i >= 0; i--)
  			if (strcmp(options.git_am_opts.argv[i], "-q"))
#48 lore raw

Re: [PATCH v3 01/15] rebase: extend the options for handling of empty commits

In reply to Phillip Wood

Hi Phillip,

Quoted 6 lines · Phillip Wood

Hi Elijah

Thanks for working on this series, I think making the sequencer the default backend is a good idea. I have a few reservations about this path though...

Thanks for the feedback. You've provided some good food for thought. While responding, I came up with an alternate proposal below which I think may be more in line with what you want. Let me know...

Quoted 17 lines · Elijah Newren via GitGitGadget
Quoted 8 lines

From: Elijah Newren <[email protected]>

Extend the interactive machinery with the ability to handle the full spread of options for how to handle commits that either start or become empty (by "become empty" I mean the changes in a commit are a subset of changes that exist upstream, so the net effect of applying the commit is no changes). Introduce a new command line flag for selecting the desired behavior:

Quoted 6 lines
    --empty={drop,keep,ask}

with the definitions:
drop: drop empty commits
keep: keep empty commits
ask: provide the user a chance to interact and pick what to do with
empty commits on a case-by-case basis

Quoted 3 lines

I think we want to distinguish between commits that are empty before rebasing and those that become empty when they are rebased. --keep-empty explicily only applies to commits that are already empty. Cherry-pick

I am open to changing how the empty handling works; I figured it was the most likely area people might have feedback about.

However, I do strongly disagree with the "explicit" claim here. It is possible that --keep-empty was designed to only apply to commits that begin empty, but that is not at all explicit. The only place in the documentation I could find that mentions the distinction at all was in the "Behavioral Differences" section, which was written by me as part of commit 0661e49aeb84 ("git-rebase.txt: document behavioral differences between modes", 2018-06-27). Quoting from that commit message:

"(In fact, it's not clear -- to me at least -- that these differences were even desirable or intentional.) Document these differences."

I cannot find anywhere else making a distinction in either the documentation or the commit messages or even any code comments, which has left me wondering whether the distinction was intentional at all. Things like commit b00bf1c9a8dd ("git-rebase: make --allow-empty-message the default", 2018-06-27) give heavy precedence to the assumption that git-rebase is often more happenstance than design when it comes to edge cases like these.

Quoted 8 lines

has distinct options for those two cases. If I've explicitly created an empty commit then I want to keep it but I don't want to keep commits that become empty because the changes they contain are already upstream.

If we want an option that keeps commits that become empty (Off hand I don't know why we would though) we should consider if that option should disable --cherry-mark when we create the todo list so that it keeps all commits that become empty when they're rebased.

To answer the particular reason I have seen why folks might want to keep both commits that start empty and become empty, but still use --cherry-mark to discard commits that are already upstream:
* If cherry-mark can determine the commit was "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).
* If a commit does not start empty, but becomes so because its
changes were a subset of those upstream, then while there are clearly no more changes that need to be applied, the commit messages can be expected to differ -- either there will be an upstream commit that included a bunch of other changes too, or the commit we are rebasing will be spread across multiple upstream commits. If the project has bad commit message hygiene (because e.g. they are using atrocious code review tools like GitHub that don't allow you to review them), there's a reasonable likelihood that the upstream version only talks about the other changes in the commit instead of the fix we are rebasing. If so, there may be useful information in this commit message that we want to copy somewhere.

It's a somewhat rare usecase involving projects that generally aren't careful with commit messages but which has some people working on it who are careful. But even for these projects, the odds that a commit message will just get tossed anyway seem fairly high, so I will not strongly defend this choice; it just seemed like "--keep-empty" or "--empty=keep" should mean "keep empty", not "sometimes keep empty". So, if it'd be nice if we could come up with clear wording if we are going to implement other possibilities.

Anyway, am I correctly understanding that you would like to see the following matrix of options?
* Drop commits that either start or become empty [the current --empty=drop]
* Pause and ask users whether they want commits that either start or
become empty [the current --empty=ask]
* Keep commits that start empty, but drop ones that become empty [a
new option]
and possibly drop the current --empty=keep behavior?

Quoted 10 lines
Quoted 7 lines

Note that traditionally, am-based rebases have always dropped commits that either started or became empty, while interactive-based rebases have defaulted to ask (and provided an option to keep commits that started empty). This difference made sense since users of an am-based rebase just wanted to quickly batch apply a sequence of commits, while users editing a todo list will likely want the chance to interact and handle unusual cases on a case-by-case basis.

I don't see why it makes sense to drop an empty commit that I've made just because it is being rebased.

Are you arguing that keeping commits that started empty should actually be the default, i.e. that all three rebase backends have traditionally had the wrong default?

Quoted 3 lines

I'm pretty sure the behavor of the
am-based rebase is a function of git am not being able to create empty commits.

That may be, but it's not quite clear. Junio kind of commented on this in https://lore.kernel.org/git/[email protected]/.
His comments that "lack of --keep-empty on the 'am' side is probably a bug that wants to be fixed" might imply that he only sees it as a useful option. If the behavior of the am-based rebase dropping commits which started empty was solely a function of git-am not being able to create empty commits, then the correct fix would be to make keeping the commits which started empty as the default or maybe only behavior rather than just making it an option.

I'd actually be open to changing the default here. We have a precedent in doing so with commit b00bf1c9a8dd ("git-rebase: make --allow-empty-message the default", 2018-06-27) where we stated that really rare edge cases (which I think both empty commit messages and commits which start empty qualify as) probably have the wrong default in rebase. And changing the default would yield a much easier to understand matrix of options:

* Drop commits that become empty (modification to --empty=drop in
that it keeps commits which started empty)
* Pause and ask users if commits become empty (modification to
--empty=ask in that it keeps commits which started empty)
* Keep commits that become empty, in addition to those that started
empty (--empty=keep).

Quoted 13 lines
Quoted 5 lines

However, not all rebases
using the interactive machinery are explicitly interactive anymore. In particular --merge was always meant to behave more like --am: just rebase a batch of commits without popping up a todo list.

Quoted 7 lines

If the --empty flag is not specified, pick defaults as follows:
explicitly interactive: ask
--exec: keep (exec is about checking existing commits, and often
used without actually changing the base. Thus the expectation is that the user doesn't necessarily want anything to change; they just want to test).
otherwise: drop

Quoted 4 lines

I'm not sure I like changing the behavior based on --exec, I see what you're getting at but it has the potential to be confusing. What if I want to rearrange the commits without changing the base - why must I specify --empty=keep there but not if I add --exec to the command line?

This was probably poorly explained and also poorly implemented. Let me try from a slightly different angle, and come to a slightly different result than I did previously:

Quoting from Junio in b00bf1c9a8dd ("git-rebase: make --allow-empty-message the default", 2018-06-27),
'"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'

I would modify Junio's wording to expand "am based rebase" to say that if the user hasn't requested an interactive rebase or otherwise explicitly signalled that they want some measure of interactivity (e.g. specified --empty=ask), then we should not stop and ask how to handle commits which started OR became empty. In other worse, it's not just the am-based rebase that is about transplanting commits but any rebase where interactivity isn't requested. I think the traditional --keep-empty is quite broken in this regard since it does stop and ask with commits that become empty. rebase -m, rebase --exec, etc. should not stop when a commit becomes empty, there should be some default it just uses unless the user also adds --interactive/-i/--empty=ask.

But, if we take your above suggestion to make keeping the commits which started empty not only the default but remove it from the options, then this becomes much easier. The choices become (kind of repeated from above):

[Keep the commits that started empty in all three cases, otherwise:]
* Drop commits that become empty (--empty=drop)
* Pause and ask users if commits become empty (--empty=ask)
* Keep commits that become empty (--empty=keep)

and the defaults are simply:
--interactive/-i: --empty=ask
everything else: --empty=drop

(And as a bonus, in the thread I've referenced twice above Junio mentioned -- in respect to commits that become empty -- that he thinks these are the right defaults. He didn't comment on the behavior of commits that start empty, which seems to be your primary concern.)

Quoted 11 lines
Quoted 10 lines

Also, this commit makes --keep-empty just imply --empty=keep, and hides it from help so that we aren't confusing users with different ways to do the same thing. (I could have added a --drop-empty flag, but then that invites users to specify both --keep-empty and --drop-empty and we have to add sanity checking around that; it seems cleaner to have a single multi-valued option.) This actually fixes --keep-empty too; previously, it only meant to sometimes keep empty commits, in particular commits which started empty would be kept. But it would still error out and ask the user what to do with commits that became empty. Now it keeps empty commits, as instructed.

Quoted 8 lines

It certainly changes the behavior of --keep-empty but I'm not sure it "fixes" it. If I have some empty commits I want to keep as placeholders then that's different from wanting to keep commits that become empty because their changes are upstream but --cherry-mark didn't detect them.

In summary I'm in favor of making it easier to drop commits that become empty but not tying that to the handling of commits that are empty before they are rebased.

Sounds like my modified proposal above may be in line with your tastes?

Quoted 2 lines

I'm also not happy that the deprecation of --keep-empty suddenly makes --no-keep-empty an error.

We could translate that to --empty=drop; does that sound reasonable?

[...]

Quoted 5 lines
Quoted 5 lines
builtin/rebase.c +0 -1
@@ -466,7 +494,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")),
Quoted 8 lines

builtin/rebase.c int cmd_rebase__interactive(int argc, const char **argv, const char *prefix) · line 499

Quoted 3 lines
builtin/rebase.c +3 -0
+             { OPTION_CALLBACK, 'k', "keep-empty", &options, NULL,
+                     N_("(DEPRECATED) keep empty commits"),
+                     PARSE_OPT_NOARG | PARSE_OPT_NONEG | PARSE_OPT_HIDDEN,

It is all very well deprecating --keep-empty but suddenly making '--no-keep-empty' an error goes beyond deprecation. Also I'm not sure it's worth changing these options as I think the only user is git-rebase--preserve-merges.sh

Side track: Since git-rebase--preserve-merges.sh is deprecated and we want to get rid of it, and rebase-merges exists and is a better implementation of the original idea, can we just translate rebase -p into rebase -r and delete git-rebase--preserve-merges.sh? (With a few wrinkles, such as telling users in the middle of an existing preserve-merges-rebase that they either need to use an old version of git to continue their rebase or else abort the rebase?)

Quoted 4 lines

Best Wishes

Phillip

[...]

Quoted 2 lines

This function is used by cherry-pick/revert not rebase do we need to change it?

Possibly not; I can take a look. I had quite a difficult time trying to figure out how to make sure that --empty options were saved and restored across rebase --continue; it's not at all unlikely that I modified more than I needed to "for consistency".

[...]

Quoted 3 lines

Review static int save_opts(struct replay_opts *opts)

Quoted 1 line
(quoted diff) +0 -0
@@ -3033,9 +3064,15 @@ static int save_opts(struct replay_opts *opts)

again this is for cherry-pick/revert

Okay, will double check this too.

[...]

#61 lore raw

Re: [PATCH v3 10/15] rebase: add an --am option

In reply to Phillip Wood

Hi Phillip,

Quoted 11 lines · Phillip Wood

Hi Elijah

Quoted 6 lines · Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

Currently, this option doesn't do anything except error out if any options requiring the interactive-backend are also passed. However, when we make the default backend configurable later in this series, this flag will provide a way to override the config setting.

Quoted 6 lines

I wonder if we really want to add a new option that we know we don't want to keep in the long term. Could we not just default to the interactive backend and fallback to the am backend if the user passes any of the am-based options on the commandline? i.e. reverse the current situation of defaulting to the am backend and selecting the interactive backend when the user passes certain options.

Good question. For context, this option served a few purposes:
* Providing an escape hatch during the transition in case the merge
backend doesn't handle everything the am one does in our first attempts (i.e. similar to how we allowed using the scripted rebase even after providing a built-in one).
* Makes the documentation much more concise and clear (being able to
specify that various flags "imply --am" is simple, whereas removing the flag makes me feel I need the full description of --am in each of the few places a flag that would have implied it).
* Provides an easy way to make the plethora of am-specific rebase
tests use the am-backend.

However, I thought of this option before Junio suggested a rebase.backend config setting, so we could just rely on that instead. Thus, getting rid of the "--am" flag in detail would mean:
* I need to redo the test changes in this series to use "-c
rebase.backend=am" instead of "--am"
* It will be slightly harder for users to use the escape hatch in
one-off cases during the transition
* We need to figure out the right way to reword the documentation

The first two are pretty minor, so that probably means I just need to come up with some good wording for the documentation (suggestions welcome...)

Elijah

#62 lore raw

Re: [PATCH v3 10/15] rebase: add an --am option

Quoted 9 lines · Elijah Newren

However, I thought of this option before Junio suggested a rebase.backend config setting, so we could just rely on that instead. Thus, getting rid of the "--am" flag in detail would mean:
* I need to redo the test changes in this series to use "-c
rebase.backend=am" instead of "--am"
* It will be slightly harder for users to use the escape hatch in
one-off cases during the transition
* We need to figure out the right way to reword the documentation

Quoted 3 lines

The first two are pretty minor, so that probably means I just need to come up with some good wording for the documentation (suggestions welcome...)

It probably is a good idea to keep --am (and possibly --[no-]am) as long as rebase.backend option exists. A configuration variable that changes behaviour without allowing it to be overridden from the command line is not a good idea.

#49 lore raw

Re: [PATCH v3 01/15] rebase: extend the options for handling of empty commits

In reply to Elijah Newren

Hi Elijah

Thanks for such a detailed reply, I'll try and do it justice.

TLDR I like your new proposal

Quoted 35 lines · Elijah Newren

Hi Phillip,

Quoted 6 lines · Phillip Wood

Hi Elijah

Thanks for working on this series, I think making the sequencer the default backend is a good idea. I have a few reservations about this path though...

Thanks for the feedback. You've provided some good food for thought. While responding, I came up with an alternate proposal below which I think may be more in line with what you want. Let me know...

Quoted 17 lines · Elijah Newren via GitGitGadget
Quoted 8 lines

From: Elijah Newren <[email protected]>

Extend the interactive machinery with the ability to handle the full spread of options for how to handle commits that either start or become empty (by "become empty" I mean the changes in a commit are a subset of changes that exist upstream, so the net effect of applying the commit is no changes). Introduce a new command line flag for selecting the desired behavior:

Quoted 6 lines
    --empty={drop,keep,ask}

with the definitions:
drop: drop empty commits
keep: keep empty commits
ask: provide the user a chance to interact and pick what to do with
empty commits on a case-by-case basis

Quoted 3 lines

I think we want to distinguish between commits that are empty before rebasing and those that become empty when they are rebased. --keep-empty explicily only applies to commits that are already empty. Cherry-pick

Quoted 6 lines

I am open to changing how the empty handling works; I figured it was the most likely area people might have feedback about.

However, I do strongly disagree with the "explicit" claim here. It is possible that --keep-empty was designed to only apply to commits that begin empty, but that is not at all explicit.

Maybe explicit wasn't the best choice of words. I was going off the implementation which takes pains to only allow and empty commit when the original was empty. This has been the case since the feature was
introduced in 90e1818f9a ("git-rebase: add keep_empty flag",
2012-04-20). I did some digging in the mail archive this morning and
found [1] which seems to confirm the intention was to preserve existing
empty commits, not keep those that become empty. This fits with the
discussion [2] which lead to this feature. The same patch series that
introduced --keep-empty for rebase introduced the two cherry-pick flags --allow-empty and --keep-redundant-commits to distinguish between the
two cases and used cherry-pick --allow-empty to implement rebase --keep-empty (and failed to provide a way to continue after stopping for a conflict resolution)

[1] https://lore.kernel.org/git/[email protected]/
[2]
https://lore.kernel.org/git/[email protected]/

Quoted 39 lines

The only place in the
documentation I could find that mentions the distinction at all was in the "Behavioral Differences" section, which was written by me as part of commit 0661e49aeb84 ("git-rebase.txt: document behavioral differences between modes", 2015-06-27). Quoting from that commit message:

"(In fact, it's not clear -- to me at least -- that these differences were even desirable or intentional.) Document these differences."

I cannot find anywhere else making a distinction in either the documentation or the commit messages or even any code comments, which has left me wondering whether the distinction was intentional at all. Things like commit b00bf1c9a8dd ("git-rebase: make --allow-empty-message the default", 2018-06-27) give heavy precedence to the assumption that git-rebase is often more happenstance than design when it comes to edge cases like these.

Quoted 8 lines

has distinct options for those two cases. If I've explicitly created an empty commit then I want to keep it but I don't want to keep commits that become empty because the changes they contain are already upstream.

If we want an option that keeps commits that become empty (Off hand I don't know why we would though) we should consider if that option should disable --cherry-mark when we create the todo list so that it keeps all commits that become empty when they're rebased.

To answer the particular reason I have seen why folks might want to keep both commits that start empty and become empty, but still use --cherry-mark to discard commits that are already upstream:
* If cherry-mark can determine the commit was "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).
* If a commit does not start empty, but becomes so because its
changes were a subset of those upstream, then while there are clearly no more changes that need to be applied, the commit messages can be expected to differ -- either there will be an upstream commit that

Quoted 8 lines

included a bunch of other changes too, or the commit we are rebasing will be spread across multiple upstream commits. If the project has bad commit message hygiene (because e.g. they are using atrocious code review tools like GitHub that don't allow you to review them), there's a reasonable likelihood that the upstream version only talks about the other changes in the commit instead of the fix we are rebasing. If so, there may be useful information in this commit message that we want to copy somewhere.

That sounds reasonable, (I'm not sure what one would do with the commit message though as creating an empty commit wont be any use for blame or someone looking at the commit that introduced the change upstream. One could use add a note to the upstream commit but then there's the problem of sharing notes)

Quoted 9 lines

It's a somewhat rare usecase involving projects that generally aren't careful with commit messages but which has some people working on it who are careful. But even for these projects, the odds that a commit message will just get tossed anyway seem fairly high, so I will not strongly defend this choice; it just seemed like "--keep-empty" or "--empty=keep" should mean "keep empty", not "sometimes keep empty". So, if it'd be nice if we could come up with clear wording if we are going to implement other possibilities.

Quoted 3 lines

Anyway, am I correctly understanding that you would like to see the following matrix of options?
* Drop commits that either start or become empty [the current --empty=drop]

Yes please

Quoted 2 lines

* Pause and ask users whether they want commits that either start or
become empty [the current --empty=ask]

I'd prefer an option to keep the commits that start empty and ask about the ones that become empty (just like --keep-empty does now)

Quoted 2 lines

* Keep commits that start empty, but drop ones that become empty [a
new option]

Yes please

Quoted 1 line

and possibly drop the current --empty=keep behavior?

Yes as I'm not sure it's that useful, if users want to keep the information from the message of a commit that becomes empty I'm not sure just creating an empty commit is going to be much use in practice.

Quoted 8 lines
Quoted 8 lines
Quoted 7 lines

Note that traditionally, am-based rebases have always dropped commits that either started or became empty, while interactive-based rebases have defaulted to ask (and provided an option to keep commits that started empty). This difference made sense since users of an am-based rebase just wanted to quickly batch apply a sequence of commits, while users editing a todo list will likely want the chance to interact and handle unusual cases on a case-by-case basis.

Quoted 6 lines
Quoted 2 lines

I don't see why it makes sense to drop an empty commit that I've made just because it is being rebased.

Are you arguing that keeping commits that started empty should actually be the default, i.e. that all three rebase backends have traditionally had the wrong default?

Yes, I think rebase should preserve the commits that are being rebased as far as possible and the current default is probably a historical accident

Quoted 4 lines
Quoted 3 lines

I'm pretty sure the behavor of the
am-based rebase is a function of git am not being able to create empty commits.

Quoted 9 lines

That may be, but it's not quite clear. Junio kind of commented on this in https://lore.kernel.org/git/[email protected]/.
His comments that "lack of --keep-empty on the 'am' side is probably a bug that wants to be fixed" might imply that he only sees it as a useful option. If the behavior of the am-based rebase dropping commits which started empty was solely a function of git-am not being able to create empty commits, then the correct fix would be to make keeping the commits which started empty as the default or maybe only behavior rather than just making it an option.

Yeah I'm not sure how Junio feels defaulting to keeping commits that start empty

Quoted 1 line

I'd actually be open to changing the default here.

I'd be keen to do that but we should try and find out what others think.

Quoted 8 lines

We have a
precedent in doing so with commit b00bf1c9a8dd ("git-rebase: make --allow-empty-message the default", 2018-06-27) where we stated that really rare edge cases (which I think both empty commit messages and commits which start empty qualify as) probably have the wrong default in rebase. And changing the default would yield a much easier to understand matrix of options:

Quoted 6 lines

* Drop commits that become empty (modification to --empty=drop in
that it keeps commits which started empty)
* Pause and ask users if commits become empty (modification to
--empty=ask in that it keeps commits which started empty)
* Keep commits that become empty, in addition to those that started
empty (--empty=keep).

That matrix is easier to understand and covers all the cases I'm interested in.

Quoted 48 lines
Quoted 13 lines
Quoted 5 lines

However, not all rebases
using the interactive machinery are explicitly interactive anymore. In particular --merge was always meant to behave more like --am: just rebase a batch of commits without popping up a todo list.

Quoted 7 lines

If the --empty flag is not specified, pick defaults as follows:
explicitly interactive: ask
--exec: keep (exec is about checking existing commits, and often
used without actually changing the base. Thus the expectation is that the user doesn't necessarily want anything to change; they just want to test).
otherwise: drop

Quoted 4 lines

I'm not sure I like changing the behavior based on --exec, I see what you're getting at but it has the potential to be confusing. What if I want to rearrange the commits without changing the base - why must I specify --empty=keep there but not if I add --exec to the command line?

This was probably poorly explained and also poorly implemented. Let me try from a slightly different angle, and come to a slightly different result than I did previously:

Quoting from Junio in b00bf1c9a8dd ("git-rebase: make --allow-empty-message the default", 2018-06-27),
'"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'

I would modify Junio's wording to expand "am based rebase" to say that if the user hasn't requested an interactive rebase or otherwise explicitly signalled that they want some measure of interactivity (e.g. specified --empty=ask), then we should not stop and ask how to handle commits which started OR became empty. In other worse, it's not just the am-based rebase that is about transplanting commits but any rebase where interactivity isn't requested. I think the traditional --keep-empty is quite broken in this regard since it does stop and ask with commits that become empty. rebase -m, rebase --exec, etc. should not stop when a commit becomes empty, there should be some default it just uses unless the user also adds --interactive/-i/--empty=ask.

But, if we take your above suggestion to make keeping the commits which started empty not only the default but remove it from the options, then this becomes much easier. The choices become (kind of repeated from above):

Quoted 4 lines

[Keep the commits that started empty in all three cases, otherwise:]
* Drop commits that become empty (--empty=drop)
* Pause and ask users if commits become empty (--empty=ask)
* Keep commits that become empty (--empty=keep)

That's nice - I like it

Quoted 3 lines

and the defaults are simply:

   --interactive/-i: --empty=ask
   everything else: --empty=drop

While I see the point about the difference between -i and the other rebase modes any flavor of rebase can stop at any time due to conflicts so it can never be fully automatic. Given that commits becoming empty is a bit of a corner case I'm not sure the complexity of different defaults is justified but I don't feel that strongly about it.

Quoted 21 lines

(And as a bonus, in the thread I've referenced twice above Junio mentioned -- in respect to commits that become empty -- that he thinks these are the right defaults. He didn't comment on the behavior of commits that start empty, which seems to be your primary concern.)

Quoted 16 lines
Quoted 10 lines

Also, this commit makes --keep-empty just imply --empty=keep, and hides it from help so that we aren't confusing users with different ways to do the same thing. (I could have added a --drop-empty flag, but then that invites users to specify both --keep-empty and --drop-empty and we have to add sanity checking around that; it seems cleaner to have a single multi-valued option.) This actually fixes --keep-empty too; previously, it only meant to sometimes keep empty commits, in particular commits which started empty would be kept. But it would still error out and ask the user what to do with commits that became empty. Now it keeps empty commits, as instructed.

It certainly changes the behavior of --keep-empty but I'm not sure it "fixes" it. If I have some empty commits I want to keep as placeholders then that's different from wanting to keep commits that become empty because their changes are upstream but --cherry-mark didn't detect them.

Quoted 5 lines
Quoted 3 lines

In summary I'm in favor of making it easier to drop commits that become empty but not tying that to the handling of commits that are empty before they are rebased.

Sounds like my modified proposal above may be in line with your tastes?

Yes. Keep commits that start empty and have an option to control what happens to those that become empty (defaulting to --empty=ask for interactive rebases at least).

Quoted 4 lines
Quoted 2 lines

I'm also not happy that the deprecation of --keep-empty suddenly makes --no-keep-empty an error.

We could translate that to --empty=drop; does that sound reasonable?

translating it to --empty=ask would keep the behavior the same even if it sounds a bit odd. I don't think we want to change the behavior of --[no-]keep-empty while it is deprecated.

Quoted 16 lines

[...]

Quoted 5 lines
Quoted 5 lines
builtin/rebase.c +0 -1
@@ -466,7 +494,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")),
Quoted 8 lines

builtin/rebase.c int cmd_rebase__interactive(int argc, const char **argv, const char *prefix) · line 499

Quoted 3 lines
builtin/rebase.c +3 -0
+             { OPTION_CALLBACK, 'k', "keep-empty", &options, NULL,
+                     N_("(DEPRECATED) keep empty commits"),
+                     PARSE_OPT_NOARG | PARSE_OPT_NONEG | PARSE_OPT_HIDDEN,

It is all very well deprecating --keep-empty but suddenly making '--no-keep-empty' an error goes beyond deprecation. Also I'm not sure it's worth changing these options as I think the only user is git-rebase--preserve-merges.sh

Quoted 7 lines

Side track: Since git-rebase--preserve-merges.sh is deprecated and we want to get rid of it, and rebase-merges exists and is a better implementation of the original idea, can we just translate rebase -p into rebase -r and delete git-rebase--preserve-merges.sh? (With a few wrinkles, such as telling users in the middle of an existing preserve-merges-rebase that they either need to use an old version of git to continue their rebase or else abort the rebase?)

dscho posted some patches in November or December moving along this path, I'm not sure what the outcome was. It's a bit complicated by the different todo list formats between -p and -r but I think the end game should be to treat -p as -r as you suggest.

Quoted 23 lines
Quoted 5 lines

Best Wishes

Phillip

[...]

Quoted 2 lines

This function is used by cherry-pick/revert not rebase do we need to change it?

Possibly not; I can take a look. I had quite a difficult time trying to figure out how to make sure that --empty options were saved and restored across rebase --continue; it's not at all unlikely that I modified more than I needed to "for consistency".

[...]

Quoted 3 lines

Review static int save_opts(struct replay_opts *opts)

Quoted 1 line
(quoted diff) +0 -0
@@ -3033,9 +3064,15 @@ static int save_opts(struct replay_opts *opts)

again this is for cherry-pick/revert

Okay, will double check this too.

[...]

#63 lore raw

Re: [PATCH v3 10/15] rebase: add an --am option

In reply to Junio C Hamano
Quoted 11 lines · Junio C Hamano
Quoted 9 lines · Elijah Newren

However, I thought of this option before Junio suggested a rebase.backend config setting, so we could just rely on that instead. Thus, getting rid of the "--am" flag in detail would mean:
* I need to redo the test changes in this series to use "-c
rebase.backend=am" instead of "--am"
* It will be slightly harder for users to use the escape hatch in
one-off cases during the transition
* We need to figure out the right way to reword the documentation

Quoted 8 lines
Quoted 3 lines

The first two are pretty minor, so that probably means I just need to come up with some good wording for the documentation (suggestions welcome...)

It probably is a good idea to keep --am (and possibly --[no-]am) as long as rebase.backend option exists. A configuration variable that changes behaviour without allowing it to be overridden from the command line is not a good idea.

I view this change in the default backend as similar to the rewrite in C in that it is an implementation detail we're changing that should be transparent (and beneficial in terms of performance) to the user. There we provided a configuration variable but not a command line option to control if it was used or not. I don't see it as something the user will want to change from rebase to rebase but perhaps I'm missing something.

Best Wishes

Phillip

#64 lore raw

Re: [PATCH v3 10/15] rebase: add an --am option

Quoted 5 lines · Phillip Wood

I view this change in the default backend as similar to the rewrite in C in that it is an implementation detail we're changing that should be transparent (and beneficial in terms of performance) to the user. There we provided a configuration variable but not a command line option to control if it was used or not.

Do you mean things like GIT_TEST_ADD_I_USE_BUILTIN? I think it is OK to have such an escape mechanism to allow people to opt out of a new reimplementation until it matures, but I do not offhand recall doing so with config.

#65 lore raw

Re: [PATCH v3 10/15] rebase: add an --am option

Quoted 8 lines · Junio C Hamano
Quoted 5 lines · Phillip Wood

I view this change in the default backend as similar to the rewrite in C in that it is an implementation detail we're changing that should be transparent (and beneficial in terms of performance) to the user. There we provided a configuration variable but not a command line option to control if it was used or not.

Quoted 4 lines

Do you mean things like GIT_TEST_ADD_I_USE_BUILTIN? I think it is OK to have such an escape mechanism to allow people to opt out of a new reimplementation until it matures, but I do not offhand recall doing so with config.

I was thinking of rebase.usebuiltin

Best Wishes

Phillip

#50 lore raw

Re: [PATCH v3 01/15] rebase: extend the options for handling of empty commits

In reply to Phillip Wood

Hi Phillip,

Quoted 33 lines · Phillip Wood
Quoted 24 lines · Elijah Newren
Quoted 2 lines · Phillip Wood

On 24/12/2019 19:54, Elijah Newren via GitGitGadget wrote:

[...]

Quoted 8 lines
Quoted 8 lines
builtin/rebase.c +1 -1
@@ -466,7 +494,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,
Quoted 8 lines

Review int cmd_rebase__interactive(int argc, const char · line 502

Quoted 3 lines
(quoted diff) +2 -0
+                     N_("(DEPRECATED) keep empty commits"),
+                     PARSE_OPT_NOARG | PARSE_OPT_NONEG |
 PARSE_OPT_HIDDEN,

It is all very well deprecating --keep-empty but suddenly making '--no-keep-empty' an error goes beyond deprecation. Also I'm not sure it's worth changing these options as I think the only user is git-rebase--preserve-merges.sh

Quoted 7 lines

Side track: Since git-rebase--preserve-merges.sh is deprecated and we want to get rid of it, and rebase-merges exists and is a better implementation of the original idea, can we just translate rebase -p into rebase -r and delete git-rebase--preserve-merges.sh? (With a few wrinkles, such as telling users in the middle of an existing preserve-merges-rebase that they either need to use an old version of git to continue their rebase or else abort the rebase?)

Quoted 4 lines

dscho posted some patches in November or December moving along this path, I'm not sure what the outcome was. It's a bit complicated by the different todo list formats between -p and -r but I think the end game should be to treat -p as -r as you suggest.

I sent some patches to switch git svn over to rebase -r, but that was only in non-interactive mode.

In interactive mode, the todo lists indeed look too different and I think we'll just need to wait a year or so and then we can just delete git-rebase--preserve-merges.sh.

Ciao,
Dscho

#71 lore raw

Re: [PATCH v3 15/15] rebase: change the default backend from "am" to "merge"

In reply to Elijah Newren via GitGitGadget

Hi,

Quoted 22 lines · Elijah Newren via GitGitGadget

The am-backend drops information and thus limits what we can do:

* lack of full tree information from the original commits means we cannot do directory rename detection and warn users that they might want to move some of their new files that they placed in old directories to prevent their becoming orphaned.[1]
* reduction in context from only having a few lines beyond those changed means that when context lines are non-unique we can apply patches incorrectly.[2]
* lack of access to original commits means that conflict marker annotation has less information available.

Also, the merge/interactive backend have far more abilities, appear to currently have a slight performance advantage[3] and have room for more optimizations than the am backend[4] (and work is underway to take advantage of some of those possibilities).

[1] https://lore.kernel.org/git/[email protected]/
[2] https://lore.kernel.org/git/CABPp-BGiu2nVMQY_t-rnFR5GQUz_ipyEE8oDocKeO+h+t4Mn4A@mail.gmail.com/
[3] https://public-inbox.org/git/CABPp-BF=ev03WgODk6TMQmuNoatg2kiEe5DR__gJ0OTVqHSnfQ@mail.gmail.com/
[4] https://lore.kernel.org/git/CABPp-BGh7yW69QwxQb13K0HM38NKmQif3A6C6UULEKYnkEJ5vA@mail.gmail.com/

Quoted 7 lines
---
 Documentation/git-rebase.txt           |  2 +-
 builtin/rebase.c                       |  4 ++--
 t/t5520-pull.sh                        | 10 ++++++----
 t/t9106-git-svn-commit-diff-clobber.sh |  3 ++-
 4 files changed, 11 insertions(+), 8 deletions(-)

Thanks for writing this. We finally rolled this out to our internal population at $DAYJOB and ran into a couple of issues:

1. "git rebase --am" does not invoke the post-commit hook, but "git rebase --merge" does. Is this behavior change intended?

Noticed because jiri[1] installs a post-commit hook that warns about commits on detached HEAD, so this change makes rebases more noisy in repositories that were set up using jiri.

2. GIT_REFLOG_ACTION contains "rebase -i" even though the rebase is not interactive.

3. In circumstances I haven't pinned down yet, we get the error

    message "invalid date format: @@2592000 +0000":

	$ git rebase --committer-date-is-author-date --onto branch_K branch_L~1 branch_L
	$ git checkout --theirs file
	$ git add file
	$ git rebase --continue
	fatal: invalid date format: @@2592000 +0000
	error: could not commit staged changes.

    This isn't reproducible without --committer-date-is-author-date.
    More context (the test where it happens) is in [2].

4. I suspect the exit status in the "you need to resolve conflicts" case has changed. With rebase --am, [3] would automatically invoke rebase --abort when conflicts are present, but with rebase --merge it does not.

Known?

Thanks,
Jonathan

[1] https://fuchsia.googlesource.com/jiri/+/60436c301224231cb99be41ce937dfc223bee272/project/manifest.go#1347
[2] https://source.chromium.org/chromium/chromium/tools/depot_tools/+/master:tests/git_common_test.py;l=721;drc=6b52dc21e166c46707b4c8eb26c74c70d4f9977e;bpv=1;bpt=0
[3] https://fuchsia.googlesource.com/jiri/+/60436c301224231cb99be41ce937dfc223bee272/project/project.go#1664

#72 lore raw

Re: [PATCH v3 15/15] rebase: change the default backend from "am" to "merge"

Hi Jonathan,

Quoted 35 lines · Jonathan Nieder

Hi,

Quoted 22 lines · Elijah Newren via GitGitGadget

The am-backend drops information and thus limits what we can do:

* lack of full tree information from the original commits means we cannot do directory rename detection and warn users that they might want to move some of their new files that they placed in old directories to prevent their becoming orphaned.[1]
* reduction in context from only having a few lines beyond those changed means that when context lines are non-unique we can apply patches incorrectly.[2]
* lack of access to original commits means that conflict marker annotation has less information available.

Also, the merge/interactive backend have far more abilities, appear to currently have a slight performance advantage[3] and have room for more optimizations than the am backend[4] (and work is underway to take advantage of some of those possibilities).

[1] https://lore.kernel.org/git/[email protected]/
[2] https://lore.kernel.org/git/CABPp-BGiu2nVMQY_t-rnFR5GQUz_ipyEE8oDocKeO+h+t4Mn4A@mail.gmail.com/
[3] https://public-inbox.org/git/CABPp-BF=ev03WgODk6TMQmuNoatg2kiEe5DR__gJ0OTVqHSnfQ@mail.gmail.com/
[4] https://lore.kernel.org/git/CABPp-BGh7yW69QwxQb13K0HM38NKmQif3A6C6UULEKYnkEJ5vA@mail.gmail.com/

Quoted 7 lines
---
 Documentation/git-rebase.txt           |  2 +-
 builtin/rebase.c                       |  4 ++--
 t/t5520-pull.sh                        | 10 ++++++----
 t/t9106-git-svn-commit-diff-clobber.sh |  3 ++-
 4 files changed, 11 insertions(+), 8 deletions(-)
Quoted 2 lines

Thanks for writing this. We finally rolled this out to our internal population at $DAYJOB and ran into a couple of issues:

Cool, thanks for testing it out.

Quoted 6 lines

1. "git rebase --am" does not invoke the post-commit hook, but "git rebase --merge" does. Is this behavior change intended?

Noticed because jiri[1] installs a post-commit hook that warns about commits on detached HEAD, so this change makes rebases more noisy in repositories that were set up using jiri.

I've never used a post-commit hook or seen one in the wild. Certainly wasn't intentional, but it's not clear to me if it's wrong or right either. I don't see why it would make sense to distinguish between any of git rebase --am/--merge/--interactive, but it isn't too surprising that by historical accident the two rebase backends which happened to call git-commit behind the scenes would call a post-commit hook and the other rebase backend that didn't call git-commit wouldn't.

But the big question here, is what is correct behavior? Should rebase call the post-commit hook, or should it skip it? I haven't any clue what the answer to that is.

Quoted 2 lines

2. GIT_REFLOG_ACTION contains "rebase -i" even though the rebase is not interactive.

Yep, as does --keep, --exec, --rebase-merges, etc. There are lots of rebases which use the interactive machinery even if they aren't explicitly interactive. I've never seen the "-i" in the reflog message defined, but clearly it has always been used whenever the interactive machinery was in play regardless of whether the rebase was interactive. In that regard, I figured that --merge fit in rather nicely. (And I noted the fact that reflog messages were different between the backends among the "BEHAVIORAL DIFFERENCES" section of git-rebase.txt). But if others think we should just drop the -i (much as we did for the bash prompt), I'd be happy with that too. If we go that route, I think I'd rather drop the -i in the reflog for all rebases, not just the using-the-interactive-machinery-but-not-explicitly-interactive ones.

Quoted 12 lines

3. In circumstances I haven't pinned down yet, we get the error

    message "invalid date format: @@2592000 +0000":

        $ git rebase --committer-date-is-author-date --onto branch_K branch_L~1 branch_L
        $ git checkout --theirs file
        $ git add file
        $ git rebase --continue
        fatal: invalid date format: @@2592000 +0000
        error: could not commit staged changes.

    This isn't reproducible without --committer-date-is-author-date.
    More context (the test where it happens) is in [2].

Interesting. Do you happen to know if this started happening with ra/rebase-i-more-options, or did it just become an issue with en/rebase-backend? I looked around at the link you provided and feel a bit confused; I'm not sure which test does this or how I'd reproduce.

Quoted 6 lines

4. I suspect the exit status in the "you need to resolve conflicts" case has changed. With rebase --am, [3] would automatically invoke rebase --abort when conflicts are present, but with rebase --merge it does not.

Known?

Nope, but I would certainly hope that "you need to resolve conflicts" would result in a non-zero exit status. If it doesn't, that sounds like a bug in the interactive backend that we need to fix. I'll dig in.

Thanks for the reports!
Elijah

#73 lore raw

Re: [PATCH v3 15/15] rebase: change the default backend from "am" to "merge"

Hi Jonathan & Elijah

Quoted 38 lines · Elijah Newren

Hi Jonathan,

Quoted 35 lines · Jonathan Nieder

Hi,

Quoted 29 lines · Elijah Newren via GitGitGadget

The am-backend drops information and thus limits what we can do:

* lack of full tree information from the original commits means we cannot do directory rename detection and warn users that they might want to move some of their new files that they placed in old directories to prevent their becoming orphaned.[1]
* reduction in context from only having a few lines beyond those changed means that when context lines are non-unique we can apply patches incorrectly.[2]
* lack of access to original commits means that conflict marker annotation has less information available.

Also, the merge/interactive backend have far more abilities, appear to currently have a slight performance advantage[3] and have room for more optimizations than the am backend[4] (and work is underway to take advantage of some of those possibilities).

[1] https://lore.kernel.org/git/[email protected]/
[2] https://lore.kernel.org/git/CABPp-BGiu2nVMQY_t-rnFR5GQUz_ipyEE8oDocKeO+h+t4Mn4A@mail.gmail.com/
[3] https://public-inbox.org/git/CABPp-BF=ev03WgODk6TMQmuNoatg2kiEe5DR__gJ0OTVqHSnfQ@mail.gmail.com/
[4] https://lore.kernel.org/git/CABPp-BGh7yW69QwxQb13K0HM38NKmQif3A6C6UULEKYnkEJ5vA@mail.gmail.com/

---
  Documentation/git-rebase.txt           |  2 +-
  builtin/rebase.c                       |  4 ++--
  t/t5520-pull.sh                        | 10 ++++++----
  t/t9106-git-svn-commit-diff-clobber.sh |  3 ++-
  4 files changed, 11 insertions(+), 8 deletions(-)
Quoted 11 lines
Quoted 2 lines

Thanks for writing this. We finally rolled this out to our internal population at $DAYJOB and ran into a couple of issues:

Cool, thanks for testing it out.

Quoted 6 lines

1. "git rebase --am" does not invoke the post-commit hook, but "git rebase --merge" does. Is this behavior change intended?

Noticed because jiri[1] installs a post-commit hook that warns about commits on detached HEAD, so this change makes rebases more noisy in repositories that were set up using jiri.

Perhaps that hook could learn not to warn if a branch is being rebased? git could be more helpful there by having a porcelain option to status that prints the branch name if we're rebasing (git worktree --list shows the branch correctly when it's being rebased but does not (yet - I have a patch to do it) mark the current worktree so isn't very helpful.)

Quoted 8 lines

I've never used a post-commit hook or seen one in the wild. Certainly wasn't intentional, but it's not clear to me if it's wrong or right either. I don't see why it would make sense to distinguish between any of git rebase --am/--merge/--interactive, but it isn't too surprising that by historical accident the two rebase backends which happened to call git-commit behind the scenes would call a post-commit hook and the other rebase backend that didn't call git-commit wouldn't.

Looking through the history the am based rebase has never run the post-commit hook as am has its own set of hooks and the scripted version used commit-tree. The merge based rebase ran git commit which ran the post commit hook. The interactive rebase ran the hook until and I broke it in a356ee4659b ("sequencer: try to commit without forking 'git commit'", 2017-11-24) and after I fixed it in 4627bc777e ("sequencer: run post-commit hook", 2019-10-15). As it was broken for two years with no one noticing it can't be that popular.

Quoted 3 lines

But the big question here, is what is correct behavior? Should rebase call the post-commit hook, or should it skip it? I haven't any clue what the answer to that is.

It's creating a new commit so I lean towards thinking it should run the post-commit hook. As an example I have a post-commit hook that prints a warning if a commit is created on a branch that is being rewritten by one of my scripts in another worktree. There are pre-commit and pre-rebase hooks to try and prevent that, but the warning is there as a last resort if those hooks are by-passed.

Quoted 2 lines
Quoted 2 lines

2. GIT_REFLOG_ACTION contains "rebase -i" even though the rebase is not interactive.

If this is important to people I think it should be easy enough to set GIT_REFLOG_ACTION to the appropriate string in builtin/rebase.c (so long as it hasn't already been set by the user) rather than relying on sequencer.c to do it.

Quoted 24 lines

Yep, as does --keep, --exec, --rebase-merges, etc. There are lots of rebases which use the interactive machinery even if they aren't explicitly interactive. I've never seen the "-i" in the reflog message defined, but clearly it has always been used whenever the interactive machinery was in play regardless of whether the rebase was interactive. In that regard, I figured that --merge fit in rather nicely. (And I noted the fact that reflog messages were different between the backends among the "BEHAVIORAL DIFFERENCES" section of git-rebase.txt). But if others think we should just drop the -i (much as we did for the bash prompt), I'd be happy with that too. If we go that route, I think I'd rather drop the -i in the reflog for all rebases, not just the using-the-interactive-machinery-but-not-explicitly-interactive ones.

Quoted 10 lines

3. In circumstances I haven't pinned down yet, we get the error

     message "invalid date format: @@2592000 +0000":

         $ git rebase --committer-date-is-author-date --onto branch_K branch_L~1 branch_L
         $ git checkout --theirs file
         $ git add file
         $ git rebase --continue
         fatal: invalid date format: @@2592000 +0000
         error: could not commit staged changes.
Quoted 8 lines
Quoted 2 lines

This isn't reproducible without --committer-date-is-author-date. More context (the test where it happens) is in [2].

Interesting. Do you happen to know if this started happening with ra/rebase-i-more-options, or did it just become an issue with en/rebase-backend? I looked around at the link you provided and feel a bit confused; I'm not sure which test does this or how I'd reproduce.

I'm confused by the test as well. As ra/rebase-i-more-options only touched the sequencer then any bugs would only show up in this test (which runs a non-interactive rebase) once en/rbease-backend switched to that backend. It seems likely that ra/rebase-i-more-options is to blame.

Jonathan - do you happen to know if your users create empty commits at all? and if so what do they expect rebase to do with them (and any that become empty when they are rebased) - cf https://lore.kernel.org/git/<CABPp-BEH=9qejeqysHYE+AJ+JPaBympZizq-bx_OjArYFa4xUQ@mail.gmail.com>

Best Wishes

Phillip

Quoted 17 lines
Quoted 6 lines

4. I suspect the exit status in the "you need to resolve conflicts" case has changed. With rebase --am, [3] would automatically invoke rebase --abort when conflicts are present, but with rebase --merge it does not.

Known?

Nope, but I would certainly hope that "you need to resolve conflicts" would result in a non-zero exit status. If it doesn't, that sounds like a bug in the interactive backend that we need to fix. I'll dig in.

Thanks for the reports!
Elijah

#74 lore raw

Re: [PATCH v3 15/15] rebase: change the default backend from "am" to "merge"

Hi,

Quoted 28 lines · Phillip Wood
Quoted 11 lines · Elijah Newren
Quoted 9 lines · Jonathan Nieder

Elijah Newren via GitGitGadget wrote:

1. "git rebase --am" does not invoke the post-commit hook, but "git rebase --merge" does. Is this behavior change intended?

Noticed because jiri[1] installs a post-commit hook that warns about commits on detached HEAD, so this change makes rebases more noisy in repositories that were set up using jiri.

Perhaps that hook could learn not to warn if a branch is being rebased? git could be more helpful there by having a porcelain option to status that prints the branch name if we're rebasing (git worktree --list shows the branch correctly when it's being rebased but does not (yet - I have a patch to do it) mark the current worktree so isn't very helpful.)

Quoted 8 lines

I've never used a post-commit hook or seen one in the wild. Certainly wasn't intentional, but it's not clear to me if it's wrong or right either. I don't see why it would make sense to distinguish between any of git rebase --am/--merge/--interactive, but it isn't too surprising that by historical accident the two rebase backends which happened to call git-commit behind the scenes would call a post-commit hook and the other rebase backend that didn't call git-commit wouldn't.

Quoted 8 lines

Looking through the history the am based rebase has never run the post-commit hook as am has its own set of hooks and the scripted version used commit-tree. The merge based rebase ran git commit which ran the post commit hook. The interactive rebase ran the hook until and I broke it in a356ee4659b ("sequencer: try to commit without forking 'git commit'", 2017-11-24) and after I fixed it in 4627bc777e ("sequencer: run post-commit hook", 2019-10-15). As it was broken for two years with no one noticing it can't be that popular.

Maybe a crazy idea, but maybe not: how about running the post-commit hook _only_ if --merge was specified explicitly, and in that case (and guarded behind a check verifying that the post-commit hook _actually_ exists _and_ is executable) warn the user that this hook won't be run in future versions?

To make things better for users who actually want to run that hook during rebases, we could introduce a config option, say, rebase.runPostCommitHook that is a tri-state (true, false, onlyForDashDashMerge, at first defaulting to the last, eventually to false).

Crazy? Or helpful?

Quoted 10 lines
Quoted 3 lines

But the big question here, is what is correct behavior? Should rebase call the post-commit hook, or should it skip it? I haven't any clue what the answer to that is.

It's creating a new commit so I lean towards thinking it should run the post-commit hook. As an example I have a post-commit hook that prints a warning if a commit is created on a branch that is being rewritten by one of my scripts in another worktree. There are pre-commit and pre-rebase hooks to try and prevent that, but the warning is there as a last resort if those hooks are by-passed.

I guess you're right, it is quite surprising that the post-commit hook is _not_ run for --am rebases even though commits are created.

Quoted 7 lines
Quoted 2 lines
Quoted 2 lines

2. GIT_REFLOG_ACTION contains "rebase -i" even though the rebase is not interactive.

If this is important to people I think it should be easy enough to set GIT_REFLOG_ACTION to the appropriate string in builtin/rebase.c (so long as it hasn't already been set by the user) rather than relying on sequencer.c to do it.

I agree (but won't have time to implement it, so maybe I should shut up already...)

Quoted 39 lines
Quoted 25 lines

Yep, as does --keep, --exec, --rebase-merges, etc. There are lots of rebases which use the interactive machinery even if they aren't explicitly interactive. I've never seen the "-i" in the reflog message defined, but clearly it has always been used whenever the interactive machinery was in play regardless of whether the rebase was interactive. In that regard, I figured that --merge fit in rather nicely. (And I noted the fact that reflog messages were different between the backends among the "BEHAVIORAL DIFFERENCES" section of git-rebase.txt). But if others think we should just drop the -i (much as we did for the bash prompt), I'd be happy with that too. If we go that route, I think I'd rather drop the -i in the reflog for all rebases, not just the using-the-interactive-machinery-but-not-explicitly-interactive ones.

Quoted 11 lines

3. In circumstances I haven't pinned down yet, we get the error

     message "invalid date format: @@2592000 +0000":

         $ git rebase --committer-date-is-author-date --onto branch_K
         branch_L~1 branch_L
         $ git checkout --theirs file
         $ git add file
         $ git rebase --continue
         fatal: invalid date format: @@2592000 +0000
         error: could not commit staged changes.
Quoted 8 lines
Quoted 2 lines

This isn't reproducible without --committer-date-is-author-date. More context (the test where it happens) is in [2].

Interesting. Do you happen to know if this started happening with ra/rebase-i-more-options, or did it just become an issue with en/rebase-backend? I looked around at the link you provided and feel a bit confused; I'm not sure which test does this or how I'd reproduce.

I'm confused by the test as well. As ra/rebase-i-more-options only touched the sequencer then any bugs would only show up in this test (which runs a non-interactive rebase) once en/rbease-backend switched to that backend. It seems likely that ra/rebase-i-more-options is to blame.

Quoted 4 lines

Jonathan - do you happen to know if your users create empty commits at all? and if so what do they expect rebase to do with them (and any that become empty when they are rebased) - cf
https://lore.kernel.org/git/<CABPp-BEH=9qejeqysHYE+AJ+JPaBympZizq-bx_OjArYFa4xUQ@mail.gmail.com>

The double @ looks very funny. I would be interested in seeing an MCVE.

Quoted 11 lines
Quoted 11 lines
Quoted 6 lines

4. I suspect the exit status in the "you need to resolve conflicts" case has changed. With rebase --am, [3] would automatically invoke rebase --abort when conflicts are present, but with rebase --merge it does not.

Known?

Nope, but I would certainly hope that "you need to resolve conflicts" would result in a non-zero exit status. If it doesn't, that sounds like a bug in the interactive backend that we need to fix. I'll dig in.

Yes, exiting with status 0 would be a major bug, and I think it might even be a bug that was introduced by me when I re-implemented the core loop of the interactive rebase in C.

But to me it sounds as if 4. is not so about the exit code but about aborting immediately. I do not recall seeing --am rebases to abort, though, but to exit with error (and I saw the same behavior in interactive rebases).

We will need to see a reduced concrete example (preferably as a new test case) of the described behavior.

Ciao,
Dscho

#88 lore raw

Re: [PATCH v3 15/15] rebase: change the default backend from "am" to "merge"

In reply to Phillip Wood
Quoted 10 lines · Phillip Wood

Looking through the history the am based rebase has never run the post-commit hook as am has its own set of hooks and the scripted version used commit-tree.

Quoted 3 lines

But the big question here, is what is correct behavior? Should rebase call the post-commit hook, or should it skip it? I haven't any clue what the answer to that is.

It's creating a new commit so I lean towards thinking it should run the post-commit hook. As an example I have a post-commit hook that

The reason why "am" did not run "post-commit" is because "post-commit" was (originally) meant to be for cases where the end user types "git commit", IOW, the notion of "since we are creating a new commit object, let's run the post-commit hook" used to be totally misguided way of thinking. The hook was "the user _ran_ 'commit', so post-commit is run immediately afterwards".

These days, I think most of our tools subscribe to the newer "a commit got created--have hook the chance to report it" idea, simply because the original position is untenable---"git merge" may not want to run the post-commit hook according to the initial design, and it is fine to make it not to run it when clean automerge was recorded, but sometimes the end users have to type "git commit" to conclude a conflicted merge.

So from that point of view, I do not think it is too bad to bring "am" into the new ear and start running post-commit from it.

The "pre-commit" hook is a different story, though. From purely practical point of view, starting to run extra verification that may veto new commits from getting created only because we changed the implementation detail for some reason is a disservice to the users.

#89 lore raw

Re: [PATCH v3 15/15] rebase: change the default backend from "am" to "merge"

In reply to Phillip Wood

Hi,

Quoted 6 lines · Phillip Wood
Quoted 3 lines · Elijah Newren

But the big question here, is what is correct behavior? Should rebase call the post-commit hook, or should it skip it? I haven't any clue what the answer to that is.

Quoted 6 lines

It's creating a new commit so I lean towards thinking it should run the post-commit hook. As an example I have a post-commit hook that prints a warning if a commit is created on a branch that is being rewritten by one of my scripts in another worktree. There are pre-commit and pre-rebase hooks to try and prevent that, but the warning is there as a last resort if those hooks are by-passed.

The initial design for the post-commit hook was

commit 89e2c5f17b901edf28a8bb778ece3f6c18bbb721
Author: Junio C Hamano <[email protected]>
Date: Thu Aug 18 17:20:08 2005 -0700

Add commit hook and make the verification customizable.

There are three hooks:

- 'pre-commit' is given an opportunity to inspect what is being committed, before we invoke the EDITOR for the commit message;

- 'commit-msg' is invoked on the commit log message after the user prepares it;

- 'post-commit' is run after a successful commit is made.

The first two can interfere to stop the commit. The last one is for after-the-fact notification.

The initial implementation was only in "git commit" and didn't affect other commands, but that's an artifact of implementation, not intention. The intention is more murky: certainly "creating a new commit" is not the event we want to notify about (for example, "git commit-tree" should not invoke the hook), but if I were designing it today then all operations that create a new commit and update the current branch with it might qualify.

Even that definition is a bit fuzzy: when I run "git rebase <upstream> <branch>", am I updating <branch> or the current branch? Are cherry-picks that carry over changes that were committed previously new commits?

Worse, we have the body of existing post-commit hooks to contend with, and each one may have made different assumptions about the semantics. We do not have the luxury of designing the hook without regard to that body of existing hooks today.

The hook that jiri installs is very simple. It wants to check when a user has (interactively) made a commit on a detached HEAD, to let them know that they might want to use "git checkout -b" afterward. With this particular hook, the behavior is better when git rebase does not invoke the hook, since in the context of a rebase, the user has no need to use "git checkout -b" afterward. This is a regression, and I think we need to take that seriously.

It doesn't seem like jiri is doing anything weird here, so there's a reasonably high probability that other hooks would be affected in the same way. How can we help authors of such hooks to handle this change?

I don't think that starting to invoke the 'post-commit' hook even more without going through that thought process is an acceptable fix. Since the "right" semantics aren't obvious here, the first step is probably to collect some typical examples of post-commit hooks, like the example from the message I'm replying to (thank you!). Ideas for where we can find more?

Thanks,
Jonathan

#90 lore raw

Re: [PATCH v3 15/15] rebase: change the default backend from "am" to "merge"

Quoted 51 lines · Jonathan Nieder

Hi,

Quoted 6 lines · Phillip Wood
Quoted 3 lines · Elijah Newren

But the big question here, is what is correct behavior? Should rebase call the post-commit hook, or should it skip it? I haven't any clue what the answer to that is.

Quoted 6 lines

It's creating a new commit so I lean towards thinking it should run the post-commit hook. As an example I have a post-commit hook that prints a warning if a commit is created on a branch that is being rewritten by one of my scripts in another worktree. There are pre-commit and pre-rebase hooks to try and prevent that, but the warning is there as a last resort if those hooks are by-passed.

The initial design for the post-commit hook was

commit 89e2c5f17b901edf28a8bb778ece3f6c18bbb721
Author: Junio C Hamano <[email protected]>
Date: Thu Aug 18 17:20:08 2005 -0700

Add commit hook and make the verification customizable.

There are three hooks:

- 'pre-commit' is given an opportunity to inspect what is being committed, before we invoke the EDITOR for the commit message;

- 'commit-msg' is invoked on the commit log message after the user prepares it;

- 'post-commit' is run after a successful commit is made.

The first two can interfere to stop the commit. The last one is for after-the-fact notification.

The initial implementation was only in "git commit" and didn't affect other commands, but that's an artifact of implementation, not intention. The intention is more murky: certainly "creating a new commit" is not the event we want to notify about (for example, "git commit-tree" should not invoke the hook), but if I were designing it today then all operations that create a new commit and update the current branch with it might qualify.

Even that definition is a bit fuzzy: when I run "git rebase <upstream> <branch>", am I updating <branch> or the current branch? Are cherry-picks that carry over changes that were committed previously new commits?

Quoted 4 lines

Worse, we have the body of existing post-commit hooks to contend with, and each one may have made different assumptions about the semantics. We do not have the luxury of designing the hook without regard to that body of existing hooks today.

This seems like the most important point to me. I took a very cursory peek at some search results on Github
(https://github.com/search?l=&q=filename%3A%2Apost-commit%2A&type=Code)
and I see mostly shims - make sure certain files were included in the commit, bring a worktree up to date, stage stuff that wasn't committed - but again, that's a very brief glance at over 19000 probable post-commit hooks. Of course it's not possible for us to examine all of them, but having these 19k scripts being run lots of times when they didn't used to during an operation as common as rebase seems a little scary.

Quoted 8 lines

The hook that jiri installs is very simple. It wants to check when a user has (interactively) made a commit on a detached HEAD, to let them know that they might want to use "git checkout -b" afterward. With this particular hook, the behavior is better when git rebase does not invoke the hook, since in the context of a rebase, the user has no need to use "git checkout -b" afterward. This is a regression, and I think we need to take that seriously.

Quoted 4 lines

It doesn't seem like jiri is doing anything weird here, so there's a reasonably high probability that other hooks would be affected in the same way. How can we help authors of such hooks to handle this change?

This part I do disagree with. Git warns (advice-optional) when I enter detached HEAD state. When I leave my detached HEAD with some orphaned commits within (via checkout), I get a warning unconditionally, and an advice-optional tip to use 'git branch <name> <commit-id>'. The Jiri hook is warning that these commits I'm making could be lost, but in fact Git itself is telling me that (and how to save them) at the first moment when they are actually in danger. So I think this particular example is not a good one.

I'm more worried about ones like this, although in the rebase case it may well be a no-op as I can't start a rebase with a dirty index:
https://github.com/alphagov/digitalmarketplace-runner/blob/f8524d9ee7465caef0571d8beb8dff4b25d10fe7/hooks/post-commit

Perhaps a good question is "what could be very catastrophic if it happened during the middle of a rebase, but not such a bad thing if it happened when I run 'git commit'?"

Quoted 6 lines

I don't think that starting to invoke the 'post-commit' hook even more without going through that thought process is an acceptable fix. Since the "right" semantics aren't obvious here, the first step is probably to collect some typical examples of post-commit hooks, like the example from the message I'm replying to (thank you!). Ideas for where we can find more?

That Github search query I posted above is a pretty big haystack. I suppose plenty of folks who contribute to Git have a userbase and might be familiar with some workflows in use there, right?

- Emily

#92 lore raw

PATCH v4 1/19 git-rebase.txt: update description of --allow-empty-message

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

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.

---
 Documentation/git-rebase.txt |  7 ++++---
 builtin/rebase.c             | 12 +++++++-----
 2 files changed, 11 insertions(+), 8 deletions(-)
Documentation/git-rebase.txt +4 -3
index 0c4f038dd6..c83be7ffc2 100644
@@ -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.
 
builtin/rebase.c +7 -5
index 8081741f8a..faa4e0d406 100644
@@ -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"),
Signature · 1 line
gitgitgadget
#95 lore raw

PATCH v4 2/19 t3404: directly test the behavior of interest

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

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.

---
 t/t3404-rebase-interactive.sh | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)
t/t3404-rebase-interactive.sh +4 -3
index ae6e55ce79..c41531f349 100755
@@ -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
 '
 
Signature · 1 line
gitgitgadget
#96 lore raw

PATCH v4 3/19 rebase (interactive-backend): make --keep-empty the default

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

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 for the interactive backend. 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/[email protected]/
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.

---
 Documentation/git-rebase.txt      | 24 ++++++-----
 builtin/rebase.c                  | 28 ++++++++----
 rebase-interactive.c              |  7 +--
 rebase-interactive.h              |  2 +-
 sequencer.c                       | 20 +++------
 sequencer.h                       |  2 +-
 t/t3421-rebase-topology-linear.sh | 16 +++----
 t/t3424-rebase-empty.sh           | 72 +++++++++++++++++++++++++++++++
 t/t3427-rebase-subtree.sh         | 12 +++---
 9 files changed, 128 insertions(+), 55 deletions(-)
 create mode 100755 t/t3424-rebase-empty.sh
Documentation/git-rebase.txt +13 -11
index c83be7ffc2..1d19542d79 100644
@@ -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
 ~~~~~~~~~~~~~~~~~~~~~~~~~~
builtin/rebase.c +19 -9
index faa4e0d406..537b3241ce 100644
@@ -77,7 +77,6 @@ struct rebase_options {
 	const char *action;
 	int signoff;
 	int allow_rerere_autoupdate;
-	int keep_empty;
 	int autosquash;
 	char *gpg_sign_opt;
 	int autostash;
@@ -375,7 +374,6 @@ static int run_rebase_interactive(struct rebase_options *opts,
 
 	git_config_get_bool("rebase.abbreviatecommands", &abbreviate_commands);
 
-	flags |= opts->keep_empty ? TODO_LIST_KEEP_EMPTY : 0;
 	flags |= abbreviate_commands ? TODO_LIST_ABBREVIATE_CMDS : 0;
 	flags |= opts->rebase_merges ? TODO_LIST_REBASE_MERGES : 0;
 	flags |= opts->rebase_cousins > 0 ? TODO_LIST_REBASE_COUSINS : 0;
@@ -439,6 +437,17 @@ static int run_rebase_interactive(struct rebase_options *opts,
 	return ret;
 }
 
+static int parse_opt_keep_empty(const struct option *opt, const char *arg,
+				int unset)
+{
+	struct rebase_options *opts = opt->value;
+
+	BUG_ON_OPT_ARG(arg);
+
+	opts->type = REBASE_INTERACTIVE;
+	return 0;
+}
+
 static const char * const builtin_rebase_interactive_usage[] = {
 	N_("git rebase--interactive [<options>]"),
 	NULL
@@ -452,7 +461,10 @@ int cmd_rebase__interactive(int argc, const char **argv, const char *prefix)
 	struct option options[] = {
 		OPT_NEGBIT(0, "ff", &opts.flags, N_("allow fast-forward"),
 			   REBASE_FORCE),
-		OPT_BOOL(0, "keep-empty", &opts.keep_empty, N_("keep empty commits")),
+		{ OPTION_CALLBACK, 'k', "keep-empty", &options, NULL,
+			N_("(DEPRECATED) keep empty commits"),
+			PARSE_OPT_NOARG | PARSE_OPT_HIDDEN,
+			parse_opt_keep_empty },
 		OPT_BOOL_F(0, "allow-empty-message", &opts.allow_empty_message,
 			   N_("allow commits with empty messages"),
 			   PARSE_OPT_HIDDEN),
@@ -1145,7 +1157,6 @@ static int run_specific_rebase(struct rebase_options *opts, enum action action)
 		opts->allow_rerere_autoupdate ?
 			opts->allow_rerere_autoupdate == RERERE_AUTOUPDATE ?
 			"--rerere-autoupdate" : "--no-rerere-autoupdate" : "");
-	add_var(&script_snippet, "keep_empty", opts->keep_empty ? "yes" : "");
 	add_var(&script_snippet, "autosquash", opts->autosquash ? "t" : "");
 	add_var(&script_snippet, "gpg_sign_opt", opts->gpg_sign_opt);
 	add_var(&script_snippet, "cmd", opts->cmd);
@@ -1483,8 +1494,10 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
 				 "ignoring them"),
 			      REBASE_PRESERVE_MERGES, PARSE_OPT_HIDDEN),
 		OPT_RERERE_AUTOUPDATE(&options.allow_rerere_autoupdate),
-		OPT_BOOL('k', "keep-empty", &options.keep_empty,
-			 N_("preserve empty commits during rebase")),
+		{ OPTION_CALLBACK, 'k', "keep-empty", &options, NULL,
+			N_("(DEPRECATED) keep empty commits"),
+			PARSE_OPT_NOARG | PARSE_OPT_HIDDEN,
+			parse_opt_keep_empty },
 		OPT_BOOL(0, "autosquash", &options.autosquash,
 			 N_("move commits that begin with "
 			    "squash!/fixup! under -i")),
@@ -1747,9 +1760,6 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
 	if (!(options.flags & REBASE_NO_QUIET))
 		argv_array_push(&options.git_am_opts, "-q");
 
-	if (options.keep_empty)
-		imply_interactive(&options, "--keep-empty");
-
 	if (gpg_sign) {
 		free(options.gpg_sign_opt);
 		options.gpg_sign_opt = xstrfmt("-S%s", gpg_sign);
rebase-interactive.c +1 -6
index aa18ae82b7..7e7b4061bf 100644
@@ -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,
rebase-interactive.h +1 -1
index 44dbb06311..05354ca341 100644
@@ -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,
sequencer.c +7 -13
index b9dbf1adb0..c21fc202b1 100644
@@ -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);
sequencer.h +1 -1
index 9f9ae291e3..c165e0ff25 100644
@@ -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)
t/t3421-rebase-topology-linear.sh +8 -8
index 325072b0a3..57334dca7e 100755
@@ -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
t/t3424-rebase-empty.sh new file +72 -0

72 lines not on this page · raw message

t/t3427-rebase-subtree.sh +6 -6
index bec48e6a1f..8dceef61cf 100755
@@ -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"
Signature · 1 line
gitgitgadget
#99 lore raw

PATCH v4 4/19 rebase (interactive-backend): fix handling of commits that become empty

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

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/[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

---
 Documentation/git-rebase.txt | 27 ++++++++++++++++---
 builtin/rebase.c             | 52 ++++++++++++++++++++++++++++++++++++
 sequencer.c                  | 48 +++++++++++++++++++++++++--------
 sequencer.h                  |  1 +
 t/t3424-rebase-empty.sh      | 50 +++++++++++++++++++++++++++++-----
 t/t3427-rebase-subtree.sh    |  8 +++---
 6 files changed, 161 insertions(+), 25 deletions(-)
Documentation/git-rebase.txt +24 -3
index 1d19542d79..551a91d764 100644
@@ -258,6 +258,22 @@ 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.
++
+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 +577,7 @@ are incompatible with the following options:
  * --interactive
  * --exec
  * --keep-empty
+ * --empty=
  * --edit-todo
  * --root when used in combination with --onto
 
@@ -569,6 +586,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 +603,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
 ~~~~~~~~~~~~~~~~~~~~~~~~~~
builtin/rebase.c +52 -0

102 lines not on this page · raw message

sequencer.c +37 -11

106 lines not on this page · raw message

sequencer.h +1 -0
index c165e0ff25..3b0ab9141f 100644
@@ -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;
t/t3424-rebase-empty.sh +43 -7

70 lines not on this page · raw message

t/t3427-rebase-subtree.sh +4 -4
index 8dceef61cf..79e43a370b 100755
@@ -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" &&
Signature · 1 line
gitgitgadget
#109 lore raw

PATCH v4 11/19 git-rebase.txt: add more details about behavioral differences of backends

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

---
 Documentation/git-rebase.txt | 85 +++++++++++++++++++++++++++++++++---
 1 file changed, 80 insertions(+), 5 deletions(-)
Documentation/git-rebase.txt +80 -5
index 551a91d764..df02d76e2d 100644
@@ -414,7 +414,7 @@ with `--keep-base` in order to drop those commits from your branch.
 
 --ignore-whitespace::
 --whitespace=<option>::
-	These flag are passed to the 'git apply' program
+	These flags are passed to the 'git apply' program
 	(see linkgit:git-apply[1]) that applies the patch.
 +
 See also INCOMPATIBLE OPTIONS below.
@@ -561,8 +561,8 @@ The following options:
 
  * --committer-date-is-author-date
  * --ignore-date
- * --whitespace
  * --ignore-whitespace
+ * --whitespace
  * -C
 
 are incompatible with the following options:
@@ -613,9 +613,84 @@ handling commits that become empty.
 Directory rename detection
 ~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-Directory rename heuristics are enabled in the merge and interactive
-backends.  Due to the lack of accurate tree information, directory
-rename detection is disabled in the am backend.
+Due to the lack of accurate tree information (arising from
+constructing fake ancestors with the limited information available in
+patches), directory rename detection is disabled in the am backend.
+Disabled directory rename detection means that if one side of history
+renames a directory and the other adds new files to the old directory,
+then the new files will be left behind in the old directory without
+any warning at the time of rebasing that you may want to move these
+files into the new directory.
+
+Directory rename detection works with the merge and interactive
+backends to provide you warnings in such cases.
+
+Context
+~~~~~~~
+
+The am backend works by creating a sequence of patches (by calling
+`format-patch` internally), and then applying the patches in sequence
+(calling `am` internally).  Patches are composed of multiple hunks,
+each with line numbers, a context region, and the actual changes.  The
+line numbers have to be taken with some fuzz, since the other side
+will likely have inserted or deleted lines earlier in the file.  The
+context region is meant to help find how to adjust the line numbers in
+order to apply the changes to the right lines.  However, if multiple
+areas of the code have the same surrounding lines of context, the
+wrong one can be picked.  There are real-world cases where this has
+caused commits to be reapplied incorrectly with no conflicts reported.
+Setting diff.context to a larger value may prevent such types of
+problems, but increases the chance of spurious conflicts (since it
+will require more lines of matching context to apply).
+
+The interactive backend works with a full copy of each relevant file,
+insulating it from these types of problems.
+
+Labelling of conflicts markers
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+When there are content conflicts, the merge machinery tries to
+annotate each side's conflict markers with the commits where the
+content came from.  Since the am backend drops the original
+information about the rebased commits and their parents (and instead
+generates new fake commits based off limited information in the
+generated patches), those commits cannot be identified; instead it has
+to fall back to a commit summary.  Also, when merge.conflictStyle is
+set to diff3, the am backend will use "constructed merge base" to
+label the content from the merge base, and thus provide no information
+about the merge base commit whatsoever.
+
+The interactive backend works with the full commits on both sides of
+history and thus has no such limitations.
+
+Hooks
+~~~~~
+
+The am backend has not traditionally called the post-commit hook,
+while the merge/interactive backend has.  However, this was by
+accident of implementation rather than by design.  Both backends
+should have the same behavior, though it is not clear which one is
+correct.
+
+Miscellaneous differences
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+There are a few more behavioral differences that most folks would
+probably consider inconsequential but which are mentioned for
+completeness:
+
+* Reflog: The two backends will use different wording when describing
+  the changes made in the reflog, though both will make use of the
+  word "rebase".
+
+* Progress, informational, and error messages: The two backends
+  provide slightly different progress and informational messages.
+  Also, the am backend writes error messages (such as "Your files
+  would be overwritten...") to stdout, while the interactive backend
+  writes them to stderr.
+
+* State directories: The two backends keep their state in different
+  directories under .git/
 
 include::merge-strategies.txt[]
 
Signature · 1 line
gitgitgadget
#110 lore raw

PATCH v4 12/19 rebase: move incompatibility checks between backend options a bit earlier

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

---
 builtin/rebase.c | 22 +++++++++++-----------
 1 file changed, 11 insertions(+), 11 deletions(-)
builtin/rebase.c +11 -11
index 4b7d8fc908..55a0b2a288 100644
@@ -1878,6 +1878,17 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
 	if (isatty(2) && options.flags & REBASE_NO_QUIET)
 		strbuf_addstr(&options.git_format_patch_opt, " --progress");
 
+	if (options.git_am_opts.argc) {
+		/* all am options except -q are compatible only with --am */
+		for (i = options.git_am_opts.argc - 1; i >= 0; i--)
+			if (strcmp(options.git_am_opts.argv[i], "-q"))
+				break;
+
+		if (is_interactive(&options) && i >= 0)
+			die(_("cannot combine am options with either "
+			      "interactive or merge options"));
+	}
+
 	switch (options.type) {
 	case REBASE_MERGE:
 	case REBASE_INTERACTIVE:
@@ -1908,17 +1919,6 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
 	if (reschedule_failed_exec >= 0)
 		options.reschedule_failed_exec = reschedule_failed_exec;
 
-	if (options.git_am_opts.argc) {
-		/* all am options except -q are compatible only with --am */
-		for (i = options.git_am_opts.argc - 1; i >= 0; i--)
-			if (strcmp(options.git_am_opts.argv[i], "-q"))
-				break;
-
-		if (is_interactive(&options) && i >= 0)
-			die(_("cannot combine am options with either "
-			      "interactive or merge options"));
-	}
-
 	if (options.signoff) {
 		if (options.type == REBASE_PRESERVE_MERGES)
 			die("cannot combine '--signoff' with "
Signature · 1 line
gitgitgadget
#91 lore raw

v4 cover rebase: make the default backend configurable

In reply to Elijah Newren via GitGitGadget

This is a re-roll of en/rebase-backend which has been rebased on v2.25.0 and updated to remove the dependence on ra/rebase-i-more-options, and also tries to address feedback from Phillip, Junio, and Jonathan. This series does a lot of work around making the default rebase backend configurable, and switches the default from the am backend to the merge/interactive one.

Changes since v3:

* Rebased on origin/master and updated to remove the dependence on ra/rebase-i-more-options.
* Added two new patches at the start of the series.
* Split the old first patch into two, while modifying them based on Phillip's feedback (though slightly differently than discussed on the list; instead of making --keep-empty a synonym for --empty=keep, I instead kept backward compatibility by making it a no-op).
* I noted the post-commit hook in the differences between backends. Emily is investigating what changes need to happen there, so I merely documented the existing differences.
* dropped '-i' from the reflog messages; now they just refer to 'rebase'

Changes possibly missing from this version, for discussion:

* I did not remove the --am option as suggested by Phillip, since Junio and Phillip were still discussing whether it is wanted/needed.
* I did not address the last two items Jonathan brought up as I couldn't find enough information to reproduce or understand the problems.

Elijah Newren (19):
git-rebase.txt: update description of --allow-empty-message
t3404: directly test the behavior of interest
rebase (interactive-backend): make --keep-empty the default rebase (interactive-backend): fix handling of commits that become
empty
t3406: simplify an already simple test
rebase, sequencer: remove the broken GIT_QUIET handling
rebase: make sure to pass along the quiet flag to the sequencer
rebase: fix handling of restrict_revision
t3432: make these tests work with either am or merge backends
rebase: allow more types of rebases to fast-forward git-rebase.txt: add more details about behavioral differences of
backends
rebase: move incompatibility checks between backend options a bit
earlier
rebase: add an --am option
git-prompt: change the prompt for interactive-based rebases
rebase: drop '-i' from the reflog for interactive-based rebases rebase tests: mark tests specific to the am-backend with --am rebase tests: repeat some tests using the merge backend instead of am
rebase: make the backend configurable via config setting
rebase: change the default backend from "am" to "merge"

 Documentation/config/rebase.txt        |   8 ++
 Documentation/git-rebase.txt           | 150 +++++++++++++++++---
 builtin/rebase.c                       | 186 +++++++++++++++++++------
 contrib/completion/git-prompt.sh       |   6 +-
 rebase-interactive.c                   |   7 +-
 rebase-interactive.h                   |   2 +-
 sequencer.c                            |  84 ++++++-----
 sequencer.h                            |   3 +-
 t/t3400-rebase.sh                      |  36 ++++-
 t/t3401-rebase-and-am-rename.sh        |   4 +-
 t/t3404-rebase-interactive.sh          |  19 +--
 t/t3406-rebase-message.sh              |  19 ++-
 t/t3407-rebase-abort.sh                |   6 +-
 t/t3420-rebase-autostash.sh            |   2 +-
 t/t3421-rebase-topology-linear.sh      |  16 +--
 t/t3424-rebase-empty.sh                | 108 ++++++++++++++
 t/t3425-rebase-topology-merges.sh      |   8 +-
 t/t3427-rebase-subtree.sh              |  12 +-
 t/t3432-rebase-fast-forward.sh         |  54 ++++---
 t/t5407-post-rewrite-hook.sh           |  12 +-
 t/t5520-pull.sh                        |  27 +++-
 t/t6047-diff3-conflict-markers.sh      |  13 +-
 t/t7512-status-help.sh                 |  12 +-
 t/t9106-git-svn-commit-diff-clobber.sh |   3 +-
 t/t9903-bash-prompt.sh                 |   8 +-
 25 files changed, 595 insertions(+), 210 deletions(-)
 create mode 100755 t/t3424-rebase-empty.sh

base-commit: d0654dc308b0ba76dd8ed7bbb33c8d8f7aacd783
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-679%2Fnewren%2Frebase-fixes-v4
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-679/newren/rebase-fixes-v4
Pull-Request: https://github.com/git/git/pull/679

Range-diff vs v3:

  -:  ---------- >  1:  3ea48d5394 git-rebase.txt: update description of --allow-empty-message
  -:  ---------- >  2:  10fdd162a0 t3404: directly test the behavior of interest
  1:  1c2b77e94d !  3:  179f82ab83 rebase: extend the options for handling of empty commits
     @@ -1,49 +1,46 @@
      Author: Elijah Newren <[email protected]>
      
     -    rebase: extend the options for handling of empty commits
     +    rebase (interactive-backend): make --keep-empty the default
      
     -    Extend the interactive machinery with the ability to handle the full
     -    spread of options for how to handle commits that either start or become
     -    empty (by "become empty" I mean the changes in a commit are a subset of
     -    changes that exist upstream, so the net effect of applying the commit is
     -    no changes).  Introduce a new command line flag for selecting the
     -    desired behavior:
     -        --empty={drop,keep,ask}
     -    with the definitions:
     -        drop: drop empty commits
     -        keep: keep empty commits
     -        ask:  provide the user a chance to interact and pick what to do with
     -              empty commits on a case-by-case basis
     +    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
     +    for the interactive backend.  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.
      
     -    Note that traditionally, am-based rebases have always dropped commits
     -    that either started or became empty, while interactive-based rebases
     -    have defaulted to ask (and provided an option to keep commits that
     -    started empty).  This difference made sense since users of an am-based
     -    rebase just wanted to quickly batch apply a sequence of commits, while
     -    users editing a todo list will likely want the chance to interact and
     -    handle unusual cases on a case-by-case basis.  However, not all rebases
     -    using the interactive machinery are explicitly interactive anymore.  In
     -    particular --merge was always meant to behave more like --am: just
     -    rebase a batch of commits without popping up a todo list.
     +    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.
      
     -    If the --empty flag is not specified, pick defaults as follows:
     -        explicitly interactive: ask
     -        --exec: keep (exec is about checking existing commits, and often
     -                      used without actually changing the base.  Thus the
     -                      expectation is that the user doesn't necessarily want
     -                      anything to change; they just want to test).
     -        otherwise: drop
     -
     -    Also, this commit makes --keep-empty just imply --empty=keep, and hides
     -    it from help so that we aren't confusing users with different ways to do
     -    the same thing.  (I could have added a --drop-empty flag, but then that
     -    invites users to specify both --keep-empty and --drop-empty and we have
     -    to add sanity checking around that; it seems cleaner to have a single
     -    multi-valued option.)  This actually fixes --keep-empty too; previously,
     -    it only meant to sometimes keep empty commits, in particular commits
     -    which started empty would be kept.  But it would still error out and ask
     -    the user what to do with commits that became empty.  Now it keeps empty
     -    commits, as instructed.
     +    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/[email protected]/
     +    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 <[email protected]>
      
     @@ -51,126 +48,62 @@
       --- a/Documentation/git-rebase.txt
       +++ b/Documentation/git-rebase.txt
      @@
     - 	original branch. The index and working tree are also left
       	unchanged as a result.
       
     -+--empty={drop,keep,ask}::
     -+	How to handle commits that become empty (because they contain a
     -+	subset of already upstream changes) or start empty.  With drop
     -+	(the default), commits that start or become empty are dropped.
     -+	With keep (implied by --exec), 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 or
     -+	commit it.  Also with ask, if the rebase is interactive then
     -+	commits which start empty will be commented out in the todo
     -+	action list (giving you a chance to uncomment).
     -++
     -+Note that this has no effect on commits which are already upstream (as
     -+can be checked via `git log --cherry-mark ...`), which are always
     -+dropped by rebase.
     -++
     -+See also INCOMPATIBLE OPTIONS below.
     -+
       --keep-empty::
      -	Keep the commits that do not change anything from its
      -	parents in the result.
     -+	Deprecated alias for what is now known as --empty=keep.
     ++	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.
     - 
     -@@
     -  * --interactive
     -  * --exec
     -  * --keep-empty
     -+ * --empty=
     -  * --edit-todo
     -  * --root when used in combination with --onto
     - 
     -@@
     -  * --preserve-merges and --ignore-whitespace
     -  * --preserve-merges and --committer-date-is-author-date
     -  * --preserve-merges and --ignore-date
     -+ * --preserve-merges and --empty=
     -  * --keep-base and --onto
     -  * --keep-base and --root
     +-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
      @@
     + Empty commits
     + ~~~~~~~~~~~~~
       
     - 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
       --- a/builtin/rebase.c
       +++ b/builtin/rebase.c
     -@@
     - 	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;
      @@
       	const char *action;
       	int signoff;
       	int allow_rerere_autoupdate;
      -	int keep_empty;
       	int autosquash;
     - 	int ignore_whitespace;
       	char *gpg_sign_opt;
     -@@
     - 
     - #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	\
     -@@
     - 		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.ask_on_initially_empty = (opts->empty == EMPTY_ASK &&
     -+					 !(opts->flags & REBASE_INTERACTIVE_EXPLICIT));
     - 	replay.verbose = opts->flags & REBASE_VERBOSE;
     - 	replay.reschedule_failed_exec = opts->reschedule_failed_exec;
     - 	replay.committer_date_is_author_date =
     + 	int autostash;
      @@
       
       	git_config_get_bool("rebase.abbreviatecommands", &abbreviate_commands);
       
      -	flags |= opts->keep_empty ? TODO_LIST_KEEP_EMPTY : 0;
     -+	flags |= (opts->empty == EMPTY_DROP) ? TODO_LIST_DROP_EMPTY : 0;
     -+	flags |= (opts->empty == EMPTY_ASK &&
     -+		  opts->flags & REBASE_INTERACTIVE_EXPLICIT) ?
     -+			TODO_LIST_ASK_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;
     @@ -183,10 +116,8 @@
      +{
      +	struct rebase_options *opts = opt->value;
      +
     -+	BUG_ON_OPT_NEG(unset);
      +	BUG_ON_OPT_ARG(arg);
      +
     -+	opts->empty = EMPTY_KEEP;
      +	opts->type = REBASE_INTERACTIVE;
      +	return 0;
      +}
     @@ -201,62 +132,28 @@
      -		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_NONEG | PARSE_OPT_HIDDEN,
     ++			PARSE_OPT_NOARG | PARSE_OPT_HIDDEN,
      +			parse_opt_keep_empty },
     - 		OPT_BOOL(0, "allow-empty-message", &opts.allow_empty_message,
     - 			 N_("allow commits with empty messages")),
     - 		OPT_BOOL(0, "rebase-merges", &opts.rebase_merges, N_("rebase merge commits")),
     + 		OPT_BOOL_F(0, "allow-empty-message", &opts.allow_empty_message,
     + 			   N_("allow commits with empty messages"),
     + 			   PARSE_OPT_HIDDEN),
      @@
       		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, "empty", opts->empty == EMPTY_KEEP ? "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);
     -@@
     - 	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);
      @@
       				 "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")),
     -+		OPT_CALLBACK_F(0, "empty", &options, N_("{drop,keep,ask}"),
     -+			       N_("how to handle empty commits"),
     -+			       PARSE_OPT_NONEG, parse_opt_empty),
      +		{ OPTION_CALLBACK, 'k', "keep-empty", &options, NULL,
      +			N_("(DEPRECATED) keep empty commits"),
     -+			PARSE_OPT_NOARG | PARSE_OPT_NONEG | PARSE_OPT_HIDDEN,
     ++			PARSE_OPT_NOARG | PARSE_OPT_HIDDEN,
      +			parse_opt_keep_empty },
       		OPT_BOOL(0, "autosquash", &options.autosquash,
       			 N_("move commits that begin with "
     @@ -267,26 +164,10 @@
       
      -	if (options.keep_empty)
      -		imply_interactive(&options, "--keep-empty");
     -+	if (options.empty != EMPTY_UNSPECIFIED)
     -+		imply_interactive(&options, "--empty");
     - 
     +-
       	if (gpg_sign) {
       		free(options.gpg_sign_opt);
     -@@
     - 		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"));
     + 		options.gpg_sign_opt = xstrfmt("-S%s", gpg_sign);
      
       diff --git a/rebase-interactive.c b/rebase-interactive.c
       --- a/rebase-interactive.c
     @@ -296,19 +177,22 @@
       }
       
      -void append_todo_help(unsigned keep_empty, int command_count,
     -+void append_todo_help(unsigned no_ask_empty, int command_count,
     ++void append_todo_help(int command_count,
       		      const char *shortrevisions, const char *shortonto,
       		      struct strbuf *buf)
       {
      @@
     + 			"the rebase will be aborted.\n\n");
       
       	strbuf_add_commented_lines(buf, msg, strlen(msg));
     - 
     +-
      -	if (!keep_empty) {
     -+	if (!no_ask_empty) {
     - 		msg = _("Note that empty commits are commented out");
     - 		strbuf_add_commented_lines(buf, msg, strlen(msg));
     - 	}
     +-		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
       --- a/rebase-interactive.h
     @@ -318,7 +202,7 @@
       struct todo_list;
       
      -void append_todo_help(unsigned keep_empty, int command_count,
     -+void append_todo_help(unsigned no_ask_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,
     @@ -327,157 +211,51 @@
       --- a/sequencer.c
       +++ b/sequencer.c
      @@
     - 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 GIT_PATH_FUNC(rebase_path_ask_on_initially_empty, "rebase-merge/ask_on_initially_empty")
     - 
     - static int git_sequencer_config(const char *k, const char *v, void *cb)
     + 		       struct replay_opts *opts,
     + 		       struct commit *commit)
       {
     +-	int index_unchanged, empty_commit;
     ++	int index_unchanged, originally_empty;
     + 
     + 	/*
     + 	 * Three cases:
      @@
     - 	empty_commit = is_original_commit_empty(commit);
     - 	if (empty_commit < 0)
     - 		return empty_commit;
     + 	if (opts->keep_redundant_commits)
     + 		return 1;
     + 
     +-	empty_commit = is_original_commit_empty(commit);
     +-	if (empty_commit < 0)
     +-		return empty_commit;
      -	if (!empty_commit)
     -+	if (!empty_commit || opts->ask_on_initially_empty)
     ++	originally_empty = is_original_commit_empty(commit);
     ++	if (originally_empty < 0)
     ++		return originally_empty;
     ++	if (!originally_empty)
       		return 0;
       	else
       		return 1;
     -@@
     - 	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) {
     - 		/*
     -@@
     - 		goto leave;
     - 	}
     - 
     --	allow = allow_empty(r, opts, commit);
     --	if (allow < 0) {
     --		res = allow;
     --		goto leave;
     --	} else if (allow)
     --		flags |= ALLOW_EMPTY;
     --	if (!opts->no_commit) {
     -+	drop_commit = 0;
     -+	if (opts->drop_redundant_commits && is_index_unchanged(r)) {
     -+		drop_commit = 1;
     -+		fprintf(stderr, _("No changes -- Patch already applied."));
     -+	} else {
     -+		allow = allow_empty(r, opts, commit);
     -+		if (allow < 0) {
     -+			res = allow;
     -+			goto leave;
     -+		} else if (allow) {
     -+			flags |= ALLOW_EMPTY;
     -+		}
     -+	}
     -+	if (!opts->no_commit && !drop_commit) {
     - 		if (author || command == TODO_REVERT || (flags & AMEND_MSG))
     - 			res = do_commit(r, msg_file, author, opts, flags);
     - 		else
     -@@
     - 	else if (!strcmp(key, "options.allow-empty-message"))
     - 		opts->allow_empty_message =
     - 			git_config_bool_or_int(key, value, &error_flag);
     -+	else if (!strcmp(key, "options.drop-redundant-commits"))
     -+		opts->drop_redundant_commits =
     -+			git_config_bool_or_int(key, value, &error_flag);
     - 	else if (!strcmp(key, "options.keep-redundant-commits"))
     - 		opts->keep_redundant_commits =
     - 			git_config_bool_or_int(key, value, &error_flag);
     -+	else if (!strcmp(key, "options.ask_on_initially_empty"))
     -+		opts->ask_on_initially_empty =
     -+			git_config_bool_or_int(key, value, &error_flag);
     - 	else if (!strcmp(key, "options.signoff"))
     - 		opts->signoff = git_config_bool_or_int(key, value, &error_flag);
     - 	else if (!strcmp(key, "options.record-origin"))
     -@@
     - 		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;
     -+
     -+		if (file_exists(rebase_path_ask_on_initially_empty()))
     -+			opts->ask_on_initially_empty = 1;
     -+
     - 		read_strategy_opts(opts, &buf);
     - 		strbuf_release(&buf);
     - 
     -@@
     - 		write_file(rebase_path_cdate_is_adate(), "%s", "");
     - 	if (opts->ignore_date)
     - 		write_file(rebase_path_ignore_date(), "%s", "");
     -+	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->ask_on_initially_empty)
     -+		write_file(rebase_path_ask_on_initially_empty(), "%s", "");
     - 	if (opts->reschedule_failed_exec)
     - 		write_file(rebase_path_reschedule_failed_exec(), "%s", "");
     - 
     -@@
     - 	if (opts->allow_empty_message)
     - 		res |= git_config_set_in_file_gently(opts_file,
     - 				"options.allow-empty-message", "true");
     -+	if (opts->drop_redundant_commits)
     -+		res |= git_config_set_in_file_gently(opts_file,
     -+				"options.drop-redundant-commits", "true");
     - 	if (opts->keep_redundant_commits)
     - 		res |= git_config_set_in_file_gently(opts_file,
     - 				"options.keep-redundant-commits", "true");
     -+	if (opts->ask_on_initially_empty)
     -+		res |= git_config_set_in_file_gently(opts_file,
     -+				"options.ask_on_initially_empty", "true");
     - 	if (opts->signoff)
     - 		res |= git_config_set_in_file_gently(opts_file,
     - 					"options.signoff", "true");
      @@
       				   struct rev_info *revs, struct strbuf *out,
       				   unsigned flags)
       {
      -	int keep_empty = flags & TODO_LIST_KEEP_EMPTY;
     -+	int drop_empty = flags & TODO_LIST_DROP_EMPTY;
     -+	int ask_empty = flags & TODO_LIST_ASK_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;
     -@@
     - 		is_empty = is_original_commit_empty(commit);
     - 		if (!is_empty && (commit->object.flags & PATCHSAME))
     - 			continue;
     -+		if (is_empty && drop_empty)
     -+			continue;
     - 
     - 		strbuf_reset(&oneline);
     - 		pretty_print_commit(pp, commit, &oneline);
      @@
       		if (!to_merge) {
       			/* non-merge commit: easy case */
       			strbuf_reset(&buf);
      -			if (!keep_empty && is_empty)
     -+			if (is_empty && ask_empty)
     - 				strbuf_addf(&buf, "%c ", comment_line_char);
     +-				strbuf_addf(&buf, "%c ", comment_line_char);
       			strbuf_addf(&buf, "%s %s %s", cmd_pick,
       				    oid_to_hex(&commit->object.oid),
     + 				    oneline.buf);
      @@
       	struct pretty_print_context pp = {0};
       	struct rev_info revs;
       	struct commit *commit;
      -	int keep_empty = flags & TODO_LIST_KEEP_EMPTY;
     -+	int drop_empty = flags & TODO_LIST_DROP_EMPTY;
     -+	int ask_empty = flags & TODO_LIST_ASK_EMPTY;
       	const char *insn = flags & TODO_LIST_ABBREVIATE_CMDS ? "p" : "pick";
       	int rebase_merges = flags & TODO_LIST_REBASE_MERGES;
       
     @@ -491,19 +269,16 @@
       		if (!is_empty && (commit->object.flags & PATCHSAME))
       			continue;
      -		if (!keep_empty && is_empty)
     -+		if (is_empty && drop_empty)
     -+			continue;
     -+		if (is_empty && ask_empty)
     - 			strbuf_addf(out, "%c ", comment_line_char);
     +-			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);
      @@
       
       	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(!(flags & TODO_LIST_ASK_EMPTY),
     -+				 count_commands(todo_list),
     ++		append_todo_help(count_commands(todo_list),
       				 shortrevisions, shortonto, &buf);
       
       	res = write_message(buf.buf, buf.len, file, 0);
     @@ -511,16 +286,6 @@
       diff --git a/sequencer.h b/sequencer.h
       --- a/sequencer.h
       +++ b/sequencer.h
     -@@
     - 	int allow_rerere_auto;
     - 	int allow_empty;
     - 	int allow_empty_message;
     -+	int drop_redundant_commits;
     - 	int keep_redundant_commits;
     -+	int ask_on_initially_empty;
     - 	int verbose;
     - 	int quiet;
     - 	int reschedule_failed_exec;
      @@
       int sequencer_skip(struct repository *repo, struct replay_opts *opts);
       int sequencer_remove_state(struct replay_opts *opts);
     @@ -530,19 +295,34 @@
       #define TODO_LIST_SHORTEN_IDS (1U << 1)
       #define TODO_LIST_ABBREVIATE_CMDS (1U << 2)
       #define TODO_LIST_REBASE_MERGES (1U << 3)
     -@@
     -  * `--onto`, we do not want to re-generate the root commits.
     -  */
     - #define TODO_LIST_ROOT_WITH_ONTO (1U << 6)
     -+#define TODO_LIST_DROP_EMPTY (1U << 7)
     -+#define TODO_LIST_ASK_EMPTY (1U << 8)
     - 
     - 
     - int sequencer_make_script(struct repository *r, struct strbuf *out, int argc,
      
       diff --git a/t/t3421-rebase-topology-linear.sh b/t/t3421-rebase-topology-linear.sh
       --- a/t/t3421-rebase-topology-linear.sh
       +++ b/t/t3421-rebase-topology-linear.sh
     +@@
     + 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
      @@
       test_run_rebase success ''
       test_run_rebase success -m
     @@ -603,31 +383,22 @@
      +	git commit -m "Five letters ought to be enough for anybody"
      +'
      +
     -+test_expect_success 'rebase --merge --empty=drop' '
     ++test_expect_failure 'rebase (am-backend) with a variety of empty commits' '
     ++	test_when_finished "git rebase --abort" &&
      +	git checkout -B testing localmods &&
     -+	git rebase --merge --empty=drop upstream &&
     -+
     -+	test_write_lines C B A >expect &&
     -+	git log --format=%s >actual &&
     -+	test_cmp expect actual
     -+'
     ++	# rebase (--am) should not drop commits that start empty
     ++	git rebase upstream &&
      +
     -+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 &&
     ++	test_write_lines D C B A >expect &&
      +	git log --format=%s >actual &&
      +	test_cmp expect actual
      +'
      +
     -+test_expect_success 'rebase --merge --empty=ask' '
     ++test_expect_failure 'rebase --merge with a variety of empty commits' '
     ++	test_when_finished "git rebase --abort" &&
      +	git checkout -B testing localmods &&
     -+	test_must_fail git rebase --merge --empty=ask upstream &&
     -+
     -+	test_must_fail git rebase --skip &&
     -+	git commit --allow-empty &&
     -+	git rebase --continue &&
     ++	# 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 &&
     @@ -636,25 +407,17 @@
      +
      +GIT_SEQUENCE_EDITOR=: && export GIT_SEQUENCE_EDITOR
      +
     -+test_expect_success 'rebase --interactive --empty=drop' '
     ++test_expect_success 'rebase --interactive with a variety of empty commits' '
      +	git checkout -B testing localmods &&
     -+	git rebase --interactive --empty=drop upstream &&
     ++	test_must_fail git rebase --interactive upstream &&
      +
     -+	test_write_lines 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 &&
     ++	git rebase --skip &&
      +
     -+	test_write_lines D C2 C B A >expect &&
     ++	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
     @@ -665,32 +428,28 @@
       '
       
      -test_expect_success 'Rebase -Xsubtree --keep-empty --onto commit' '
     -+test_expect_success 'Rebase -Xsubtree --empty=ask --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 --empty=ask --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 &&
     -+	test_must_fail git rebase --skip &&
     -+	: last pick was an empty commit that has no changes, but we want to keep it &&
     -+	git commit --allow-empty &&
     ++	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 --empty=ask --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 --empty=ask --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 &&
     -+	test_must_fail git rebase --skip &&
     -+	: last pick was an empty commit that has no changes, but we want to keep it &&
     -+	git commit --allow-empty &&
     ++	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"
  -:  ---------- >  4:  c9542a2abe rebase (interactive-backend): fix handling of commits that become empty
  2:  bd3c5ec155 =  5:  9f66229d5c t3406: simplify an already simple test
  3:  49388b79fd =  6:  8d731fa39c rebase, sequencer: remove the broken GIT_QUIET handling
  4:  478479358f !  7:  b6b6597eef rebase: make sure to pass along the quiet flag to the sequencer
     @@ -8,13 +8,13 @@
       --- a/builtin/rebase.c
       +++ b/builtin/rebase.c
      @@
     + 	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.ask_on_initially_empty = (opts->empty == EMPTY_ASK &&
     - 					 !(opts->flags & REBASE_INTERACTIVE_EXPLICIT));
      +	replay.quiet = !(opts->flags & REBASE_NO_QUIET);
       	replay.verbose = opts->flags & REBASE_VERBOSE;
       	replay.reschedule_failed_exec = opts->reschedule_failed_exec;
     - 	replay.committer_date_is_author_date =
     + 	replay.gpg_sign = xstrdup_or_null(opts->gpg_sign_opt);
      @@
       			 N_("allow pre-rebase hook to run")),
       		OPT_NEGBIT('q', "quiet", &options.flags,
  5:  ee26f5a161 =  8:  0acefa988b rebase: fix handling of restrict_revision
  6:  34a69def33 =  9:  8c5b5b5133 t3432: make these tests work with either am or merge backends
  7:  f2c92853b4 ! 10:  b8c087d6fb rebase: allow more types of rebases to fast-forward
     @@ -35,8 +35,8 @@
       		OPT_STRING(0, "onto", &options.onto_name,
       			   N_("revision"),
      @@
     - 	    options.ignore_date)
     - 		options.flags |= REBASE_FORCE;
     + 		    state_dir_base, cmd_live_rebase, buf.buf);
     + 	}
       
      +	if ((options.flags & REBASE_INTERACTIVE_EXPLICIT) ||
      +	    (action != ACTION_NONE) ||
     @@ -47,7 +47,9 @@
      +
       	for (i = 0; i < options.git_am_opts.argc; i++) {
       		const char *option = options.git_am_opts.argv[i], *p;
     - 		if (!strcmp(option, "--whitespace=fix") ||
     + 		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;

8: b307340f7c ! 11: b50a1741e0 git-rebase.txt: add more details about behavioral differences of backends
@@ -8,23 +8,24 @@
--- a/Documentation/git-rebase.txt
+++ b/Documentation/git-rebase.txt
@@
- with --keep-base in order to drop those commits from your branch.

--ignore-whitespace::
-- Behaves differently depending on which backend is selected.
--+
--'am' backend: When applying a patch, ignore changes in whitespace in --context lines if necessary.
--+
--'interactive' backend: Treat lines with only whitespace changes as --unchanged for the sake of a three-way merge.
-+ Ignore whitespace-only changes in the commits being rebased,
-+ which may avoid "unnecessary" conflicts. (Both backends
-+ currently have differing edgecase bugs with this option; see
-+ BEHAVIORAL DIFFERENCES.)
-
--whitespace=<option>::
- This flag is passed to the 'git apply' program
+- These flag are passed to the 'git apply' program
++ These flags are passed to the 'git apply' program
+ (see linkgit:git-apply[1]) that applies the patch.
+ +
+ See also INCOMPATIBLE OPTIONS below.
+@@
+
+ * --committer-date-is-author-date
+ * --ignore-date
+- * --whitespace
+ * --ignore-whitespace
++ * --whitespace
+ * -C
+
+ are incompatible with the following options:
@@
Directory rename detection
~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -82,24 +83,14 @@
+The interactive backend works with the full commits on both sides of +history and thus has no such limitations.
+
-+--ignore-whitespace
-+~~~~~~~~~~~~~~~~~~~
++Hooks
++~~~~~
+
-+The --ignore-whitespace option is supposed to ignore whitespace-only -+changes if it allows the code to merge cleanly. Unfortunately, the -+different backends implement this differently, and both have different -+edge case bugs.
-++
-+'am' backend: When applying a patch, ignore changes in whitespace in -+context lines if necessary. (Which implies that if the whitespace -+change was not in the context lines but on a line with a real change, -+then the rebase will still fail with "unnecessary" content conflicts.)
-++
-+'interactive' backend: Treat lines with only whitespace changes as -+unchanged for the sake of a three-way merge. This means that if one -+side made no changes and the commits being rebased had whitespace-only -+changes, those whitespaces fixups will be discarded despite the fact -+that they present no content conflict.
++The am backend has not traditionally called the post-commit hook, ++while the merge/interactive backend has. However, this was by ++accident of implementation rather than by design. Both backends ++should have the same behavior, though it is not clear which one is ++correct.
+
+Miscellaneous differences
+~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -123,19 +114,3 @@

include::merge-strategies.txt[]

-
- diff --git a/t/t3433-rebase-options-compatibility.sh b/t/t3433-rebase-options-compatibility.sh
- --- a/t/t3433-rebase-options-compatibility.sh
- +++ b/t/t3433-rebase-options-compatibility.sh
-@@
- GIT_AUTHOR_DATE="1999-04-02T08:03:20+05:30"
- export GIT_AUTHOR_DATE
-
--# This is a special case in which both am and interactive backends
--# provide the same output. It was done intentionally because
--# both the backends fall short of optimal behaviour.
-+# This is a common case in which both am and interactive backends
-+# provide the same output with --ignore-whitespace.
- test_expect_success 'setup' '
- git checkout -b topic &&
- q_to_tab >file <<-\EOF &&

  9:  7c3f2e07f3 = 12:  58e6e4ffb3 rebase: move incompatibility checks between backend options a bit earlier
 10:  1df11f0b51 ! 13:  5478c730ac rebase: add an --am option

@@ -24,8 +24,8 @@
+See also INCOMPATIBLE OPTIONS below.
+
--empty={drop,keep,ask}::
- How to handle commits that become empty (because they contain a
- subset of already upstream changes) or start empty. With drop
+ How to handle commits that are not empty to start and are not
+ clean cherry-picks of any upstream commit, but which become
@@
Ensure at least <n> lines of surrounding context match before and after each change. When fewer lines of surrounding
@@ -37,7 +37,7 @@

@@
--whitespace=<option>::
- This flag is passed to the 'git apply' program
+ These flags are passed to the 'git apply' program
(see linkgit:git-apply[1]) that applies the patch.
+ Implies --am.
+
@@ -48,9 +48,9 @@
The following options:

+ * --am
- * --whitespace
- * -C
-
+ * --committer-date-is-author-date
+ * --ignore-date
+ * --ignore-whitespace

diff --git a/builtin/rebase.c b/builtin/rebase.c --- a/builtin/rebase.c

 11:  ff43593211 = 14:  db5e29bd81 git-prompt: change the prompt for interactive-based rebases
  -:  ---------- > 15:  413e190ac9 rebase: drop '-i' from the reflog for interactive-based rebases
 12:  99388f24e5 ! 16:  170be283a8 rebase tests: mark tests specific to the am-backend with --am

@@ -228,40 +228,6 @@
test_rebase_same_head_ $status_f $what_f $cmp_f " --merge --no-ff" "$*"
}

- diff --git a/t/t3433-rebase-options-compatibility.sh b/t/t3433-rebase-options-compatibility.sh
- --- a/t/t3433-rebase-options-compatibility.sh
- +++ b/t/t3433-rebase-options-compatibility.sh
-@@
- new line 2
- line 3
- EOF
-- test_must_fail git rebase main side &&
-+ test_must_fail git rebase --am main side &&
- git rebase --abort &&
-- git rebase --ignore-whitespace main side &&
-+ git rebase --am --ignore-whitespace main side &&
- test_cmp expect file
- '
-
-@@
-
- test_expect_success '--committer-date-is-author-date works with am backend' '
- git commit --amend &&
-- git rebase --committer-date-is-author-date HEAD^ &&
-+ git rebase --am --committer-date-is-author-date HEAD^ &&
- git show HEAD --pretty="format:%ai" >authortime &&
- git show HEAD --pretty="format:%ci" >committertime &&
- test_cmp authortime committertime
-@@
- # sets to +0530.
- test_expect_success '--ignore-date works with am backend' '
- git commit --amend --date="$GIT_AUTHOR_DATE" &&
-- git rebase --ignore-date HEAD^ &&
-+ git rebase --am --ignore-date HEAD^ &&
- git show HEAD --pretty="format:%ai" >authortime &&
- grep "+0000" authortime
- '
-
diff --git a/t/t5407-post-rewrite-hook.sh b/t/t5407-post-rewrite-hook.sh
--- a/t/t5407-post-rewrite-hook.sh
+++ b/t/t5407-post-rewrite-hook.sh

 13:  c2ba6317bf = 17:  1e3d4066c4 rebase tests: repeat some tests using the merge backend instead of am
 14:  8bec6df51a = 18:  9b4ac83d2d rebase: make the backend configurable via config setting
 15:  044853fd61 = 19:  859a4a94d7 rebase: change the default backend from "am" to "merge"
Signature · 1 line
gitgitgadget
#105 lore raw

PATCH v4 7/19 rebase: make sure to pass along the quiet flag to the sequencer

From: Elijah Newren <[email protected]>

---
 builtin/rebase.c  | 3 ++-
 t/t3400-rebase.sh | 8 +++++++-
 2 files changed, 9 insertions(+), 2 deletions(-)
builtin/rebase.c +2 -1
index 57875485df..e8d518ac8d 100644
@@ -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),
t/t3400-rebase.sh +7 -1
index 221b35f2df..79762b989a 100755
@@ -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" &&
Signature · 1 line
gitgitgadget
#112 lore raw

PATCH v4 14/19 git-prompt: change the prompt for interactive-based rebases

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

In the past, we had different prompts for different types of rebases:
REBASE: for am-based rebases
REBASE-m: for merge-based rebases
REBASE-i: for interactive-based rebases

It's not clear why this distinction was necessary or helpful; when the prompt was added in commit e75201963f67 ("Improve bash prompt to detect various states like an unfinished merge", 2007-09-30), it simply added these three different types. Perhaps there was a useful purpose back then, but there have been some changes:

* The merge backend was deleted after being implemented on top of the interactive backend, causing the prompt for merge-based rebases to change from REBASE-m to REBASE-i.
* The interactive backend is used for multiple different types of non-interactive rebases, so the "-i" part of the prompt doesn't really mean what it used to.
* Rebase backends have gained more abilities and have a great deal of overlap, sometimes making it hard to distinguish them.
* Behavioral differences between the backends have also been ironed out.
* We want to change the default backend from am to interactive, which means people would get "REBASE-i" by default if we didn't change the prompt, and only if they specified --am or --whitespace or -C would they get the "REBASE" prompt.
* In the future, we plan to have "--whitespace", "-C", and even "--am" run the interactive backend once it can handle everything the am-backend can.

For all these reasons, make the prompt for any type of rebase just be "REBASE".

---
 contrib/completion/git-prompt.sh | 6 +-----
 t/t9903-bash-prompt.sh           | 8 ++++----
 2 files changed, 5 insertions(+), 9 deletions(-)
contrib/completion/git-prompt.sh +1 -5
index 1d510cd47b..014cd7c3cf 100644
@@ -429,11 +429,7 @@ __git_ps1 ()
 		__git_eread "$g/rebase-merge/head-name" b
 		__git_eread "$g/rebase-merge/msgnum" step
 		__git_eread "$g/rebase-merge/end" total
-		if [ -f "$g/rebase-merge/interactive" ]; then
-			r="|REBASE-i"
-		else
-			r="|REBASE-m"
-		fi
+		r="|REBASE"
 	else
 		if [ -d "$g/rebase-apply" ]; then
 			__git_eread "$g/rebase-apply/next" step
t/t9903-bash-prompt.sh +4 -4
index 88bc733ad6..7ca35d358d 100755
@@ -163,7 +163,7 @@ test_expect_success 'prompt - inside bare repository' '
 '
 
 test_expect_success 'prompt - interactive rebase' '
-	printf " (b1|REBASE-i 2/3)" >expected &&
+	printf " (b1|REBASE 2/3)" >expected &&
 	write_script fake_editor.sh <<-\EOF &&
 		echo "exec echo" >"$1"
 		echo "edit $(git log -1 --format="%h")" >>"$1"
@@ -180,7 +180,7 @@ test_expect_success 'prompt - interactive rebase' '
 '
 
 test_expect_success 'prompt - rebase merge' '
-	printf " (b2|REBASE-i 1/3)" >expected &&
+	printf " (b2|REBASE 1/3)" >expected &&
 	git checkout b2 &&
 	test_when_finished "git checkout master" &&
 	test_must_fail git rebase --merge b1 b2 &&
@@ -189,11 +189,11 @@ test_expect_success 'prompt - rebase merge' '
 	test_cmp expected "$actual"
 '
 
-test_expect_success 'prompt - rebase' '
+test_expect_success 'prompt - rebase am' '
 	printf " (b2|REBASE 1/3)" >expected &&
 	git checkout b2 &&
 	test_when_finished "git checkout master" &&
-	test_must_fail git rebase b1 b2 &&
+	test_must_fail git rebase --am b1 b2 &&
 	test_when_finished "git rebase --abort" &&
 	__git_ps1 >"$actual" &&
 	test_cmp expected "$actual"
Signature · 1 line
gitgitgadget
#104 lore raw

PATCH v4 6/19 rebase, sequencer: remove the broken GIT_QUIET handling

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

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.

---
 builtin/rebase.c | 6 ++----
 sequencer.c      | 6 ++----
 2 files changed, 4 insertions(+), 8 deletions(-)
builtin/rebase.c +2 -4
index c299869e7b..57875485df 100644
@@ -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);
sequencer.c +2 -4
index 354d0b5a38..078a68eaf3 100644
@@ -2568,8 +2568,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)
@@ -2578,8 +2576,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)
Signature · 1 line
gitgitgadget
#108 lore raw

PATCH v4 10/19 rebase: allow more types of rebases to fast-forward

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

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.

---
 builtin/rebase.c               | 18 ++++++++++++++----
 t/t3432-rebase-fast-forward.sh |  2 ++
 2 files changed, 16 insertions(+), 4 deletions(-)
builtin/rebase.c +14 -4
index fe1175af32..4b7d8fc908 100644
@@ -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)) {
t/t3432-rebase-fast-forward.sh +2 -0
index 7432c0e241..40388ccf9f 100755
@@ -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_ () {
Signature · 1 line
gitgitgadget
#107 lore raw

PATCH v4 9/19 t3432: make these tests work with either am or merge backends

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

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.

---
 t/t3432-rebase-fast-forward.sh | 48 ++++++++++++++++------------------
 1 file changed, 22 insertions(+), 26 deletions(-)
t/t3432-rebase-fast-forward.sh +22 -26
index 92f95b57da..7432c0e241 100755
@@ -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 <expect) &&
+			test_line_count '-gt' \$old .git/logs/HEAD
 		elif test $what = noop
 		then
-			test_i18ngrep 'is up to date' stdout &&
-			test_i18ngrep ! 'rebase forced' stdout
-		elif test $what = noop-force
-		then
-			test_i18ngrep 'is up to date, rebase forced' stdout
+			test_cmp expect .git/logs/HEAD
 		fi &&
 		newhead=\$(git rev-parse HEAD) &&
 		if test $cmp = same
@@ -71,14 +67,14 @@ test_rebase_same_head_ () {
 
 changes='no changes'
 test_rebase_same_head success noop same success work same
-test_rebase_same_head success noop same success noop-force same master
-test_rebase_same_head success noop same success noop-force diff --onto B B
-test_rebase_same_head success noop same success noop-force diff --onto B... B
-test_rebase_same_head success noop same success noop-force same --onto master... master
-test_rebase_same_head success noop same success noop-force same --keep-base master
-test_rebase_same_head success noop same success noop-force same --keep-base
-test_rebase_same_head success noop same success noop-force same --no-fork-point
-test_rebase_same_head success noop same success noop-force same --keep-base --no-fork-point
+test_rebase_same_head success noop same success work same master
+test_rebase_same_head success noop same success work diff --onto B B
+test_rebase_same_head success noop same success work diff --onto B... B
+test_rebase_same_head success noop same success work same --onto master... master
+test_rebase_same_head success noop same success work same --keep-base master
+test_rebase_same_head success noop same success work same --keep-base
+test_rebase_same_head success noop same success work same --no-fork-point
+test_rebase_same_head success noop same success work same --keep-base --no-fork-point
 test_rebase_same_head success noop same success work same --fork-point master
 test_rebase_same_head success noop same success work diff --fork-point --onto B B
 test_rebase_same_head success noop same success work diff --fork-point --onto B... B
@@ -91,14 +87,14 @@ test_expect_success 'add work same to side' '
 
 changes='our changes'
 test_rebase_same_head success noop same success work same
-test_rebase_same_head success noop same success noop-force same master
-test_rebase_same_head success noop same success noop-force diff --onto B B
-test_rebase_same_head success noop same success noop-force diff --onto B... B
-test_rebase_same_head success noop same success noop-force same --onto master... master
-test_rebase_same_head success noop same success noop-force same --keep-base master
-test_rebase_same_head success noop same success noop-force same --keep-base
-test_rebase_same_head success noop same success noop-force same --no-fork-point
-test_rebase_same_head success noop same success noop-force same --keep-base --no-fork-point
+test_rebase_same_head success noop same success work same master
+test_rebase_same_head success noop same success work diff --onto B B
+test_rebase_same_head success noop same success work diff --onto B... B
+test_rebase_same_head success noop same success work same --onto master... master
+test_rebase_same_head success noop same success work same --keep-base master
+test_rebase_same_head success noop same success work same --keep-base
+test_rebase_same_head success noop same success work same --no-fork-point
+test_rebase_same_head success noop same success work same --keep-base --no-fork-point
 test_rebase_same_head success noop same success work same --fork-point master
 test_rebase_same_head success noop same success work diff --fork-point --onto B B
 test_rebase_same_head success noop same success work diff --fork-point --onto B... B
@@ -112,8 +108,8 @@ test_expect_success 'add work same to upstream' '
 '
 
 changes='our and their changes'
-test_rebase_same_head success noop same success noop-force diff --onto B B
-test_rebase_same_head success noop same success noop-force diff --onto B... B
+test_rebase_same_head success noop same success work diff --onto B B
+test_rebase_same_head success noop same success work diff --onto B... B
 test_rebase_same_head success noop same success work diff --onto master... master
 test_rebase_same_head success noop same success work diff --keep-base master
 test_rebase_same_head success noop same success work diff --keep-base
Signature · 1 line
gitgitgadget
#103 lore raw

PATCH v4 5/19 t3406: simplify an already simple test

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

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.

---
 t/t3406-rebase-message.sh | 7 ++-----
 1 file changed, 2 insertions(+), 5 deletions(-)
t/t3406-rebase-message.sh +2 -5
index b393e1e9fe..0c2c569f95 100755
@@ -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' '
Signature · 1 line
gitgitgadget
#106 lore raw

PATCH v4 8/19 rebase: fix handling of restrict_revision

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

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.

---
 builtin/rebase.c  |  4 ++--
 t/t3400-rebase.sh | 20 +++++++++++++++++++-
 2 files changed, 21 insertions(+), 3 deletions(-)
builtin/rebase.c +2 -2
index e8d518ac8d..fe1175af32 100644
@@ -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,
t/t3400-rebase.sh +19 -1
index 79762b989a..71fd6396cd 100755
@@ -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 &&
Signature · 1 line
gitgitgadget
#117 lore raw

PATCH v4 19/19 rebase: change the default backend from "am" to "merge"

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

The am-backend drops information and thus limits what we can do:

* lack of full tree information from the original commits means we cannot do directory rename detection and warn users that they might want to move some of their new files that they placed in old directories to prevent their becoming orphaned.[1]
* reduction in context from only having a few lines beyond those changed means that when context lines are non-unique we can apply patches incorrectly.[2]
* lack of access to original commits means that conflict marker annotation has less information available.

Also, the merge/interactive backend have far more abilities, appear to currently have a slight performance advantage[3] and have room for more optimizations than the am backend[4] (and work is underway to take advantage of some of those possibilities).

[1] https://lore.kernel.org/git/[email protected]/
[2] https://lore.kernel.org/git/CABPp-BGiu2nVMQY_t-rnFR5GQUz_ipyEE8oDocKeO+h+t4Mn4A@mail.gmail.com/
[3] https://public-inbox.org/git/CABPp-BF=ev03WgODk6TMQmuNoatg2kiEe5DR__gJ0OTVqHSnfQ@mail.gmail.com/
[4] https://lore.kernel.org/git/CABPp-BGh7yW69QwxQb13K0HM38NKmQif3A6C6UULEKYnkEJ5vA@mail.gmail.com/

---
 Documentation/git-rebase.txt           |  2 +-
 builtin/rebase.c                       |  4 ++--
 t/t5520-pull.sh                        | 10 ++++++----
 t/t9106-git-svn-commit-diff-clobber.sh |  3 ++-
 4 files changed, 11 insertions(+), 8 deletions(-)
Documentation/git-rebase.txt +1 -1
index 5230084be9..880e8bff5a 100644
@@ -313,7 +313,7 @@ See also INCOMPATIBLE OPTIONS below.
 --merge::
 	Use merging strategies to rebase.  When the recursive (default) merge
 	strategy is used, this allows rebase to be aware of renames on the
-	upstream side.
+	upstream side.  This is the default.
 +
 Note that a rebase merge works by replaying each commit from the working
 branch on top of the <upstream> branch.  Because of this, when a merge
builtin/rebase.c +2 -2
index b96de5809e..5c5526f42e 100644
@@ -101,7 +101,7 @@ struct rebase_options {
 #define REBASE_OPTIONS_INIT {			  	\
 		.type = REBASE_UNSPECIFIED,	  	\
 		.empty = EMPTY_UNSPECIFIED,	  	\
-		.default_backend = "am",	  	\
+		.default_backend = "merge",	  	\
 		.flags = REBASE_NO_QUIET, 		\
 		.git_am_opts = ARGV_ARRAY_INIT,		\
 		.git_format_patch_opt = STRBUF_INIT	\
@@ -1917,7 +1917,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
 
 	if (options.type == REBASE_UNSPECIFIED) {
 		if (!strcmp(options.default_backend, "merge"))
-			options.type = REBASE_MERGE;
+			imply_interactive(&options, "--merge");
 		else if (!strcmp(options.default_backend, "am"))
 			options.type = REBASE_AM;
 		else
t/t5520-pull.sh +6 -4
index 3fff6a06fa..4f9e7f7ff6 100755
@@ -340,7 +340,7 @@ test_expect_success '--rebase with conflicts shows advice' '
 	test_tick &&
 	git commit -m "Create conflict" seq.txt &&
 	test_must_fail git pull --rebase . seq 2>err >out &&
-	test_i18ngrep "Resolve all conflicts manually" out
+	test_i18ngrep "Resolve all conflicts manually" err
 '
 
 test_expect_success 'failed --rebase shows advice' '
@@ -354,7 +354,7 @@ test_expect_success 'failed --rebase shows advice' '
 	git checkout -f -b fails-to-rebase HEAD^ &&
 	test_commit v2-without-cr file "2" file2-lf &&
 	test_must_fail git pull --rebase . diverging 2>err >out &&
-	test_i18ngrep "Resolve all conflicts manually" out
+	test_i18ngrep "Resolve all conflicts manually" err
 '
 
 test_expect_success '--rebase fails with multiple branches' '
@@ -774,8 +774,10 @@ test_expect_success 'git pull --rebase does not reapply old patches' '
 	(
 		cd dst &&
 		test_must_fail git pull --rebase &&
-		find .git/rebase-apply -name "000*" >patches &&
-		test_line_count = 1 patches
+		cat .git/rebase-merge/done .git/rebase-merge/git-rebase-todo >work &&
+		grep -v -e \# -e ^$ work >patches &&
+		test_line_count = 1 patches &&
+		rm -f work
 	)
 '
 
t/t9106-git-svn-commit-diff-clobber.sh +2 -1
index dbe8deac0d..aec45bca3b 100755
@@ -92,7 +92,8 @@ test_expect_success 'multiple dcommit from git svn will not clobber svn' "
 
 
 test_expect_success 'check that rebase really failed' '
-	test -d .git/rebase-apply
+	git status >output &&
+	grep currently.rebasing output
 '
 
 test_expect_success 'resolve, continue the rebase and dcommit' "
Signature · 1 line
gitgitgadget
#116 lore raw

PATCH v4 18/19 rebase: make the backend configurable via config setting

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

---
 Documentation/config/rebase.txt |  8 ++++++++
 builtin/rebase.c                | 31 ++++++++++++++++++++++++-------
 2 files changed, 32 insertions(+), 7 deletions(-)
Documentation/config/rebase.txt +8 -0
index d98e32d812..e6ae30c999 100644
@@ -5,6 +5,14 @@ rebase.useBuiltin::
 	is always used. Setting this will emit a warning, to alert any
 	remaining users that setting this now does nothing.
 
+rebase.backend::
+	Default backend to use for rebasing.  Possible choices are
+	'am' or 'merge' (note that the merge backend is sometimes also
+	refered to as the interactive backend or the interactive
+	machinery elsewhere in the docs).  Also, in the future, if the
+	merge backend gains all remaining capabilities of the am
+	backend, this setting may become unused.
+
 rebase.stat::
 	Whether to show a diffstat of what changed upstream since the last
 	rebase. False by default.
builtin/rebase.c +24 -7
index ecd23d3ee7..b96de5809e 100644
@@ -60,6 +60,7 @@ enum empty_type {
 struct rebase_options {
 	enum rebase_type type;
 	enum empty_type empty;
+	const char *default_backend;
 	const char *state_dir;
 	struct commit *upstream;
 	const char *upstream_name;
@@ -100,6 +101,7 @@ struct rebase_options {
 #define REBASE_OPTIONS_INIT {			  	\
 		.type = REBASE_UNSPECIFIED,	  	\
 		.empty = EMPTY_UNSPECIFIED,	  	\
+		.default_backend = "am",	  	\
 		.flags = REBASE_NO_QUIET, 		\
 		.git_am_opts = ARGV_ARRAY_INIT,		\
 		.git_format_patch_opt = STRBUF_INIT	\
@@ -1272,6 +1274,10 @@ static int rebase_config(const char *var, const char *value, void *data)
 		return 0;
 	}
 
+	if (!strcmp(var, "rebase.backend")) {
+		return git_config_string(&opts->default_backend, var, value);
+	}
+
 	return git_default_config(var, value, data);
 }
 
@@ -1900,9 +1906,23 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
 			if (strcmp(options.git_am_opts.argv[i], "-q"))
 				break;
 
-		if (is_interactive(&options) && i >= 0)
-			die(_("cannot combine am options with either "
-			      "interactive or merge options"));
+		if (i >= 0) {
+			if (is_interactive(&options))
+				die(_("cannot combine am options with either "
+				      "interactive or merge options"));
+			else
+				options.type = REBASE_AM;
+		}
+	}
+
+	if (options.type == REBASE_UNSPECIFIED) {
+		if (!strcmp(options.default_backend, "merge"))
+			options.type = REBASE_MERGE;
+		else if (!strcmp(options.default_backend, "am"))
+			options.type = REBASE_AM;
+		else
+			die(_("Unknown rebase backend: %s"),
+			    options.default_backend);
 	}
 
 	switch (options.type) {
@@ -1915,10 +1935,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
 		options.state_dir = apply_dir();
 		break;
 	default:
-		/* the default rebase backend is `--am` */
-		options.type = REBASE_AM;
-		options.state_dir = apply_dir();
-		break;
+		BUG("options.type was just set above; should be unreachable.");
 	}
 
 	if (options.empty == EMPTY_UNSPECIFIED) {
Signature · 1 line
gitgitgadget
#114 lore raw

PATCH v4 16/19 rebase tests: mark tests specific to the am-backend with --am

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

We have many rebase tests in the testsuite, and often the same test is repeated multiple times just testing different backends. For those tests that were specifically trying to test the am backend, add the --am flag.

---
 t/t3400-rebase.sh                 | 10 +++++-----
 t/t3401-rebase-and-am-rename.sh   |  4 ++--
 t/t3404-rebase-interactive.sh     |  2 +-
 t/t3406-rebase-message.sh         | 12 ++++++------
 t/t3407-rebase-abort.sh           |  6 +++---
 t/t3420-rebase-autostash.sh       |  2 +-
 t/t3425-rebase-topology-merges.sh |  8 ++++----
 t/t3432-rebase-fast-forward.sh    |  4 ++--
 t/t5407-post-rewrite-hook.sh      | 12 ++++++------
 t/t7512-status-help.sh            | 12 ++++++------
 10 files changed, 36 insertions(+), 36 deletions(-)
t/t3400-rebase.sh +5 -5
index 71fd6396cd..0a491f2363 100755
@@ -183,19 +183,19 @@ test_expect_success 'default to common base in @{upstream}s reflog if no upstrea
 	test_cmp expect actual
 '
 
-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 (--am)' '
 	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 rebase --am &&
 	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 &&
+	git rebase --am &&
 	git rev-parse --verify default-base >expect &&
 	git rev-parse default~1 >actual &&
 	test_cmp expect actual
@@ -226,7 +226,7 @@ test_expect_success 'cherry-picked commits and fork-point work together' '
 
 test_expect_success 'rebase --am -q is quiet' '
 	git checkout -b quiet topic &&
-	git rebase -q master >output.out 2>&1 &&
+	git rebase --am -q master >output.out 2>&1 &&
 	test_must_be_empty output.out
 '
 
@@ -325,7 +325,7 @@ test_expect_success 'rebase --am and --show-current-patch' '
 		echo two >>init.t &&
 		git commit -a -m two &&
 		git tag two &&
-		test_must_fail git rebase -f --onto init HEAD^ &&
+		test_must_fail git rebase --am -f --onto init HEAD^ &&
 		GIT_TRACE=1 git rebase --show-current-patch >/dev/null 2>stderr &&
 		grep "show.*$(git rev-parse two)" stderr
 	)
t/t3401-rebase-and-am-rename.sh +2 -2
index a0b9438b22..50803958fd 100755
@@ -52,13 +52,13 @@ test_expect_success 'rebase --interactive: directory rename detected' '
 	)
 '
 
-test_expect_failure 'rebase (am): directory rename detected' '
+test_expect_failure 'rebase --am: directory rename detected' '
 	(
 		cd dir-rename &&
 
 		git checkout B^0 &&
 
-		git -c merge.directoryRenames=true rebase A &&
+		git -c merge.directoryRenames=true rebase --am A &&
 
 		git ls-files -s >out &&
 		test_line_count = 5 out &&
t/t3404-rebase-interactive.sh +1 -1
index a31583eb2f..f964b2cd41 100755
@@ -1138,7 +1138,7 @@ test_expect_success C_LOCALE_OUTPUT 'rebase --edit-todo does not work on non-int
 	git checkout conflict-branch &&
 	(
 		set_fake_editor &&
-		test_must_fail git rebase -f --onto HEAD~2 HEAD~ &&
+		test_must_fail git rebase -f --am --onto HEAD~2 HEAD~ &&
 		test_must_fail git rebase --edit-todo
 	) &&
 	git rebase --abort
t/t3406-rebase-message.sh +6 -6
index 0c2c569f95..7ce617fc1f 100755
@@ -23,24 +23,24 @@ test_expect_success 'rebase -m' '
 '
 
 test_expect_success 'rebase against master twice' '
-	git rebase master >out &&
+	git rebase --am master >out &&
 	test_i18ngrep "Current branch topic is up to date" out
 '
 
 test_expect_success 'rebase against master twice with --force' '
-	git rebase --force-rebase master >out &&
+	git rebase --force-rebase --am master >out &&
 	test_i18ngrep "Current branch topic is up to date, rebase forced" out
 '
 
 test_expect_success 'rebase against master twice from another branch' '
 	git checkout topic^ &&
-	git rebase master topic >out &&
+	git rebase --am master topic >out &&
 	test_i18ngrep "Current branch topic is up to date" out
 '
 
 test_expect_success 'rebase fast-forward to master' '
 	git checkout topic^ &&
-	git rebase topic >out &&
+	git rebase --am topic >out &&
 	test_i18ngrep "Fast-forwarded HEAD to topic" out
 '
 
@@ -89,7 +89,7 @@ test_expect_success 'GIT_REFLOG_ACTION' '
 	git checkout -b reflog-topic start &&
 	test_commit reflog-to-rebase &&
 
-	git rebase reflog-onto &&
+	git rebase --am reflog-onto &&
 	git log -g --format=%gs -3 >actual &&
 	cat >expect <<-\EOF &&
 	rebase finished: returning to refs/heads/reflog-topic
@@ -99,7 +99,7 @@ test_expect_success 'GIT_REFLOG_ACTION' '
 	test_cmp expect actual &&
 
 	git checkout -b reflog-prefix reflog-to-rebase &&
-	GIT_REFLOG_ACTION=change-the-reflog git rebase reflog-onto &&
+	GIT_REFLOG_ACTION=change-the-reflog git rebase --am reflog-onto &&
 	git log -g --format=%gs -3 >actual &&
 	cat >expect <<-\EOF &&
 	rebase finished: returning to refs/heads/reflog-prefix
t/t3407-rebase-abort.sh +3 -3
index 910f218284..3e31826170 100755
@@ -96,14 +96,14 @@ testrebase() {
 	'
 }
 
-testrebase "" .git/rebase-apply
+testrebase " --am" .git/rebase-apply
 testrebase " --merge" .git/rebase-merge
 
-test_expect_success 'rebase --quit' '
+test_expect_success 'rebase --am --quit' '
 	cd "$work_dir" &&
 	# Clean up the state from the previous one
 	git reset --hard pre-rebase &&
-	test_must_fail git rebase master &&
+	test_must_fail git rebase --am master &&
 	test_path_is_dir .git/rebase-apply &&
 	head_before=$(git rev-parse HEAD) &&
 	git rebase --quit &&
t/t3420-rebase-autostash.sh +1 -1
index 5f7e73cf83..3816159e20 100755
@@ -234,7 +234,7 @@ test_expect_success "rebase: noop rebase" '
 	git checkout feature-branch
 '
 
-testrebase "" .git/rebase-apply
+testrebase " --am" .git/rebase-apply
 testrebase " --merge" .git/rebase-merge
 testrebase " --interactive" .git/rebase-merge
 
t/t3425-rebase-topology-merges.sh +4 -4
index fd8efe84fe..19700b025b 100755
@@ -54,7 +54,7 @@ test_run_rebase () {
 		test_linear_range 'n o' e..
 	"
 }
-test_run_rebase success ''
+test_run_rebase success --am
 test_run_rebase success -m
 test_run_rebase success -i
 
@@ -70,7 +70,7 @@ test_run_rebase () {
 		test_linear_range "\'"$expected"\'" d..
 	"
 }
-test_run_rebase success 'n o e' ''
+test_run_rebase success 'n o e' --am
 test_run_rebase success 'n o e' -m
 test_run_rebase success 'n o e' -i
 
@@ -86,7 +86,7 @@ test_run_rebase () {
 		test_linear_range "\'"$expected"\'" c..
 	"
 }
-test_run_rebase success 'd n o e' ''
+test_run_rebase success 'd n o e' --am
 test_run_rebase success 'd n o e' -m
 test_run_rebase success 'd n o e' -i
 
@@ -102,7 +102,7 @@ test_run_rebase () {
 		test_linear_range "\'"$expected"\'" c..
 	"
 }
-test_run_rebase success 'd n o e' ''
+test_run_rebase success 'd n o e' --am
 test_run_rebase success 'd n o e' -m
 test_run_rebase success 'd n o e' -i
 
t/t3432-rebase-fast-forward.sh +2 -2
index 40388ccf9f..4b3cecce56 100755
@@ -28,8 +28,8 @@ test_rebase_same_head () {
 	shift &&
 	cmp_f="$1" &&
 	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 " --am" "$*" &&
+	test_rebase_same_head_ $status_f $what_f $cmp_f " --am --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" "$*"
 }
t/t5407-post-rewrite-hook.sh +6 -6
index 7344253bfb..a8a73616e4 100755
@@ -53,10 +53,10 @@ test_expect_success 'git commit --amend --no-post-rewrite' '
 	test ! -f post-rewrite.data
 '
 
-test_expect_success 'git rebase' '
+test_expect_success 'git rebase --am' '
 	git reset --hard D &&
 	clear_hook_input &&
-	test_must_fail git rebase --onto A B &&
+	test_must_fail git rebase --am --onto A B &&
 	echo C > foo &&
 	git add foo &&
 	git rebase --continue &&
@@ -68,10 +68,10 @@ test_expect_success 'git rebase' '
 	verify_hook_input
 '
 
-test_expect_success 'git rebase --skip' '
+test_expect_success 'git rebase --am --skip' '
 	git reset --hard D &&
 	clear_hook_input &&
-	test_must_fail git rebase --onto A B &&
+	test_must_fail git rebase --am --onto A B &&
 	test_must_fail git rebase --skip &&
 	echo D > foo &&
 	git add foo &&
@@ -84,10 +84,10 @@ test_expect_success 'git rebase --skip' '
 	verify_hook_input
 '
 
-test_expect_success 'git rebase --skip the last one' '
+test_expect_success 'git rebase --am --skip the last one' '
 	git reset --hard F &&
 	clear_hook_input &&
-	test_must_fail git rebase --onto D A &&
+	test_must_fail git rebase --am --onto D A &&
 	git rebase --skip &&
 	echo rebase >expected.args &&
 	cat >expected.data <<-EOF &&
t/t7512-status-help.sh +6 -6
index 66d7a62797..d22b0acf2a 100755
@@ -71,10 +71,10 @@ test_expect_success 'prepare for rebase conflicts' '
 '
 
 
-test_expect_success 'status when rebase in progress before resolving conflicts' '
+test_expect_success 'status when rebase --am in progress before resolving conflicts' '
 	test_when_finished "git rebase --abort" &&
 	ONTO=$(git rev-parse --short HEAD^^) &&
-	test_must_fail git rebase HEAD^ --onto HEAD^^ &&
+	test_must_fail git rebase --am HEAD^ --onto HEAD^^ &&
 	cat >expected <<EOF &&
 rebase in progress; onto $ONTO
 You are currently rebasing branch '\''rebase_conflicts'\'' on '\''$ONTO'\''.
@@ -94,11 +94,11 @@ EOF
 '
 
 
-test_expect_success 'status when rebase in progress before rebase --continue' '
+test_expect_success 'status when rebase --am in progress before rebase --continue' '
 	git reset --hard rebase_conflicts &&
 	test_when_finished "git rebase --abort" &&
 	ONTO=$(git rev-parse --short HEAD^^) &&
-	test_must_fail git rebase HEAD^ --onto HEAD^^ &&
+	test_must_fail git rebase --am HEAD^ --onto HEAD^^ &&
 	echo three >main.txt &&
 	git add main.txt &&
 	cat >expected <<EOF &&
@@ -688,7 +688,7 @@ EOF
 '
 
 
-test_expect_success 'status when rebase conflicts with statushints disabled' '
+test_expect_success 'status when rebase --am conflicts with statushints disabled' '
 	git reset --hard master &&
 	git checkout -b statushints_disabled &&
 	test_when_finished "git config --local advice.statushints true" &&
@@ -698,7 +698,7 @@ test_expect_success 'status when rebase conflicts with statushints disabled' '
 	test_commit three_statushints main.txt three &&
 	test_when_finished "git rebase --abort" &&
 	ONTO=$(git rev-parse --short HEAD^^) &&
-	test_must_fail git rebase HEAD^ --onto HEAD^^ &&
+	test_must_fail git rebase --am HEAD^ --onto HEAD^^ &&
 	cat >expected <<EOF &&
 rebase in progress; onto $ONTO
 You are currently rebasing branch '\''statushints_disabled'\'' on '\''$ONTO'\''.
Signature · 1 line
gitgitgadget
#113 lore raw

PATCH v4 15/19 rebase: drop '-i' from the reflog for interactive-based rebases

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

A large variety of rebase types are supported by the interactive machinery, not just the explicitly interactive ones. These all share the same code and write the same reflog messages, but the "-i" moniker in those messages doesn't really have much meaning. It also becomes somewhat distracting once we switch the default from the am-backend to the interactive one. Just remove the "-i" from these messages.

---
 builtin/rebase.c              |  2 +-
 sequencer.c                   | 12 ++++++------
 t/t3404-rebase-interactive.sh | 10 +++++-----
 3 files changed, 12 insertions(+), 12 deletions(-)
builtin/rebase.c +1 -1
index 6884590258..ecd23d3ee7 100644
@@ -1441,7 +1441,7 @@ static void set_reflog_action(struct rebase_options *options)
 	if (env && strcmp("rebase", env))
 		return; /* only override it if it is "rebase" */
 
-	strbuf_addf(&buf, "rebase -i (%s)", options->action);
+	strbuf_addf(&buf, "rebase (%s)", options->action);
 	setenv(GIT_REFLOG_ACTION_ENVIRONMENT, buf.buf, 1);
 	strbuf_release(&buf);
 }
sequencer.c +6 -6
index 078a68eaf3..cca503c11b 100644
@@ -290,7 +290,7 @@ int sequencer_remove_state(struct replay_opts *opts)
 			char *eol = strchr(p, '\n');
 			if (eol)
 				*eol = '\0';
-			if (delete_ref("(rebase -i) cleanup", p, NULL, 0) < 0) {
+			if (delete_ref("(rebase) cleanup", p, NULL, 0) < 0) {
 				warning(_("could not delete '%s'"), p);
 				ret = -1;
 			}
@@ -324,7 +324,7 @@ static const char *action_name(const struct replay_opts *opts)
 	case REPLAY_PICK:
 		return N_("cherry-pick");
 	case REPLAY_INTERACTIVE_REBASE:
-		return N_("rebase -i");
+		return N_("rebase");
 	}
 	die(_("unknown action: %d"), opts->action);
 }
@@ -628,7 +628,7 @@ static int do_recursive_merge(struct repository *r,
 			       COMMIT_LOCK | SKIP_IF_UNCHANGED))
 		/*
 		 * TRANSLATORS: %s will be "revert", "cherry-pick" or
-		 * "rebase -i".
+		 * "rebase".
 		 */
 		return error(_("%s: Unable to write new index file"),
 			_(action_name(opts)));
@@ -2885,8 +2885,8 @@ static int save_todo(struct todo_list *todo_list, struct replay_opts *opts)
 	int next = todo_list->current, offset, fd;
 
 	/*
-	 * rebase -i writes "git-rebase-todo" without the currently executing
-	 * command, appending it to "done" instead.
+	 * interactive backend writes "git-rebase-todo" without the currently
+	 * executing command, appending it to "done" instead.
 	 */
 	if (is_rebase_i(opts))
 		next++;
@@ -3197,7 +3197,7 @@ static int do_label(struct repository *r, const char *name, int len)
 		return error(_("illegal label name: '%.*s'"), len, name);
 
 	strbuf_addf(&ref_name, "refs/rewritten/%.*s", len, name);
-	strbuf_addf(&msg, "rebase -i (label) '%.*s'", len, name);
+	strbuf_addf(&msg, "rebase (label) '%.*s'", len, name);
 
 	transaction = ref_store_transaction_begin(refs, &err);
 	if (!transaction) {
t/t3404-rebase-interactive.sh +5 -5
index c41531f349..a31583eb2f 100755
@@ -223,7 +223,7 @@ test_expect_success 'reflog for the branch shows state before rebase' '
 '
 
 test_expect_success 'reflog for the branch shows correct finish message' '
-	printf "rebase -i (finish): refs/heads/branch1 onto %s\n" \
+	printf "rebase (finish): refs/heads/branch1 onto %s\n" \
 		"$(git rev-parse branch2)" >expected &&
 	git log -g --pretty=%gs -1 refs/heads/branch1 >actual &&
 	test_cmp expected actual
@@ -1162,10 +1162,10 @@ test_expect_success 'rebase -i produces readable reflog' '
 	git branch -f branch-reflog-test H &&
 	git rebase -i --onto I F branch-reflog-test &&
 	cat >expect <<-\EOF &&
-	rebase -i (finish): returning to refs/heads/branch-reflog-test
-	rebase -i (pick): H
-	rebase -i (pick): G
-	rebase -i (start): checkout I
+	rebase (finish): returning to refs/heads/branch-reflog-test
+	rebase (pick): H
+	rebase (pick): G
+	rebase (start): checkout I
 	EOF
 	git reflog -n4 HEAD |
 	sed "s/[^:]*: //" >actual &&
Signature · 1 line
gitgitgadget
#115 lore raw

PATCH v4 17/19 rebase tests: repeat some tests using the merge backend instead of am

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

In order to ensure the merge/interactive backend gets similar coverage to the am one, add some tests for cases where previously only the am backend was tested.

---
 t/t5520-pull.sh                   | 17 +++++++++++++++--
 t/t6047-diff3-conflict-markers.sh | 13 +++++++++++--
 2 files changed, 26 insertions(+), 4 deletions(-)
t/t5520-pull.sh +15 -2
index 602d996a33..3fff6a06fa 100755
@@ -277,14 +277,27 @@ test_expect_success '--rebase' '
 	test_cmp expect actual
 '
 
-test_expect_success '--rebase fast forward' '
+test_expect_success '--rebase (merge) fast forward' '
 	git reset --hard before-rebase &&
 	git checkout -b ff &&
 	echo another modification >file &&
 	git commit -m third file &&
 
 	git checkout to-rebase &&
-	git pull --rebase . ff &&
+	git -c rebase.backend=merge pull --rebase . ff &&
+	test_cmp_rev HEAD ff &&
+
+	# The above only validates the result.  Did we actually bypass rebase?
+	git reflog -1 >reflog.actual &&
+	sed "s/^[0-9a-f][0-9a-f]*/OBJID/" reflog.actual >reflog.fuzzy &&
+	echo "OBJID HEAD@{0}: pull --rebase . ff: Fast-forward" >reflog.expected &&
+	test_cmp reflog.expected reflog.fuzzy
+'
+
+test_expect_success '--rebase (am) fast forward' '
+	git reset --hard before-rebase &&
+
+	git -c rebase.backend=am pull --rebase . ff &&
 	test_cmp_rev HEAD ff &&
 
 	# The above only validates the result.  Did we actually bypass rebase?
t/t6047-diff3-conflict-markers.sh +11 -2
index 860542aad0..d383ce8130 100755
@@ -186,7 +186,7 @@ test_expect_success 'check multiple merge bases' '
 	)
 '
 
-test_expect_success 'rebase describes fake ancestor base' '
+test_expect_success 'rebase --merge describes parent of commit being picked' '
 	test_create_repo rebase &&
 	(
 		cd rebase &&
@@ -194,7 +194,16 @@ test_expect_success 'rebase describes fake ancestor base' '
 		test_commit master file &&
 		git checkout -b side HEAD^ &&
 		test_commit side file &&
-		test_must_fail git -c merge.conflictstyle=diff3 rebase master &&
+		test_must_fail git -c merge.conflictstyle=diff3 rebase --merge master &&
+		grep "||||||| parent of" file
+	)
+'
+
+test_expect_success 'rebase --am describes fake ancestor base' '
+	(
+		cd rebase &&
+		git rebase --abort &&
+		test_must_fail git -c merge.conflictstyle=diff3 rebase --am master &&
 		grep "||||||| constructed merge base" file
 	)
 '
Signature · 1 line
gitgitgadget
#111 lore raw

PATCH v4 13/19 rebase: add an --am option

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

Currently, this option doesn't do anything except error out if any options requiring the interactive-backend are also passed. However, when we make the default backend configurable later in this series, this flag will provide a way to override the config setting.

---
 Documentation/git-rebase.txt | 11 ++++++++++-
 builtin/rebase.c             | 18 +++++++++++++++++-
 2 files changed, 27 insertions(+), 2 deletions(-)
Documentation/git-rebase.txt +10 -1
index df02d76e2d..5230084be9 100644
@@ -258,6 +258,13 @@ See also INCOMPATIBLE OPTIONS below.
 	original branch. The index and working tree are also left
 	unchanged as a result.
 
+--am:
+	Use git-am internally to rebase.  This option may become a
+	no-op in the future once the interactive backend handles
+	everything the am one does.
++
+See also INCOMPATIBLE OPTIONS below.
+
 --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
@@ -376,7 +383,7 @@ See also INCOMPATIBLE OPTIONS below.
 	Ensure at least <n> lines of surrounding context match before
 	and after each change.  When fewer lines of surrounding
 	context exist they all must match.  By default no context is
-	ever ignored.
+	ever ignored.  Implies --am.
 +
 See also INCOMPATIBLE OPTIONS below.
 
@@ -416,6 +423,7 @@ with `--keep-base` in order to drop those commits from your branch.
 --whitespace=<option>::
 	These flags are passed to the 'git apply' program
 	(see linkgit:git-apply[1]) that applies the patch.
+	Implies --am.
 +
 See also INCOMPATIBLE OPTIONS below.
 
@@ -559,6 +567,7 @@ INCOMPATIBLE OPTIONS
 
 The following options:
 
+ * --am
  * --committer-date-is-author-date
  * --ignore-date
  * --ignore-whitespace
builtin/rebase.c +17 -1
index 55a0b2a288..6884590258 100644
@@ -1335,6 +1335,18 @@ static int can_fast_forward(struct commit *onto, struct commit *upstream,
 	return res && is_linear_history(onto, head);
 }
 
+static int parse_opt_am(const struct option *opt, const char *arg, int unset)
+{
+	struct rebase_options *opts = opt->value;
+
+	BUG_ON_OPT_NEG(unset);
+	BUG_ON_OPT_ARG(arg);
+
+	opts->type = REBASE_AM;
+
+	return 0;
+}
+
 /* -i followed by -m is still -i */
 static int parse_opt_merge(const struct option *opt, const char *arg, int unset)
 {
@@ -1519,6 +1531,10 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
 		OPT_CMDMODE(0, "show-current-patch", &action,
 			    N_("show the patch file being applied or merged"),
 			    ACTION_SHOW_CURRENT_PATCH),
+		{ OPTION_CALLBACK, 0, "am", &options, NULL,
+			N_("use apply-mail strategies to rebase"),
+			PARSE_OPT_NOARG | PARSE_OPT_NONEG,
+			parse_opt_am },
 		{ OPTION_CALLBACK, 'm', "merge", &options, NULL,
 			N_("use merging strategies to rebase"),
 			PARSE_OPT_NOARG | PARSE_OPT_NONEG,
@@ -1878,7 +1894,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
 	if (isatty(2) && options.flags & REBASE_NO_QUIET)
 		strbuf_addstr(&options.git_format_patch_opt, " --progress");
 
-	if (options.git_am_opts.argc) {
+	if (options.git_am_opts.argc || options.type == REBASE_AM) {
 		/* all am options except -q are compatible only with --am */
 		for (i = options.git_am_opts.argc - 1; i >= 0; i--)
 			if (strcmp(options.git_am_opts.argv[i], "-q"))
Signature · 1 line
gitgitgadget
#75 lore raw

Re: [PATCH v3 15/15] rebase: change the default backend from "am" to "merge"

In reply to Johannes Schindelin

Hi,

Quoted 48 lines · Johannes Schindelin

Hi,

Quoted 28 lines · Phillip Wood
Quoted 11 lines · Elijah Newren
Quoted 9 lines · Jonathan Nieder

Elijah Newren via GitGitGadget wrote:

1. "git rebase --am" does not invoke the post-commit hook, but "git rebase --merge" does. Is this behavior change intended?

Noticed because jiri[1] installs a post-commit hook that warns about commits on detached HEAD, so this change makes rebases more noisy in repositories that were set up using jiri.

Perhaps that hook could learn not to warn if a branch is being rebased? git could be more helpful there by having a porcelain option to status that prints the branch name if we're rebasing (git worktree --list shows the branch correctly when it's being rebased but does not (yet - I have a patch to do it) mark the current worktree so isn't very helpful.)

Quoted 8 lines

I've never used a post-commit hook or seen one in the wild. Certainly wasn't intentional, but it's not clear to me if it's wrong or right either. I don't see why it would make sense to distinguish between any of git rebase --am/--merge/--interactive, but it isn't too surprising that by historical accident the two rebase backends which happened to call git-commit behind the scenes would call a post-commit hook and the other rebase backend that didn't call git-commit wouldn't.

Quoted 8 lines

Looking through the history the am based rebase has never run the post-commit hook as am has its own set of hooks and the scripted version used commit-tree. The merge based rebase ran git commit which ran the post commit hook. The interactive rebase ran the hook until and I broke it in a356ee4659b ("sequencer: try to commit without forking 'git commit'", 2017-11-24) and after I fixed it in 4627bc777e ("sequencer: run post-commit hook", 2019-10-15). As it was broken for two years with no one noticing it can't be that popular.

Maybe a crazy idea, but maybe not: how about running the post-commit hook _only_ if --merge was specified explicitly, and in that case (and guarded behind a check verifying that the post-commit hook _actually_ exists _and_ is executable) warn the user that this hook won't be run in future versions?

Quoted 7 lines

To make things better for users who actually want to run that hook during rebases, we could introduce a config option, say, rebase.runPostCommitHook that is a tri-state (true, false, onlyForDashDashMerge, at first defaulting to the last, eventually to false).

Crazy? Or helpful?

Seems crazy. Why would you want it just for --merge? If anything, I would think --merge should be most like --am; if some mode of rebase were to be considered special, I'd think it'd only be the _explicitly_ interactive case. But I don't see the justification for treating any of the rebase modes differently. I think the hook should be on for all of them, or off for all of them, and I could go either way.

(Honestly, it's tempting to just fix the fact that the interactive backend needlessly forks a "git commit" process by having it commit on its own much like builtin/merge.c does. Then omit calling the post-commit hook and it behaves the same as the am backend and no one in the world notices because no one in the world uses or cares about that hook except a few people at Google who happen to be used to the am-backend and even then aren't convinced whether invoking the hook or not is right. And if that's wrong and someone has a solid argument about why and the harm that not calling it does, then big deal, we add calling the hook sometime in the future...)

Quoted 11 lines
Quoted 10 lines
Quoted 3 lines

But the big question here, is what is correct behavior? Should rebase call the post-commit hook, or should it skip it? I haven't any clue what the answer to that is.

It's creating a new commit so I lean towards thinking it should run the post-commit hook. As an example I have a post-commit hook that prints a warning if a commit is created on a branch that is being rewritten by one of my scripts in another worktree. There are pre-commit and pre-rebase hooks to try and prevent that, but the warning is there as a last resort if those hooks are by-passed.

Quoted 2 lines

I guess you're right, it is quite surprising that the post-commit hook is _not_ run for --am rebases even though commits are created.

I guess part of the problem is whether people think of it as "new commits" or just any commits. "New commits" are created by "git commit" and "git merge". rebase and cherry-pick just create derivatives of existing commits. Given the existence of the post-rewrite hook, one could argue that the distinction has merit. I'm not sure either way, so I'm glad Emily started investigating. I'd rather not deal with that can of worms... :-)

Quoted 64 lines
Quoted 7 lines
Quoted 2 lines
Quoted 2 lines

2. GIT_REFLOG_ACTION contains "rebase -i" even though the rebase is not interactive.

If this is important to people I think it should be easy enough to set GIT_REFLOG_ACTION to the appropriate string in builtin/rebase.c (so long as it hasn't already been set by the user) rather than relying on sequencer.c to do it.

I agree (but won't have time to implement it, so maybe I should shut up already...)

Quoted 39 lines
Quoted 25 lines

Yep, as does --keep, --exec, --rebase-merges, etc. There are lots of rebases which use the interactive machinery even if they aren't explicitly interactive. I've never seen the "-i" in the reflog message defined, but clearly it has always been used whenever the interactive machinery was in play regardless of whether the rebase was interactive. In that regard, I figured that --merge fit in rather nicely. (And I noted the fact that reflog messages were different between the backends among the "BEHAVIORAL DIFFERENCES" section of git-rebase.txt). But if others think we should just drop the -i (much as we did for the bash prompt), I'd be happy with that too. If we go that route, I think I'd rather drop the -i in the reflog for all rebases, not just the using-the-interactive-machinery-but-not-explicitly-interactive ones.

Quoted 11 lines

3. In circumstances I haven't pinned down yet, we get the error

     message "invalid date format: @@2592000 +0000":

         $ git rebase --committer-date-is-author-date --onto branch_K
         branch_L~1 branch_L
         $ git checkout --theirs file
         $ git add file
         $ git rebase --continue
         fatal: invalid date format: @@2592000 +0000
         error: could not commit staged changes.
Quoted 8 lines
Quoted 2 lines

This isn't reproducible without --committer-date-is-author-date. More context (the test where it happens) is in [2].

Interesting. Do you happen to know if this started happening with ra/rebase-i-more-options, or did it just become an issue with en/rebase-backend? I looked around at the link you provided and feel a bit confused; I'm not sure which test does this or how I'd reproduce.

I'm confused by the test as well. As ra/rebase-i-more-options only touched the sequencer then any bugs would only show up in this test (which runs a non-interactive rebase) once en/rbease-backend switched to that backend. It seems likely that ra/rebase-i-more-options is to blame.

Quoted 4 lines

Jonathan - do you happen to know if your users create empty commits at all? and if so what do they expect rebase to do with them (and any that become empty when they are rebased) - cf
https://lore.kernel.org/git/<CABPp-BEH=9qejeqysHYE+AJ+JPaBympZizq-bx_OjArYFa4xUQ@mail.gmail.com>

The double @ looks very funny. I would be interested in seeing an MCVE.

Quoted 7 lines
Quoted 7 lines
Quoted 6 lines

4. I suspect the exit status in the "you need to resolve conflicts" case has changed. With rebase --am, [3] would automatically invoke rebase --abort when conflicts are present, but with rebase --merge it does not.

Known?

Quoted 8 lines
Quoted 4 lines
Quoted 4 lines

Nope, but I would certainly hope that "you need to resolve conflicts" would result in a non-zero exit status. If it doesn't, that sounds like a bug in the interactive backend that we need to fix. I'll dig in.

Yes, exiting with status 0 would be a major bug, and I think it might even be a bug that was introduced by me when I re-implemented the core loop of the interactive rebase in C.

Ooh, that sounds interesting. Do you have any more details? My simple testing here shows that we exit with status 1, so we shouldn't have that problem unless perhaps there was something else in next (ra/rebase-i-more-options??) or some other special conditions that was causing it.

Quoted 4 lines

But to me it sounds as if 4. is not so about the exit code but about aborting immediately. I do not recall seeing --am rebases to abort, though, but to exit with error (and I saw the same behavior in interactive rebases).

The aborting "immediately" wasn't done by git but by the code calling git; following Jonathan's link you'll see the code they are complaining about is:

func tryRebase(jirix *jiri.X, project Project, branch string) (bool, error) {
scm := gitutil.New(jirix, gitutil.RootDirOpt(project.Path)) if err := scm.Rebase(branch); err != nil {
err := scm.RebaseAbort()
return false, err
}
return true, nil
}

but of course, the scm.Rebase() and scm.RebaseAbort() definitions aren't within that file so I have no idea what is really being run.

Quoted 2 lines

We will need to see a reduced concrete example (preferably as a new test case) of the described behavior.

Yes, please.

#76 lore raw

Re: [PATCH v3 15/15] rebase: change the default backend from "am" to "merge"

Quoted 5 lines · Elijah Newren

Then omit calling the
post-commit hook and it behaves the same as the am backend and no one in the world notices because no one in the world uses or cares about that hook except a few people at Google who happen to be used to the am-backend

Just responding to this part: I know this was a bit of thinking out loud, but the "just a few people at Google" bit is counter-productive. The search Emily ran
<https://github.com/search?l=&q=filename%3A%2Apost-commit%2A&type=Code>
shows that it's fairly common to use a post-commit hook for deployment, with scripts like

	#!/bin/bash
	unset GIT_INDEX_FILE
	git --work-tree=/var/www/html --git-dir=/home/daniel/proj/.git checkout -f

or

#!/bin/bash
# Sync gh-pages branch with master
#########################################
git checkout gh-pages
git rm -rf -q .
git checkout master -- .
git add .
git commit -am "Syncing gh-pages with master" git checkout master

And I'm not saying that selfishly --- obviously, from a selfish perspective, what you're proposing would change behavior the least and I'd end up with happy users. :) I'm just trying to help with updating the list's collective model of user behavior.

(Actually, I want to remove jiri's post-commit hook --- so it is only the example that revealed this behavior change and is not my motivation for continuing to chime in in this thread.)

The deployment examples above seem like examples where the user would want the script to run on "git am" (and on "git merge --ff-only", for that matter) but not on the intermediate commits in "git rebase", since when rebasing a multi-commit series, deploying earlier rebased commits would cause the deployment to lose the benefit of later fixes.

[...]

Quoted 5 lines

Ooh, that sounds interesting. Do you have any more details? My simple testing here shows that we exit with status 1, so we shouldn't have that problem unless perhaps there was something else in next (ra/rebase-i-more-options??) or some other special conditions that was causing it.

I can set aside some more time to investigate that one early next week.

Thanks for the quick answers --- it's been very helpful.

Sincerely,
Jonathan

#77 lore raw

Re: [PATCH v3 15/15] rebase: change the default backend from "am" to "merge"

Quoted 9 lines · Jonathan Nieder

#!/bin/bash
# Sync gh-pages branch with master
#########################################
git checkout gh-pages
git rm -rf -q .
git checkout master -- .
git add .
git commit -am "Syncing gh-pages with master" git checkout master

Correction: I misread this one. It was guarded with

	if [ `git rev-parse --abbrev-ref HEAD` == "master" ]; then
		...
	fi

which makes all the difference. Another example is to compute ctags:

	#!/bin/sh

	~/bin/hook_ctags.sh >/dev/null 2>&1 &

Here's a fun one that updates a timesheet:
https://github.com/GokhanArik/git-hooks-timesheet

Sorry for the noise,
Jonathan

#87 lore raw

Re: [PATCH v3 15/15] rebase: change the default backend from "am" to "merge"

In reply to Elijah Newren

Hi Elijah,

Quoted 26 lines · Elijah Newren
Quoted 15 lines · Johannes Schindelin
Quoted 12 lines · Phillip Wood
Quoted 11 lines · Elijah Newren
Quoted 6 lines · Jonathan Nieder

4. I suspect the exit status in the "you need to resolve conflicts" case has changed. With rebase --am, [3] would automatically invoke rebase --abort when conflicts are present, but with rebase --merge it does not.

Known?

Quoted 8 lines
Quoted 4 lines
Quoted 4 lines

Nope, but I would certainly hope that "you need to resolve conflicts" would result in a non-zero exit status. If it doesn't, that sounds like a bug in the interactive backend that we need to fix. I'll dig in.

Yes, exiting with status 0 would be a major bug, and I think it might even be a bug that was introduced by me when I re-implemented the core loop of the interactive rebase in C.

Quoted 5 lines

Ooh, that sounds interesting. Do you have any more details? My simple testing here shows that we exit with status 1, so we shouldn't have that problem unless perhaps there was something else in next (ra/rebase-i-more-options??) or some other special conditions that was causing it.

Oh, I did not mean to say that it exits with status 0. I meant to say that _if_ it does, it would be terrible, and that my suspicion would be that the re-implementation in C introduced that bug. But as you say, there is no bug, so for once, the re-implementation in C is not at fault! ;-)

Ciao,
Dscho

#85 lore raw

Re: [PATCH v3 15/15] rebase: change the default backend from "am" to "merge"

In reply to Jonathan Nieder
Quoted 11 lines · Jonathan Nieder
Quoted 5 lines · Elijah Newren

Then omit calling the
post-commit hook and it behaves the same as the am backend and no one in the world notices because no one in the world uses or cares about that hook except a few people at Google who happen to be used to the am-backend

Just responding to this part: I know this was a bit of thinking out loud, but the "just a few people at Google" bit is counter-productive.

Yes, you're right, I should do better. Let me try again to express my thoughts in a more constructive way...

Quoted 4 lines

The search Emily ran
<https://github.com/search?l=&q=filename%3A%2Apost-commit%2A&type=Code>
shows that it's fairly common to use a post-commit hook for deployment,

* I actually looked through a dozen or two of those yesterday. I came away assuming that many of those might be svn hooks
(http://svnbook.red-bean.com/en/1.7/svn.ref.reposhooks.post-commit.html)
or for other non-git version control systems. Those that were for git didn't seem like they'd matter whether or not they were called by rebase. Which led me down a thought experiment...

* sequencer.c has accumulated a bit of cruft. Partially this was because it's conversion to c was somewhat minimal, e.g. it'd still fork other processes. One of these other processes is "git commit". I think it's been part of the plan all along to remove any forked processes from sequencer.c and other things that were converted from shell.

* What if, before being aware of this post-commit hook, someone had removed the forking of "git commit" from sequencer.c? I think it nearly certain that whoever did that wouldn't realize that strict back-compat would involve adding a call to a post-commit hook when they did that, and thus all the non-am-based rebases and cherry-picks would suddenly stop calling that hook. Would anyone notice?

* People have been using the interactive and merge backends for years for lots of different rebase types with more and more joining. We've never had a report that users were annoyed by it calling the post-commit hook. But, on the other hand, rebase-am has run for years without ever calling the post-commit hook and no one has complained. I don't believe interactivity is the deciding factor here, as we have several non-interactive types of rebases implemented by the interactive backend (--keep-empty, --exec, --rebase-merges, --preserve-merges, --strategy, --merge) all of which are more similar in usecase to the am-backend than to the explicitly interactive usecase from a user perspective (i.e. "Just run on all the commits" vs. "Let me edit and rearrange and drop and insert and whatnot") and thus for which I'd assume similar user expectations for what hooks if any to call.

* When the issue came up, Junio argued that "post-commit" can easily be argued to mean "whenever a commit is created". That seemed eminently reasonable to me. On the flip side, we do have both "post-commit" and "post-rewrite" hooks, which could argue that you'd want to handle brand new commits ("git commit", "git merge") differently than tweaking existing ones ("git rebase", "git cherry-pick"), though explicitly interactive rebases are a monkey-wrench here since they can be used to insert brand new commits in the middle of rewriting a big pile of commits (suggesting maybe that only the explicitly interactive rebases should call "post-commit"?).

* Five different people on this list have chimed in about this "post-commit" behavior, but I haven't seen a single argument for or against calling "post-commit" based on actual user expectation for this kind of usecase (e.g. if done from a clean design). All I've seen is folks talking about whether there are backward compatibility concerns and/or what could be construed by the meaning of words like "post-commit". In other words, I don't think any of us have a clue what is correct behavior if we were designing from scratch.

* Since we don't seem to understand what "correct" behavior is...how much does or would "incorrect" behavior hurt? What if we made a random judgement call about the post-commit hook and got it wrong? Well...

* If we changed am to also call the post-commit hook, we have some
early signal that there might be some folks who could comment, though I haven't yet seen a case where people would be hurt other than a possible slow-down which the user could probably fix by adjusting their scripts. And in such cases it's easy to say "post-commit" means we call the hook when a new commit is created; if you're doing something weird where you don't want to do work during the middle of a rebase your script should check for that case -- it has always been called by other rebase backends anyway.

* If we changed sequencer.c to stop forking a git-commit process
and by proxy missed the post-commit hook, I strongly suspect no one would notice or care. But if they did, we could just point out how we have both "post-commit" and "post-rewrite" hooks and that means we "fixed" our bug of calling the "post-commit" hook from other rebase/cherry-pick backends in the past by just calling the post-rewrite hook. We can tell users they should add a post-rewrite hook, and, if they want, have it call their post-commit hook one or more times.

* If we changed sequencer.c in a way that it stopped calling the
post-commit hook, and someone did notice/care and did have a good solid argument about correct behavior and that it should involve calling "post-commit"...then we simply add it and the world is fixed (though we have to do the explaining that post-commit means whenever a commit is created mentioned above). And we provide users with a workaround for older git versions where we tell them to have the post-rewrite hook invoke their own post-commit hook for them.

In short, while I don't know what "correct" behavior is other than that the rebase backends ought to behave the same (or at least all the am-based rebases and the non-explicitly-interactive rebases implemented by the interactive backend should all behave the same), I'm not yet seeing much damage from just picking an answer and implementing it.

All that said, I'm really, really glad Emily decided to look into this so I don't have to argue or justify it from anything more than a "backseat driver" type of perspective. :-)

Quoted 27 lines

with scripts like

        #!/bin/bash
        unset GIT_INDEX_FILE
        git --work-tree=/var/www/html --git-dir=/home/daniel/proj/.git checkout -f

or

#!/bin/bash
# Sync gh-pages branch with master
#########################################
git checkout gh-pages
git rm -rf -q .
git checkout master -- .
git add .
git commit -am "Syncing gh-pages with master" git checkout master

And I'm not saying that selfishly --- obviously, from a selfish perspective, what you're proposing would change behavior the least and I'd end up with happy users. :) I'm just trying to help with updating the list's collective model of user behavior.

(Actually, I want to remove jiri's post-commit hook --- so it is only the example that revealed this behavior change and is not my motivation for continuing to chime in in this thread.)

Quoted 5 lines

The deployment examples above seem like examples where the user would want the script to run on "git am" (and on "git merge --ff-only", for that matter) but not on the intermediate commits in "git rebase", since when rebasing a multi-commit series, deploying earlier rebased commits would cause the deployment to lose the benefit of later fixes.

With your correction later in the thread that it's protected by current branch == "master", this example doesn't concern me as much. The ctags case is more interesting, but it's currently running on every commit of rebase using the interactive backend already, and while it might slow things down to run on every commit it wouldn't break anything. Plus, users can easily add a detached-head or in-the-middle-of-rebase check to their hook to avoid such a slow down if it is enough for them to notice so I'm not seeing much harm yet.

So, these are definitely interesting cases, but I'm still not seeing arguments about whether the hook should be called from a user perspective, just whether users might in the short term be adversely affected due to assuming they previously built up scripts assuming only certain types of rebases would ever be run. (Not that I'm trying to discount the short term, because it obviously matters, but that I'm worried we're letting it be the sole deciding factor rather than thinking of correct behavior and possibly introducing transition plans and whatnot if needed.)

Quoted 9 lines

[...]

Quoted 5 lines

Ooh, that sounds interesting. Do you have any more details? My simple testing here shows that we exit with status 1, so we shouldn't have that problem unless perhaps there was something else in next (ra/rebase-i-more-options??) or some other special conditions that was causing it.

I can set aside some more time to investigate that one early next week.

That'd be great, thanks.

Quoted 4 lines

Thanks for the quick answers --- it's been very helpful.

Sincerely,
Jonathan

#78 lore raw

Re: [PATCH v3 15/15] rebase: change the default backend from "am" to "merge"

In reply to Jonathan Nieder

Hi,

Quoted 6 lines · Jonathan Nieder

which makes all the difference. Another example is to compute ctags:

        #!/bin/sh

        ~/bin/hook_ctags.sh >/dev/null 2>&1 &

Thought about this ctags case a bit more. In contrast to the deployment cases you brought up (which appear to be protected by a branch check anyway), doesn't this one argue more that post-commit should be run for all rebase types? Sure, it'd take more computation time, but if a user runs "git pull --rebase" and then is frustrated that all their tags are extremely out of date then they may be kind of frustrated.

Of course, arguing against myself, we could always tell them to just implement a post-rewrite hook.

Quoted 2 lines

Here's a fun one that updates a timesheet:
https://github.com/GokhanArik/git-hooks-timesheet

So...cherry-picking commits will pad your timesheet with duplicate work on the same topic (because cherry-pick calls sequencer which calls "git commit"), and so will interactive-based rebases, but rebase-am (the default rebase in the past) wouldn't. Fun indeed...

Wouldn't such users view removing the post-commit hook from sequencer as "harmful to their productivity"? And shouldn't we add the post-commit hook to rebase-am in order to "increase their productivity"? ;-)

Quoted 1 line

Sorry for the noise,

No, these were great examples; thanks for pointing them out. My commentary about the timesheet is the only thing that is noise here. :-)

#86 lore raw

Re: [PATCH v3 15/15] rebase: change the default backend from "am" to "merge"

In reply to Jonathan Nieder
Quoted 5 lines · Jonathan Nieder

The deployment examples above seem like examples where the user would want the script to run on "git am" (and on "git merge --ff-only", for that matter) but not on the intermediate commits in "git rebase", since when rebasing a multi-commit series, deploying earlier rebased commits would cause the deployment to lose the benefit of later fixes.

Agree. The same thing can be said to "git am" accepting a series of multiple patches, though.

#79 lore raw

Re: [PATCH v3 15/15] rebase: change the default backend from "am" to "merge"

In reply to Elijah Newren
Quoted 3 lines · Elijah Newren

Thought about this ctags case a bit more. In contrast to the deployment cases you brought up (which appear to be protected by a branch check anyway),

To be clear, one of the two deployment cases I mentioned (gh-pages) is protected by a branch check, but the other (git --work-tree=srvroot checkout) is not.

Quoted 5 lines

doesn't this one argue more that post-commit
should be run for all rebase types? Sure, it'd take more computation time, but if a user runs "git pull --rebase" and then is frustrated that all their tags are extremely out of date then they may be kind of frustrated.

Yes. In fact I think all of these examples argue for having the post-commit hook run once on rebase completion (and on "git am" completion, "git merge --ff-only" completion, etc).

Of course it's not hard to find other examples that argue against that. We provided a hook "here's a notification that runs after git commit runs" without tailoring it to a particular need, and on one hand people came up with various creative ways to use it (yay!) but on the other hand it's hard to maintain as Git's ways of making new commits evolve.

Quoted 2 lines

Of course, arguing against myself, we could always tell them to just implement a post-rewrite hook.

That's an interesting facet I hadn't thought about before.

It's tempting since it kind of minimizes the blast surface while providing people a way to keep doing what they had wanted.

Historically "git cherry-pick" ran "git commit", causing it to run the post-commit hook. There's some related discussion at
https://lore.kernel.org/git/[email protected]/
of other side effects of having run "git commit".

... oh! git cherry-pick doesn't run post-rewrite. Interesting.

To summarize what we've discussed so far:

A. run post-commit hook consistently in rebase --am and --merge modes

Advantages:
* consistent with historic "rebase -i" behavior
* supports hooks like ctags generation that want to update state to match what has been committed in the worktree (especially when preparing for the rebase to stop due to conflicts)
* consistent with other commands like "git cherry-pick" that run the post-commit hook

Disadvantages:
* invokes hooks more often in a setting they are not used to being invoked in
* slows down rebase
* when post-commit hooks are used for deployment, exposes intermediate states in the middle of a rebase to the deployment environment
* inconsistent with "git am"

Mitigations:
* could go through the normal warn about change, opt-in config, opt-out config cycle to make the change smoother
* could provide new hooks (e.g., one run after a batch of objects is created for applications similar to "git gc --auto", one run when HEAD is updated for applications like the ctags one, one run on ref update for applications like the deployment case) and encourage authors to migrate to them
* could run the post-commit hook in "git am", too

B. stop running the post-commit hook in rebase

Advantages:
* makes --am and --merge consistent with minimal user impact

Disadvantages:
* meaning of the post-commit hook remains a muddle
* in particular, "git am" and "git rebase" would not be consistent with "git cherry-pick"

Mitigations:
* could provide new hooks for people to migrate to to replace that muddle

C. stop running the post-commit hook in rebase --merge or commit --amend

Advantages:
* produces a consistent definition of post-commit ("it's about new commits only")
* hook authors get the same expressiveness using the post-rewrite hook

Disadvantages:
* the change to historical "git commit --amend" behavior is likely to be surprising

Mitigations:
* could go through the warn, opt-in config, opt-out config cycle

Any bits I've missed?

Thanks,
Jonathan