git@vger.kernel.org mailing list mirror (one of many)
 help / Atom feed
* [PATCH 1/8] sequencer: introduce new commands to reset the revision
  2018-01-18 15:35 [PATCH 0/8] rebase -i: offer to recreate merge commits Johannes Schindelin
@ 2018-01-18 15:35 ` Johannes Schindelin
  2018-01-18 16:25   ` Jacob Keller
                     ` (2 more replies)
  2018-01-18 15:35 ` [PATCH 2/8] sequencer: introduce the `merge` command Johannes Schindelin
                   ` (11 subsequent siblings)
  12 siblings, 3 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-01-18 15:35 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Jacob Keller

In the upcoming commits, we will teach the sequencer to recreate merges.
This will be done in a very different way from the unfortunate design of
`git rebase --preserve-merges` (which does not allow for reordering
commits, or changing the branch topology).

The main idea is to introduce new todo list commands, to support
labeling the current revision with a given name, resetting the current
revision to a previous state, merging labeled revisions.

This idea was developed in Git for Windows' Git garden shears (that are
used to maintain the "thicket of branches" on top of upstream Git), and
this patch is part of the effort to make it available to a wider
audience, as well as to make the entire process more robust (by
implementing it in a safe and portable language rather than a Unix shell
script).

This commit implements the commands to label, and to reset to, given
revisions. The syntax is:

	label <name>
	reset <name>

As a convenience shortcut, also to improve readability of the generated
todo list, a third command is introduced: bud. It simply resets to the
"onto" revision, i.e. the commit onto which we currently rebase.

Internally, the `label <name>` command creates the ref
`refs/rewritten/<name>`. This makes it possible to work with the labeled
revisions interactively, or in a scripted fashion (e.g. via the todo
list command `exec`).

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 git-rebase--interactive.sh |   3 +
 sequencer.c                | 181 ++++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 180 insertions(+), 4 deletions(-)

diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh
index d47bd29593a..3d2cd19d65a 100644
--- a/git-rebase--interactive.sh
+++ b/git-rebase--interactive.sh
@@ -162,6 +162,9 @@ s, squash = use commit, but meld into previous commit
 f, fixup = like \"squash\", but discard this commit's log message
 x, exec = run command (the rest of the line) using shell
 d, drop = remove commit
+l, label = label current HEAD with a name
+t, reset = reset HEAD to a label
+b, bud = reset HEAD to the revision labeled 'onto'
 
 These lines can be re-ordered; they are executed from top to bottom.
 " | git stripspace --comment-lines >>"$todo"
diff --git a/sequencer.c b/sequencer.c
index 4d3f60594cb..91cc55a002f 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -21,6 +21,8 @@
 #include "log-tree.h"
 #include "wt-status.h"
 #include "hashmap.h"
+#include "unpack-trees.h"
+#include "worktree.h"
 
 #define GIT_REFLOG_ACTION "GIT_REFLOG_ACTION"
 
@@ -116,6 +118,13 @@ static GIT_PATH_FUNC(rebase_path_stopped_sha, "rebase-merge/stopped-sha")
 static GIT_PATH_FUNC(rebase_path_rewritten_list, "rebase-merge/rewritten-list")
 static GIT_PATH_FUNC(rebase_path_rewritten_pending,
 	"rebase-merge/rewritten-pending")
+
+/*
+ * The path of the file listing refs that need to be deleted after the rebase
+ * finishes. This is used by the `merge` command.
+ */
+static GIT_PATH_FUNC(rebase_path_refs_to_delete, "rebase-merge/refs-to-delete")
+
 /*
  * The following files are written by git-rebase just after parsing the
  * command-line (and are only consumed, not modified, by the sequencer).
@@ -767,6 +776,9 @@ enum todo_command {
 	TODO_SQUASH,
 	/* commands that do something else than handling a single commit */
 	TODO_EXEC,
+	TODO_LABEL,
+	TODO_RESET,
+	TODO_BUD,
 	/* commands that do nothing but are counted for reporting progress */
 	TODO_NOOP,
 	TODO_DROP,
@@ -785,6 +797,9 @@ static struct {
 	{ 'f', "fixup" },
 	{ 's', "squash" },
 	{ 'x', "exec" },
+	{ 'l', "label" },
+	{ 't', "reset" },
+	{ 'b', "bud" },
 	{ 0,   "noop" },
 	{ 'd', "drop" },
 	{ 0,   NULL }
@@ -1253,7 +1268,8 @@ static int parse_insn_line(struct todo_item *item, const char *bol, char *eol)
 		if (skip_prefix(bol, todo_command_info[i].str, &bol)) {
 			item->command = i;
 			break;
-		} else if (bol[1] == ' ' && *bol == todo_command_info[i].c) {
+		} else if ((bol + 1 == eol || bol[1] == ' ') &&
+			   *bol == todo_command_info[i].c) {
 			bol++;
 			item->command = i;
 			break;
@@ -1265,7 +1281,7 @@ static int parse_insn_line(struct todo_item *item, const char *bol, char *eol)
 	padding = strspn(bol, " \t");
 	bol += padding;
 
-	if (item->command == TODO_NOOP) {
+	if (item->command == TODO_NOOP || item->command == TODO_BUD) {
 		if (bol != eol)
 			return error(_("%s does not accept arguments: '%s'"),
 				     command_to_string(item->command), bol);
@@ -1279,7 +1295,8 @@ static int parse_insn_line(struct todo_item *item, const char *bol, char *eol)
 		return error(_("missing arguments for %s"),
 			     command_to_string(item->command));
 
-	if (item->command == TODO_EXEC) {
+	if (item->command == TODO_EXEC || item->command == TODO_LABEL ||
+	    item->command == TODO_RESET) {
 		item->commit = NULL;
 		item->arg = bol;
 		item->arg_len = (int)(eol - bol);
@@ -1919,6 +1936,139 @@ static int do_exec(const char *command_line)
 	return status;
 }
 
+static int safe_append(const char *filename, const char *fmt, ...)
+{
+	va_list ap;
+	struct lock_file lock = LOCK_INIT;
+	int fd = hold_lock_file_for_update(&lock, filename, 0);
+	struct strbuf buf = STRBUF_INIT;
+
+	if (fd < 0)
+		return error_errno(_("could not lock '%s'"), filename);
+
+	if (strbuf_read_file(&buf, filename, 0) < 0 && errno != ENOENT)
+		return error_errno(_("could not read '%s'"), filename);
+	strbuf_complete(&buf, '\n');
+	va_start(ap, fmt);
+	strbuf_vaddf(&buf, fmt, ap);
+	va_end(ap);
+
+	if (write_in_full(fd, buf.buf, buf.len) < 0) {
+		rollback_lock_file(&lock);
+		return error_errno(_("could not write to '%s'"), filename);
+	}
+	if (commit_lock_file(&lock) < 0) {
+		rollback_lock_file(&lock);
+		return error(_("failed to finalize '%s'."), filename);
+	}
+
+	return 0;
+}
+
+static int do_label(const char *name, int len)
+{
+	struct ref_store *refs = get_main_ref_store();
+	struct ref_transaction *transaction;
+	struct strbuf ref_name = STRBUF_INIT, err = STRBUF_INIT;
+	struct strbuf msg = STRBUF_INIT;
+	int ret = 0;
+	struct object_id head_oid;
+
+	strbuf_addf(&ref_name, "refs/rewritten/%.*s", len, name);
+	strbuf_addf(&msg, "label '%.*s'", len, name);
+
+	transaction = ref_store_transaction_begin(refs, &err);
+	if (!transaction ||
+	    get_oid("HEAD", &head_oid) ||
+	    ref_transaction_update(transaction, ref_name.buf, &head_oid, NULL,
+				   0, msg.buf, &err) < 0 ||
+	    ref_transaction_commit(transaction, &err)) {
+		error("%s", err.buf);
+		ret = -1;
+	}
+	ref_transaction_free(transaction);
+	strbuf_release(&err);
+	strbuf_release(&msg);
+
+	if (!ret)
+		ret = safe_append(rebase_path_refs_to_delete(),
+				  "%s\n", ref_name.buf);
+	strbuf_release(&ref_name);
+
+	return ret;
+}
+
+static int do_reset(const char *name, int len)
+{
+	struct strbuf ref_name = STRBUF_INIT;
+	struct object_id oid;
+	struct lock_file lock = LOCK_INIT;
+	struct tree_desc desc;
+	struct tree *tree;
+	struct unpack_trees_options opts;
+	int ret = 0, i;
+
+	if (hold_locked_index(&lock, LOCK_REPORT_ON_ERROR) < 0)
+		return -1;
+
+	for (i = 0; i < len; i++)
+		if (isspace(name[i]))
+			len = i;
+
+	strbuf_addf(&ref_name, "refs/rewritten/%.*s", len, name);
+	if (get_oid(ref_name.buf, &oid) &&
+	    get_oid(ref_name.buf + strlen("refs/rewritten/"), &oid)) {
+		error(_("could not read '%s'"), ref_name.buf);
+		rollback_lock_file(&lock);
+		strbuf_release(&ref_name);
+		return -1;
+	}
+
+	memset(&opts, 0, sizeof(opts));
+	opts.head_idx = 1;
+	opts.src_index = &the_index;
+	opts.dst_index = &the_index;
+	opts.fn = oneway_merge;
+	opts.merge = 1;
+	opts.update = 1;
+	opts.reset = 1;
+
+	read_cache_unmerged();
+	if (!fill_tree_descriptor(&desc, &oid)) {
+		error(_("Failed to find tree of %s."), oid_to_hex(&oid));
+		rollback_lock_file(&lock);
+		free((void *)desc.buffer);
+		strbuf_release(&ref_name);
+		return -1;
+	}
+
+	if (unpack_trees(1, &desc, &opts)) {
+		rollback_lock_file(&lock);
+		free((void *)desc.buffer);
+		strbuf_release(&ref_name);
+		return -1;
+	}
+
+	tree = parse_tree_indirect(&oid);
+	prime_cache_tree(&the_index, tree);
+
+	if (write_locked_index(&the_index, &lock, COMMIT_LOCK) < 0)
+		ret = error(_("could not write index"));
+	free((void *)desc.buffer);
+
+	if (!ret) {
+		struct strbuf msg = STRBUF_INIT;
+
+		strbuf_addf(&msg, "(rebase -i) reset '%.*s'", len, name);
+		ret = update_ref(msg.buf, "HEAD", &oid, NULL, 0,
+				 UPDATE_REFS_MSG_ON_ERR);
+		strbuf_release(&msg);
+	}
+
+	strbuf_release(&ref_name);
+	return ret;
+}
+
 static int is_final_fixup(struct todo_list *todo_list)
 {
 	int i = todo_list->current;
@@ -2102,7 +2252,13 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts)
 				/* `current` will be incremented below */
 				todo_list->current = -1;
 			}
-		} else if (!is_noop(item->command))
+		} else if (item->command == TODO_LABEL)
+			res = do_label(item->arg, item->arg_len);
+		else if (item->command == TODO_RESET)
+			res = do_reset(item->arg, item->arg_len);
+		else if (item->command == TODO_BUD)
+			res = do_reset("onto", 4);
+		else if (!is_noop(item->command))
 			return error(_("unknown command %d"), item->command);
 
 		todo_list->current++;
@@ -2207,6 +2363,23 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts)
 		}
 		apply_autostash(opts);
 
+		strbuf_reset(&buf);
+		if (strbuf_read_file(&buf, rebase_path_refs_to_delete(), 0)
+		    > 0) {
+			char *p = buf.buf;
+			while (*p) {
+				char *eol = strchr(p, '\n');
+				if (eol)
+					*eol = '\0';
+				if (delete_ref("(rebase -i) cleanup",
+					       p, NULL, 0) < 0)
+					warning(_("could not delete '%s'"), p);
+				if (!eol)
+					break;
+				p = eol + 1;
+			}
+		}
+
 		fprintf(stderr, "Successfully rebased and updated %s.\n",
 			head_ref.buf);
 
-- 
2.15.1.windows.2.1430.ga56c4f9e2a9



^ permalink raw reply	[flat|nested] 276+ messages in thread

* [PATCH 2/8] sequencer: introduce the `merge` command
  2018-01-18 15:35 [PATCH 0/8] rebase -i: offer to recreate merge commits Johannes Schindelin
  2018-01-18 15:35 ` [PATCH 1/8] sequencer: introduce new commands to reset the revision Johannes Schindelin
@ 2018-01-18 15:35 ` Johannes Schindelin
  2018-01-18 16:31   ` Jacob Keller
                     ` (3 more replies)
  2018-01-18 15:35 ` [PATCH 3/8] sequencer: fast-forward merge commits, if possible Johannes Schindelin
                   ` (10 subsequent siblings)
  12 siblings, 4 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-01-18 15:35 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Jacob Keller

This patch is part of the effort to reimplement `--preserve-merges` with
a substantially improved design, a design that has been developed in the
Git for Windows project to maintain the dozens of Windows-specific patch
series on top of upstream Git.

The previous patch implemented the `label`, `bud` and `reset` commands
to label commits and to reset to a labeled commits. This patch adds the
`merge` command, with the following syntax:

	merge <commit> <rev> <oneline>

The <commit> parameter in this instance is the *original* merge commit,
whose author and message will be used for the to-be-created merge
commit.

The <rev> parameter refers to the (possibly rewritten) revision to
merge. Let's see an example of a todo list:

	label onto

	# Branch abc
	bud
	pick deadbeef Hello, world!
	label abc

	bud
	pick cafecafe And now for something completely different
	merge baaabaaa abc Merge the branch 'abc' into master

To support creating *new* merges, i.e. without copying the commit
message from an existing commit, use the special value `-` as <commit>
parameter (in which case the text after the <rev> parameter is used as
commit message):

	merge - abc This will be the actual commit message of the merge

This comes in handy when splitting a branch into two or more branches.

Note: this patch only adds support for recursive merges, to keep things
simple. Support for octopus merges will be added later in this patch
series, support for merges using strategies other than the recursive
merge is left for future contributions.

The design of the `merge` command as introduced by this patch only
supports creating new merge commits with exactly two parents, i.e. it
adds no support for octopus merges.

We will introduce support for octopus merges in a later commit.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 git-rebase--interactive.sh |   1 +
 sequencer.c                | 146 +++++++++++++++++++++++++++++++++++++++++++--
 2 files changed, 143 insertions(+), 4 deletions(-)

diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh
index 3d2cd19d65a..5bf1ea3781f 100644
--- a/git-rebase--interactive.sh
+++ b/git-rebase--interactive.sh
@@ -165,6 +165,7 @@ d, drop = remove commit
 l, label = label current HEAD with a name
 t, reset = reset HEAD to a label
 b, bud = reset HEAD to the revision labeled 'onto'
+m, merge = create a merge commit using a given commit's message
 
 These lines can be re-ordered; they are executed from top to bottom.
 " | git stripspace --comment-lines >>"$todo"
diff --git a/sequencer.c b/sequencer.c
index 91cc55a002f..567cfcbbe8b 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -779,6 +779,7 @@ enum todo_command {
 	TODO_LABEL,
 	TODO_RESET,
 	TODO_BUD,
+	TODO_MERGE,
 	/* commands that do nothing but are counted for reporting progress */
 	TODO_NOOP,
 	TODO_DROP,
@@ -800,6 +801,7 @@ static struct {
 	{ 'l', "label" },
 	{ 't', "reset" },
 	{ 'b', "bud" },
+	{ 'm', "merge" },
 	{ 0,   "noop" },
 	{ 'd', "drop" },
 	{ 0,   NULL }
@@ -1304,14 +1306,20 @@ static int parse_insn_line(struct todo_item *item, const char *bol, char *eol)
 	}
 
 	end_of_object_name = (char *) bol + strcspn(bol, " \t\n");
+	item->arg = end_of_object_name + strspn(end_of_object_name, " \t");
+	item->arg_len = (int)(eol - item->arg);
+
 	saved = *end_of_object_name;
+	if (item->command == TODO_MERGE && *bol == '-' &&
+	    bol + 1 == end_of_object_name) {
+		item->commit = NULL;
+		return 0;
+	}
+
 	*end_of_object_name = '\0';
 	status = get_oid(bol, &commit_oid);
 	*end_of_object_name = saved;
 
-	item->arg = end_of_object_name + strspn(end_of_object_name, " \t");
-	item->arg_len = (int)(eol - item->arg);
-
 	if (status < 0)
 		return -1;
 
@@ -2069,6 +2077,132 @@ static int do_reset(const char *name, int len)
 	return ret;
 }
 
+static int do_merge(struct commit *commit, const char *arg, int arg_len,
+		    struct replay_opts *opts)
+{
+	int merge_arg_len;
+	struct strbuf ref_name = STRBUF_INIT;
+	struct commit *head_commit, *merge_commit, *i;
+	struct commit_list *common, *j, *reversed = NULL;
+	struct merge_options o;
+	int ret;
+	static struct lock_file lock;
+
+	for (merge_arg_len = 0; merge_arg_len < arg_len; merge_arg_len++)
+		if (isspace(arg[merge_arg_len]))
+			break;
+
+	if (hold_locked_index(&lock, LOCK_REPORT_ON_ERROR) < 0)
+		return -1;
+
+	if (commit) {
+		const char *message = get_commit_buffer(commit, NULL);
+		const char *body;
+		int len;
+
+		if (!message) {
+			rollback_lock_file(&lock);
+			return error(_("could not get commit message of '%s'"),
+				     oid_to_hex(&commit->object.oid));
+		}
+		write_author_script(message);
+		find_commit_subject(message, &body);
+		len = strlen(body);
+		if (write_message(body, len, git_path_merge_msg(), 0) < 0) {
+			error_errno(_("Could not write '%s'"),
+				    git_path_merge_msg());
+			unuse_commit_buffer(commit, message);
+			rollback_lock_file(&lock);
+			return -1;
+		}
+		unuse_commit_buffer(commit, message);
+	} else {
+		const char *p = arg + merge_arg_len;
+		struct strbuf buf = STRBUF_INIT;
+		int len;
+
+		strbuf_addf(&buf, "author %s", git_author_info(0));
+		write_author_script(buf.buf);
+		strbuf_reset(&buf);
+
+		p += strspn(p, " \t");
+		if (*p)
+			len = strlen(p);
+		else {
+			strbuf_addf(&buf, "Merge branch '%.*s'",
+				    merge_arg_len, arg);
+			p = buf.buf;
+			len = buf.len;
+		}
+
+		if (write_message(p, len, git_path_merge_msg(), 0) < 0) {
+			error_errno(_("Could not write '%s'"),
+				    git_path_merge_msg());
+			strbuf_release(&buf);
+			rollback_lock_file(&lock);
+			return -1;
+		}
+		strbuf_release(&buf);
+	}
+
+	head_commit = lookup_commit_reference_by_name("HEAD");
+	if (!head_commit) {
+		rollback_lock_file(&lock);
+		return error(_("Cannot merge without a current revision"));
+	}
+
+	strbuf_addf(&ref_name, "refs/rewritten/%.*s", merge_arg_len, arg);
+	merge_commit = lookup_commit_reference_by_name(ref_name.buf);
+	if (!merge_commit) {
+		/* fall back to non-rewritten ref or commit */
+		strbuf_splice(&ref_name, 0, strlen("refs/rewritten/"), "", 0);
+		merge_commit = lookup_commit_reference_by_name(ref_name.buf);
+	}
+	if (!merge_commit) {
+		error(_("could not resolve '%s'"), ref_name.buf);
+		strbuf_release(&ref_name);
+		rollback_lock_file(&lock);
+		return -1;
+	}
+	write_message(oid_to_hex(&merge_commit->object.oid), GIT_SHA1_HEXSZ,
+		      git_path_merge_head(), 0);
+	write_message("no-ff", 5, git_path_merge_mode(), 0);
+
+	common = get_merge_bases(head_commit, merge_commit);
+	for (j = common; j; j = j->next)
+		commit_list_insert(j->item, &reversed);
+	free_commit_list(common);
+
+	read_cache();
+	init_merge_options(&o);
+	o.branch1 = "HEAD";
+	o.branch2 = ref_name.buf;
+	o.buffer_output = 2;
+
+	ret = merge_recursive(&o, head_commit, merge_commit, reversed, &i);
+	if (ret <= 0)
+		fputs(o.obuf.buf, stdout);
+	strbuf_release(&o.obuf);
+	if (ret < 0) {
+		strbuf_release(&ref_name);
+		rollback_lock_file(&lock);
+		return error(_("conflicts while merging '%.*s'"),
+			     merge_arg_len, arg);
+	}
+
+	if (active_cache_changed &&
+	    write_locked_index(&the_index, &lock, COMMIT_LOCK)) {
+		strbuf_release(&ref_name);
+		return error(_("merge: Unable to write new index file"));
+	}
+	rollback_lock_file(&lock);
+
+	ret = run_git_commit(git_path_merge_msg(), opts, 0);
+	strbuf_release(&ref_name);
+
+	return ret;
+}
+
 static int is_final_fixup(struct todo_list *todo_list)
 {
 	int i = todo_list->current;
@@ -2258,6 +2392,9 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts)
 			res = do_reset(item->arg, item->arg_len);
 		else if (item->command == TODO_BUD)
 			res = do_reset("onto", 4);
+		else if (item->command == TODO_MERGE)
+			res = do_merge(item->commit,
+				       item->arg, item->arg_len, opts);
 		else if (!is_noop(item->command))
 			return error(_("unknown command %d"), item->command);
 
@@ -2757,7 +2894,8 @@ int transform_todos(unsigned flags)
 					  oid_to_hex(&item->commit->object.oid);
 
 			strbuf_addf(&buf, " %s", oid);
-		}
+		} else if (item->command == TODO_MERGE)
+			strbuf_addstr(&buf, " -");
 		/* add all the rest */
 		if (!item->arg_len)
 			strbuf_addch(&buf, '\n');
-- 
2.15.1.windows.2.1430.ga56c4f9e2a9



^ permalink raw reply	[flat|nested] 276+ messages in thread

* [PATCH 3/8] sequencer: fast-forward merge commits, if possible
  2018-01-18 15:35 [PATCH 0/8] rebase -i: offer to recreate merge commits Johannes Schindelin
  2018-01-18 15:35 ` [PATCH 1/8] sequencer: introduce new commands to reset the revision Johannes Schindelin
  2018-01-18 15:35 ` [PATCH 2/8] sequencer: introduce the `merge` command Johannes Schindelin
@ 2018-01-18 15:35 ` Johannes Schindelin
  2018-01-19 14:53   ` Phillip Wood
  2018-01-23 18:51   ` Junio C Hamano
  2018-01-18 15:35 ` [PATCH 4/8] rebase-helper --make-script: introduce a flag to recreate merges Johannes Schindelin
                   ` (9 subsequent siblings)
  12 siblings, 2 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-01-18 15:35 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Jacob Keller

Just like with regular `pick` commands, if we are trying to recreate a
merge commit, we now test whether the parents of said commit match HEAD
and the commits to be merged, and fast-forward if possible.

This is not only faster, but also avoids unnecessary proliferation of
new objects.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 sequencer.c | 21 ++++++++++++++++++++-
 1 file changed, 20 insertions(+), 1 deletion(-)

diff --git a/sequencer.c b/sequencer.c
index 567cfcbbe8b..a96255426e7 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -2085,7 +2085,7 @@ static int do_merge(struct commit *commit, const char *arg, int arg_len,
 	struct commit *head_commit, *merge_commit, *i;
 	struct commit_list *common, *j, *reversed = NULL;
 	struct merge_options o;
-	int ret;
+	int can_fast_forward, ret;
 	static struct lock_file lock;
 
 	for (merge_arg_len = 0; merge_arg_len < arg_len; merge_arg_len++)
@@ -2151,6 +2151,14 @@ static int do_merge(struct commit *commit, const char *arg, int arg_len,
 		return error(_("Cannot merge without a current revision"));
 	}
 
+	/*
+	 * If HEAD is not identical to the parent of the original merge commit,
+	 * we cannot fast-forward.
+	 */
+	can_fast_forward = commit && commit->parents &&
+		!oidcmp(&commit->parents->item->object.oid,
+			&head_commit->object.oid);
+
 	strbuf_addf(&ref_name, "refs/rewritten/%.*s", merge_arg_len, arg);
 	merge_commit = lookup_commit_reference_by_name(ref_name.buf);
 	if (!merge_commit) {
@@ -2164,6 +2172,17 @@ static int do_merge(struct commit *commit, const char *arg, int arg_len,
 		rollback_lock_file(&lock);
 		return -1;
 	}
+
+	if (can_fast_forward && commit->parents->next &&
+	    !commit->parents->next->next &&
+	    !oidcmp(&commit->parents->next->item->object.oid,
+		    &merge_commit->object.oid)) {
+		strbuf_release(&ref_name);
+		rollback_lock_file(&lock);
+		return fast_forward_to(&commit->object.oid,
+				       &head_commit->object.oid, 0, opts);
+	}
+
 	write_message(oid_to_hex(&merge_commit->object.oid), GIT_SHA1_HEXSZ,
 		      git_path_merge_head(), 0);
 	write_message("no-ff", 5, git_path_merge_mode(), 0);
-- 
2.15.1.windows.2.1430.ga56c4f9e2a9



^ permalink raw reply	[flat|nested] 276+ messages in thread

* [PATCH 4/8] rebase-helper --make-script: introduce a flag to recreate merges
  2018-01-18 15:35 [PATCH 0/8] rebase -i: offer to recreate merge commits Johannes Schindelin
                   ` (2 preceding siblings ...)
  2018-01-18 15:35 ` [PATCH 3/8] sequencer: fast-forward merge commits, if possible Johannes Schindelin
@ 2018-01-18 15:35 ` Johannes Schindelin
  2018-01-18 21:39   ` Philip Oakley
                     ` (2 more replies)
  2018-01-18 15:35 ` [PATCH 5/8] rebase: introduce the --recreate-merges option Johannes Schindelin
                   ` (8 subsequent siblings)
  12 siblings, 3 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-01-18 15:35 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Jacob Keller

The sequencer just learned a new commands intended to recreate branch
structure (similar in spirit to --preserve-merges, but with a
substantially less-broken design).

Let's allow the rebase--helper to generate todo lists making use of
these commands, triggered by the new --recreate-merges option. For a
commit topology like this:

	A - B - C
	  \   /
	    D

the generated todo list would look like this:

	# branch D
	pick 0123 A
	label branch-point
	pick 1234 D
	label D

	reset branch-point
	pick 2345 B
	merge 3456 D C

To keep things simple, we first only implement support for merge commits
with exactly two parents, leaving support for octopus merges to a later
patch in this patch series.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 builtin/rebase--helper.c |   4 +-
 sequencer.c              | 343 ++++++++++++++++++++++++++++++++++++++++++++++-
 sequencer.h              |   1 +
 3 files changed, 345 insertions(+), 3 deletions(-)

diff --git a/builtin/rebase--helper.c b/builtin/rebase--helper.c
index 7daee544b7b..a34ab5c0655 100644
--- a/builtin/rebase--helper.c
+++ b/builtin/rebase--helper.c
@@ -12,7 +12,7 @@ static const char * const builtin_rebase_helper_usage[] = {
 int cmd_rebase__helper(int argc, const char **argv, const char *prefix)
 {
 	struct replay_opts opts = REPLAY_OPTS_INIT;
-	unsigned flags = 0, keep_empty = 0;
+	unsigned flags = 0, keep_empty = 0, recreate_merges = 0;
 	int abbreviate_commands = 0;
 	enum {
 		CONTINUE = 1, ABORT, MAKE_SCRIPT, SHORTEN_OIDS, EXPAND_OIDS,
@@ -22,6 +22,7 @@ int cmd_rebase__helper(int argc, const char **argv, const char *prefix)
 	struct option options[] = {
 		OPT_BOOL(0, "ff", &opts.allow_ff, N_("allow fast-forward")),
 		OPT_BOOL(0, "keep-empty", &keep_empty, N_("keep empty commits")),
+		OPT_BOOL(0, "recreate-merges", &recreate_merges, N_("recreate merge commits")),
 		OPT_CMDMODE(0, "continue", &command, N_("continue rebase"),
 				CONTINUE),
 		OPT_CMDMODE(0, "abort", &command, N_("abort rebase"),
@@ -55,6 +56,7 @@ int cmd_rebase__helper(int argc, const char **argv, const char *prefix)
 
 	flags |= keep_empty ? TODO_LIST_KEEP_EMPTY : 0;
 	flags |= abbreviate_commands ? TODO_LIST_ABBREVIATE_CMDS : 0;
+	flags |= recreate_merges ? TODO_LIST_RECREATE_MERGES : 0;
 	flags |= command == SHORTEN_OIDS ? TODO_LIST_SHORTEN_IDS : 0;
 
 	if (command == CONTINUE && argc == 1)
diff --git a/sequencer.c b/sequencer.c
index a96255426e7..1bef16647b4 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -23,6 +23,8 @@
 #include "hashmap.h"
 #include "unpack-trees.h"
 #include "worktree.h"
+#include "oidmap.h"
+#include "oidset.h"
 
 #define GIT_REFLOG_ACTION "GIT_REFLOG_ACTION"
 
@@ -2785,6 +2787,335 @@ void append_signoff(struct strbuf *msgbuf, int ignore_footer, unsigned flag)
 	strbuf_release(&sob);
 }
 
+struct labels_entry {
+	struct hashmap_entry entry;
+	char label[FLEX_ARRAY];
+};
+
+static int labels_cmp(const void *fndata, const struct labels_entry *a,
+		      const struct labels_entry *b, const void *key)
+{
+	return key ? strcmp(a->label, key) : strcmp(a->label, b->label);
+}
+
+struct string_entry {
+	struct oidmap_entry entry;
+	char string[FLEX_ARRAY];
+};
+
+struct label_state {
+	struct oidmap commit2label;
+	struct hashmap labels;
+	struct strbuf buf;
+};
+
+static const char *label_oid(struct object_id *oid, const char *label,
+			     struct label_state *state)
+{
+	struct labels_entry *labels_entry;
+	struct string_entry *string_entry;
+	struct object_id dummy;
+	size_t len;
+	int i;
+
+	string_entry = oidmap_get(&state->commit2label, oid);
+	if (string_entry)
+		return string_entry->string;
+
+	/*
+	 * For "uninteresting" commits, i.e. commits that are not to be
+	 * rebased, and which can therefore not be labeled, we use a unique
+	 * abbreviation of the commit name. This is slightly more complicated
+	 * than calling find_unique_abbrev() because we also need to make
+	 * sure that the abbreviation does not conflict with any other
+	 * label.
+	 *
+	 * We disallow "interesting" commits to be labeled by a string that
+	 * is a valid full-length hash, to ensure that we always can find an
+	 * abbreviation for any uninteresting commit's names that does not
+	 * clash with any other label.
+	 */
+	if (!label) {
+		char *p;
+
+		strbuf_reset(&state->buf);
+		strbuf_grow(&state->buf, GIT_SHA1_HEXSZ);
+		label = p = state->buf.buf;
+
+		find_unique_abbrev_r(p, oid->hash, default_abbrev);
+
+		/*
+		 * We may need to extend the abbreviated hash so that there is
+		 * no conflicting label.
+		 */
+		if (hashmap_get_from_hash(&state->labels, strihash(p), p)) {
+			size_t i = strlen(p) + 1;
+
+			oid_to_hex_r(p, oid);
+			for (; i < GIT_SHA1_HEXSZ; i++) {
+				char save = p[i];
+				p[i] = '\0';
+				if (!hashmap_get_from_hash(&state->labels,
+							   strihash(p), p))
+					break;
+				p[i] = save;
+			}
+		}
+	} else if (((len = strlen(label)) == GIT_SHA1_RAWSZ &&
+		    !get_oid_hex(label, &dummy)) ||
+		   hashmap_get_from_hash(&state->labels,
+					 strihash(label), label)) {
+		/*
+		 * If the label already exists, or if the label is a valid full
+		 * OID, we append a dash and a number to make it unique.
+		 */
+		struct strbuf *buf = &state->buf;
+
+		strbuf_reset(buf);
+		strbuf_add(buf, label, len);
+
+		for (i = 2; ; i++) {
+			strbuf_setlen(buf, len);
+			strbuf_addf(buf, "-%d", i);
+			if (!hashmap_get_from_hash(&state->labels,
+						   strihash(buf->buf),
+						   buf->buf))
+				break;
+		}
+
+		label = buf->buf;
+	}
+
+	FLEX_ALLOC_STR(labels_entry, label, label);
+	hashmap_entry_init(labels_entry, strihash(label));
+	hashmap_add(&state->labels, labels_entry);
+
+	FLEX_ALLOC_STR(string_entry, string, label);
+	oidcpy(&string_entry->entry.oid, oid);
+	oidmap_put(&state->commit2label, string_entry);
+
+	return string_entry->string;
+}
+
+static int make_script_with_merges(struct pretty_print_context *pp,
+				   struct rev_info *revs, FILE *out,
+				   unsigned flags)
+{
+	int keep_empty = flags & TODO_LIST_KEEP_EMPTY;
+	struct strbuf buf = STRBUF_INIT, oneline = STRBUF_INIT;
+	struct strbuf label = STRBUF_INIT;
+	struct commit_list *commits = NULL, **tail = &commits, *iter;
+	struct commit_list *tips = NULL, **tips_tail = &tips;
+	struct commit *commit;
+	struct oidmap commit2todo = OIDMAP_INIT;
+	struct string_entry *entry;
+	struct oidset interesting = OIDSET_INIT, child_seen = OIDSET_INIT,
+		shown = OIDSET_INIT;
+	struct label_state state = { OIDMAP_INIT, { NULL }, STRBUF_INIT };
+
+	int abbr = flags & TODO_LIST_ABBREVIATE_CMDS;
+	const char *p = abbr ? "p" : "pick", *l = abbr ? "l" : "label",
+		 *t = abbr ? "t" : "reset", *b = abbr ? "b" : "bud",
+		 *m = abbr ? "m" : "merge";
+
+	oidmap_init(&commit2todo, 0);
+	oidmap_init(&state.commit2label, 0);
+	hashmap_init(&state.labels, (hashmap_cmp_fn) labels_cmp, NULL, 0);
+	strbuf_init(&state.buf, 32);
+
+	if (revs->cmdline.nr && (revs->cmdline.rev[0].flags & BOTTOM)) {
+		struct object_id *oid = &revs->cmdline.rev[0].item->oid;
+		FLEX_ALLOC_STR(entry, string, "onto");
+		oidcpy(&entry->entry.oid, oid);
+		oidmap_put(&state.commit2label, entry);
+	}
+
+	/*
+	 * First phase:
+	 * - get onelines for all commits
+	 * - gather all branch tips (i.e. 2nd or later parents of merges)
+	 * - label all branch tips
+	 */
+	while ((commit = get_revision(revs))) {
+		struct commit_list *to_merge;
+		int is_octopus;
+		const char *p1, *p2;
+		struct object_id *oid;
+
+		tail = &commit_list_insert(commit, tail)->next;
+		oidset_insert(&interesting, &commit->object.oid);
+
+		if ((commit->object.flags & PATCHSAME))
+			continue;
+
+		strbuf_reset(&oneline);
+		pretty_print_commit(pp, commit, &oneline);
+
+		to_merge = commit->parents ? commit->parents->next : NULL;
+		if (!to_merge) {
+			/* non-merge commit: easy case */
+			strbuf_reset(&buf);
+			if (!keep_empty && is_original_commit_empty(commit))
+				strbuf_addf(&buf, "%c ", comment_line_char);
+			strbuf_addf(&buf, "%s %s %s", p,
+				    oid_to_hex(&commit->object.oid),
+				    oneline.buf);
+
+			FLEX_ALLOC_STR(entry, string, buf.buf);
+			oidcpy(&entry->entry.oid, &commit->object.oid);
+			oidmap_put(&commit2todo, entry);
+
+			continue;
+		}
+
+		is_octopus = to_merge && to_merge->next;
+
+		if (is_octopus)
+			BUG("Octopus merges not yet supported");
+
+		/* Create a label */
+		strbuf_reset(&label);
+		if (skip_prefix(oneline.buf, "Merge ", &p1) &&
+		    (p1 = strchr(p1, '\'')) &&
+		    (p2 = strchr(++p1, '\'')))
+			strbuf_add(&label, p1, p2 - p1);
+		else if (skip_prefix(oneline.buf, "Merge pull request ",
+				     &p1) &&
+			 (p1 = strstr(p1, " from ")))
+			strbuf_addstr(&label, p1 + strlen(" from "));
+		else
+			strbuf_addbuf(&label, &oneline);
+
+		for (p1 = label.buf; *p1; p1++)
+			if (isspace(*p1))
+				*(char *)p1 = '-';
+
+		strbuf_reset(&buf);
+		strbuf_addf(&buf, "%s %s", m, oid_to_hex(&commit->object.oid));
+
+		/* label the tip of merged branch */
+		oid = &to_merge->item->object.oid;
+		strbuf_addch(&buf, ' ');
+
+		if (!oidset_contains(&interesting, oid))
+			strbuf_addstr(&buf, label_oid(oid, NULL, &state));
+		else {
+			tips_tail = &commit_list_insert(to_merge->item,
+							tips_tail)->next;
+
+			strbuf_addstr(&buf, label_oid(oid, label.buf, &state));
+		}
+		strbuf_addf(&buf, " %s", oneline.buf);
+
+		FLEX_ALLOC_STR(entry, string, buf.buf);
+		oidcpy(&entry->entry.oid, &commit->object.oid);
+		oidmap_put(&commit2todo, entry);
+	}
+
+	/*
+	 * Second phase:
+	 * - label branch points
+	 * - add HEAD to the branch tips
+	 */
+	for (iter = commits; iter; iter = iter->next) {
+		struct commit_list *parent = iter->item->parents;
+		for (; parent; parent = parent->next) {
+			struct object_id *oid = &parent->item->object.oid;
+			if (!oidset_contains(&interesting, oid))
+				continue;
+			if (!oidset_contains(&child_seen, oid))
+				oidset_insert(&child_seen, oid);
+			else
+				label_oid(oid, "branch-point", &state);
+		}
+
+		/* Add HEAD as implict "tip of branch" */
+		if (!iter->next)
+			tips_tail = &commit_list_insert(iter->item,
+							tips_tail)->next;
+	}
+
+	/*
+	 * Third phase: output the todo list. This is a bit tricky, as we
+	 * want to avoid jumping back and forth between revisions. To
+	 * accomplish that goal, we walk backwards from the branch tips,
+	 * gathering commits not yet shown, reversing the list on the fly,
+	 * then outputting that list (labeling revisions as needed).
+	 */
+	fprintf(out, "%s onto\n", l);
+	for (iter = tips; iter; iter = iter->next) {
+		struct commit_list *list = NULL, *iter2;
+
+		commit = iter->item;
+		if (oidset_contains(&shown, &commit->object.oid))
+			continue;
+		entry = oidmap_get(&state.commit2label, &commit->object.oid);
+
+		if (entry)
+			fprintf(out, "\n# Branch %s\n", entry->string);
+		else
+			fprintf(out, "\n");
+
+		while (oidset_contains(&interesting, &commit->object.oid) &&
+		       !oidset_contains(&shown, &commit->object.oid)) {
+			commit_list_insert(commit, &list);
+			if (!commit->parents) {
+				commit = NULL;
+				break;
+			}
+			commit = commit->parents->item;
+		}
+
+		if (!commit)
+			fprintf(out, "%s\n", b);
+		else {
+			const char *to = NULL;
+
+			entry = oidmap_get(&state.commit2label,
+					   &commit->object.oid);
+			if (entry)
+				to = entry->string;
+
+			if (!to || !strcmp("onto", to))
+				fprintf(out, "%s\n", b);
+			else {
+				strbuf_reset(&oneline);
+				pretty_print_commit(pp, commit, &oneline);
+				fprintf(out, "%s %s %s\n",
+					t, to, oneline.buf);
+			}
+		}
+
+		for (iter2 = list; iter2; iter2 = iter2->next) {
+			struct object_id *oid = &iter2->item->object.oid;
+			entry = oidmap_get(&commit2todo, oid);
+			/* only show if not already upstream */
+			if (entry)
+				fprintf(out, "%s\n", entry->string);
+			entry = oidmap_get(&state.commit2label, oid);
+			if (entry)
+				fprintf(out, "%s %s\n", l, entry->string);
+			oidset_insert(&shown, oid);
+		}
+
+		free_commit_list(list);
+	}
+
+	free_commit_list(commits);
+	free_commit_list(tips);
+
+	strbuf_release(&label);
+	strbuf_release(&oneline);
+	strbuf_release(&buf);
+
+	oidmap_free(&commit2todo, 1);
+	oidmap_free(&state.commit2label, 1);
+	hashmap_free(&state.labels, 1);
+	strbuf_release(&state.buf);
+
+	return 0;
+}
+
 int sequencer_make_script(FILE *out, int argc, const char **argv,
 			  unsigned flags)
 {
@@ -2795,11 +3126,16 @@ int sequencer_make_script(FILE *out, int argc, const char **argv,
 	struct commit *commit;
 	int keep_empty = flags & TODO_LIST_KEEP_EMPTY;
 	const char *insn = flags & TODO_LIST_ABBREVIATE_CMDS ? "p" : "pick";
+	int recreate_merges = flags & TODO_LIST_RECREATE_MERGES;
 
 	init_revisions(&revs, NULL);
 	revs.verbose_header = 1;
-	revs.max_parents = 1;
-	revs.cherry_pick = 1;
+	if (recreate_merges)
+		revs.cherry_mark = 1;
+	else {
+		revs.max_parents = 1;
+		revs.cherry_pick = 1;
+	}
 	revs.limited = 1;
 	revs.reverse = 1;
 	revs.right_only = 1;
@@ -2823,6 +3159,9 @@ int sequencer_make_script(FILE *out, int argc, const char **argv,
 	if (prepare_revision_walk(&revs) < 0)
 		return error(_("make_script: error preparing revisions"));
 
+	if (recreate_merges)
+		return make_script_with_merges(&pp, &revs, out, flags);
+
 	while ((commit = get_revision(&revs))) {
 		strbuf_reset(&buf);
 		if (!keep_empty && is_original_commit_empty(commit))
diff --git a/sequencer.h b/sequencer.h
index 81f6d7d393f..11d1ac925ef 100644
--- a/sequencer.h
+++ b/sequencer.h
@@ -48,6 +48,7 @@ int sequencer_remove_state(struct replay_opts *opts);
 #define TODO_LIST_KEEP_EMPTY (1U << 0)
 #define TODO_LIST_SHORTEN_IDS (1U << 1)
 #define TODO_LIST_ABBREVIATE_CMDS (1U << 2)
+#define TODO_LIST_RECREATE_MERGES (1U << 3)
 int sequencer_make_script(FILE *out, int argc, const char **argv,
 			  unsigned flags);
 
-- 
2.15.1.windows.2.1430.ga56c4f9e2a9



^ permalink raw reply	[flat|nested] 276+ messages in thread

* [PATCH 5/8] rebase: introduce the --recreate-merges option
  2018-01-18 15:35 [PATCH 0/8] rebase -i: offer to recreate merge commits Johannes Schindelin
                   ` (3 preceding siblings ...)
  2018-01-18 15:35 ` [PATCH 4/8] rebase-helper --make-script: introduce a flag to recreate merges Johannes Schindelin
@ 2018-01-18 15:35 ` Johannes Schindelin
  2018-01-19 10:55   ` Eric Sunshine
                     ` (3 more replies)
  2018-01-18 15:35 ` [PATCH 6/8] sequencer: handle autosquash and post-rewrite for merge commands Johannes Schindelin
                   ` (7 subsequent siblings)
  12 siblings, 4 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-01-18 15:35 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Jacob Keller

Once upon a time, this here developer thought: wouldn't it be nice if,
say, Git for Windows' patches on top of core Git could be represented as
a thicket of branches, and be rebased on top of core Git in order to
maintain a cherry-pick'able set of patch series?

The original attempt at an answer was: git rebase --preserve-merges.

However, that experiment was never intended as an interactive option,
and it only piggy-backed on git rebase --interactive because that
command's implementation looked already very, very familiar: it was
designed by the same person who designed --preserve-merges: yours truly.

Some time later, some other developer (I am looking at you, Andreas!
;-)) decided that it would be a good idea to allow --preserve-merges to
be combined with --interactive (with caveats!) and the Git maintainer
(well, the interim Git maintainer during Junio's absence, that is)
agreed, and that is when the glamor of the --preserve-merges design
started to fall apart rather quickly and unglamorously.

The reason? In --preserve-merges mode, the parents of a merge commit (or
for that matter, of *any* commit) were not stated explicitly, but were
*implied* by the commit name passed to the `pick` command.

This made it impossible, for example, to reorder commits. Not to mention
to flatten the branch topology or, deity forbid, to split topic branches
into two.

Alas, these shortcomings also prevented that mode (whose original
purpose was to serve Git for Windows' needs, with the additional hope
that it may be useful to others, too) from serving Git for Windows'
needs.

Five years later, when it became really untenable to have one unwieldy,
big hodge-podge patch series of partly related, partly unrelated patches
in Git for Windows that was rebased onto core Git's tags from time to
time (earning the undeserved wrath of the developer of the ill-fated
git-remote-hg series that first obsoleted Git for Windows' competing
approach, only to be abandoned without maintainer later) was really
untenable, the "Git garden shears" were born [*1*/*2*]: a script,
piggy-backing on top of the interactive rebase, that would first
determine the branch topology of the patches to be rebased, create a
pseudo todo list for further editing, transform the result into a real
todo list (making heavy use of the `exec` command to "implement" the
missing todo list commands) and finally recreate the patch series on
top of the new base commit.

That was in 2013. And it took about three weeks to come up with the
design and implement it as an out-of-tree script. Needless to say, the
implementation needed quite a few years to stabilize, all the while the
design itself proved itself sound.

With this patch, the goodness of the Git garden shears comes to `git
rebase -i` itself. Passing the `--recreate-merges` option will generate
a todo list that can be understood readily, and where it is obvious
how to reorder commits. New branches can be introduced by inserting
`label` commands and calling `merge - <label> <oneline>`. And once this
mode has become stable and universally accepted, we can deprecate the
design mistake that was `--preserve-merges`.

Link *1*:
https://github.com/msysgit/msysgit/blob/master/share/msysGit/shears.sh
Link *2*:
https://github.com/git-for-windows/build-extra/blob/master/shears.sh

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 Documentation/git-rebase.txt           |   8 +-
 contrib/completion/git-completion.bash |   2 +-
 git-rebase--interactive.sh             |   1 +
 git-rebase.sh                          |   6 ++
 t/t3430-rebase-recreate-merges.sh      | 146 +++++++++++++++++++++++++++++++++
 5 files changed, 161 insertions(+), 2 deletions(-)
 create mode 100755 t/t3430-rebase-recreate-merges.sh

diff --git a/Documentation/git-rebase.txt b/Documentation/git-rebase.txt
index 8a861c1e0d6..1d061373288 100644
--- a/Documentation/git-rebase.txt
+++ b/Documentation/git-rebase.txt
@@ -368,6 +368,11 @@ The commit list format can be changed by setting the configuration option
 rebase.instructionFormat.  A customized instruction format will automatically
 have the long commit hash prepended to the format.
 
+--recreate-merges::
+	Recreate merge commits instead of flattening the history by replaying
+	merges. Merge conflict resolutions or manual amendments to merge
+	commits are not preserved.
+
 -p::
 --preserve-merges::
 	Recreate merge commits instead of flattening the history by replaying
@@ -770,7 +775,8 @@ BUGS
 The todo list presented by `--preserve-merges --interactive` does not
 represent the topology of the revision graph.  Editing commits and
 rewording their commit messages should work fine, but attempts to
-reorder commits tend to produce counterintuitive results.
+reorder commits tend to produce counterintuitive results. Use
+--recreate-merges for a more faithful representation.
 
 For example, an attempt to rearrange
 ------------
diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash
index 3683c772c55..6893c3adabc 100644
--- a/contrib/completion/git-completion.bash
+++ b/contrib/completion/git-completion.bash
@@ -2008,7 +2008,7 @@ _git_rebase ()
 	--*)
 		__gitcomp "
 			--onto --merge --strategy --interactive
-			--preserve-merges --stat --no-stat
+			--recreate-merges --preserve-merges --stat --no-stat
 			--committer-date-is-author-date --ignore-date
 			--ignore-whitespace --whitespace=
 			--autosquash --no-autosquash
diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh
index 5bf1ea3781f..3459ec5a018 100644
--- a/git-rebase--interactive.sh
+++ b/git-rebase--interactive.sh
@@ -900,6 +900,7 @@ fi
 if test t != "$preserve_merges"
 then
 	git rebase--helper --make-script ${keep_empty:+--keep-empty} \
+		${recreate_merges:+--recreate-merges} \
 		$revisions ${restrict_revision+^$restrict_revision} >"$todo" ||
 	die "$(gettext "Could not generate todo list")"
 else
diff --git a/git-rebase.sh b/git-rebase.sh
index fd72a35c65b..d69bc7d0e0d 100755
--- a/git-rebase.sh
+++ b/git-rebase.sh
@@ -17,6 +17,7 @@ q,quiet!           be quiet. implies --no-stat
 autostash          automatically stash/stash pop before and after
 fork-point         use 'merge-base --fork-point' to refine upstream
 onto=!             rebase onto given branch instead of upstream
+recreate-merges!   try to recreate merges instead of skipping them
 p,preserve-merges! try to recreate merges instead of ignoring them
 s,strategy=!       use the given merge strategy
 no-ff!             cherry-pick all commits, even if unchanged
@@ -86,6 +87,7 @@ type=
 state_dir=
 # One of {'', continue, skip, abort}, as parsed from command line
 action=
+recreate_merges=
 preserve_merges=
 autosquash=
 keep_empty=
@@ -262,6 +264,10 @@ do
 	--keep-empty)
 		keep_empty=yes
 		;;
+	--recreate-merges)
+		recreate_merges=t
+		test -z "$interactive_rebase" && interactive_rebase=implied
+		;;
 	--preserve-merges)
 		preserve_merges=t
 		test -z "$interactive_rebase" && interactive_rebase=implied
diff --git a/t/t3430-rebase-recreate-merges.sh b/t/t3430-rebase-recreate-merges.sh
new file mode 100755
index 00000000000..46ae52f88b3
--- /dev/null
+++ b/t/t3430-rebase-recreate-merges.sh
@@ -0,0 +1,146 @@
+#!/bin/sh
+#
+# Copyright (c) 2017 Johannes E. Schindelin
+#
+
+test_description='git rebase -i --recreate-merges
+
+This test runs git rebase "interactively", retaining the branch structure by
+recreating merge commits.
+
+Initial setup:
+
+    -- B --                   (first)
+   /       \
+ A - C - D - E - H            (master)
+       \       /
+         F - G                (second)
+'
+. ./test-lib.sh
+. "$TEST_DIRECTORY"/lib-rebase.sh
+
+test_expect_success 'setup' '
+	write_script replace-editor.sh <<-\EOF &&
+	mv "$1" "$(git rev-parse --git-path ORIGINAL-TODO)"
+	cp script-from-scratch "$1"
+	EOF
+
+	test_commit A &&
+	git checkout -b first &&
+	test_commit B &&
+	git checkout master &&
+	test_commit C &&
+	test_commit D &&
+	git merge --no-commit B &&
+	test_tick &&
+	git commit -m E &&
+	git tag -m E E &&
+	git checkout -b second C &&
+	test_commit F &&
+	test_commit G &&
+	git checkout master &&
+	git merge --no-commit G &&
+	test_tick &&
+	git commit -m H &&
+	git tag -m H H
+'
+
+cat >script-from-scratch <<\EOF
+label onto
+
+# onebranch
+pick G
+pick D
+label onebranch
+
+# second
+bud
+pick B
+label second
+
+bud
+merge H second
+merge - onebranch Merge the topic branch 'onebranch'
+EOF
+
+test_cmp_graph () {
+	cat >expect &&
+	git log --graph --boundary --format=%s "$@" >output &&
+	sed "s/ *$//" <output >output.trimmed &&
+	test_cmp expect output.trimmed
+}
+
+test_expect_success 'create completely different structure' '
+	test_config sequence.editor \""$PWD"/replace-editor.sh\" &&
+	test_tick &&
+	git rebase -i --recreate-merges A &&
+	test_cmp_graph <<-\EOF
+	*   Merge the topic branch '\''onebranch'\''
+	|\
+	| * D
+	| * G
+	* |   H
+	|\ \
+	| |/
+	|/|
+	| * B
+	|/
+	* A
+	EOF
+'
+
+test_expect_success 'generate correct todo list' '
+	cat >expect <<-\EOF &&
+	label onto
+
+	bud
+	pick d9df450 B
+	label E
+
+	bud
+	pick 5dee784 C
+	label branch-point
+	pick ca2c861 F
+	pick 088b00a G
+	label H
+
+	reset branch-point C
+	pick 12bd07b D
+	merge 2051b56 E E
+	merge 233d48a H H
+
+	EOF
+
+	grep -v "^#" <.git/ORIGINAL-TODO >output &&
+	test_cmp expect output
+'
+
+test_expect_success 'with a branch tip that was cherry-picked already' '
+	git checkout -b already-upstream master &&
+	base="$(git rev-parse --verify HEAD)" &&
+
+	test_commit A1 &&
+	test_commit A2 &&
+	git reset --hard $base &&
+	test_commit B1 &&
+	test_tick &&
+	git merge -m "Merge branch A" A2 &&
+
+	git checkout -b upstream-with-a2 $base &&
+	test_tick &&
+	git cherry-pick A2 &&
+
+	git checkout already-upstream &&
+	test_tick &&
+	git rebase -i --recreate-merges upstream-with-a2 &&
+	test_cmp_graph upstream-with-a2.. <<-\EOF
+	*   Merge branch A
+	|\
+	| * A1
+	* | B1
+	|/
+	o A2
+	EOF
+'
+
+test_done
-- 
2.15.1.windows.2.1430.ga56c4f9e2a9



^ permalink raw reply	[flat|nested] 276+ messages in thread

* [PATCH 6/8] sequencer: handle autosquash and post-rewrite for merge commands
  2018-01-18 15:35 [PATCH 0/8] rebase -i: offer to recreate merge commits Johannes Schindelin
                   ` (4 preceding siblings ...)
  2018-01-18 15:35 ` [PATCH 5/8] rebase: introduce the --recreate-merges option Johannes Schindelin
@ 2018-01-18 15:35 ` Johannes Schindelin
  2018-01-18 16:43   ` Jacob Keller
  2018-01-18 15:36 ` [PATCH 7/8] pull: accept --rebase=recreate to recreate the branch topology Johannes Schindelin
                   ` (6 subsequent siblings)
  12 siblings, 1 reply; 276+ messages in thread
From: Johannes Schindelin @ 2018-01-18 15:35 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Jacob Keller

In the previous patches, we implemented the basic functionality of the
`git rebase -i --recreate-merges` command, in particular the `merge`
command to create merge commits in the sequencer.

The interactive rebase is a lot more these days, though, than a simple
cherry-pick in a loop. For example, it calls the post-rewrite hook (if
any) after rebasing with a mapping of the old->new commits. And the
interactive rebase also supports the autosquash mode, where commits
whose oneline is of the form `fixup! <oneline>` or `squash! <oneline>`
are rearranged to amend commits whose oneline they match.

This patch implements the post-rewrite and autosquash handling for the
`merge` command we just introduced. The other commands that were added
recently (`label`, `reset` and `bud`) do not create new commits,
therefore post-rewrite & autosquash do not need to handle them.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 refs.c                            |  3 ++-
 sequencer.c                       | 10 +++++++---
 t/t3430-rebase-recreate-merges.sh | 39 +++++++++++++++++++++++++++++++++++++++
 3 files changed, 48 insertions(+), 4 deletions(-)

diff --git a/refs.c b/refs.c
index 20ba82b4343..e8b84c189ff 100644
--- a/refs.c
+++ b/refs.c
@@ -600,7 +600,8 @@ int dwim_log(const char *str, int len, struct object_id *oid, char **log)
 static int is_per_worktree_ref(const char *refname)
 {
 	return !strcmp(refname, "HEAD") ||
-		starts_with(refname, "refs/bisect/");
+		starts_with(refname, "refs/bisect/") ||
+		starts_with(refname, "refs/rewritten/");
 }
 
 static int is_pseudoref_syntax(const char *refname)
diff --git a/sequencer.c b/sequencer.c
index 1bef16647b4..b63bfb9a141 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -2413,10 +2413,13 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts)
 			res = do_reset(item->arg, item->arg_len);
 		else if (item->command == TODO_BUD)
 			res = do_reset("onto", 4);
-		else if (item->command == TODO_MERGE)
+		else if (item->command == TODO_MERGE) {
 			res = do_merge(item->commit,
 				       item->arg, item->arg_len, opts);
-		else if (!is_noop(item->command))
+			if (item->commit)
+				record_in_rewritten(&item->commit->object.oid,
+						    peek_command(todo_list, 1));
+		} else if (!is_noop(item->command))
 			return error(_("unknown command %d"), item->command);
 
 		todo_list->current++;
@@ -3556,7 +3559,8 @@ int rearrange_squash(void)
 		struct subject2item_entry *entry;
 
 		next[i] = tail[i] = -1;
-		if (item->command >= TODO_EXEC) {
+		if (item->command >= TODO_EXEC &&
+		    (item->command != TODO_MERGE || !item->commit)) {
 			subjects[i] = NULL;
 			continue;
 		}
diff --git a/t/t3430-rebase-recreate-merges.sh b/t/t3430-rebase-recreate-merges.sh
index 46ae52f88b3..76e615bd7c1 100755
--- a/t/t3430-rebase-recreate-merges.sh
+++ b/t/t3430-rebase-recreate-merges.sh
@@ -143,4 +143,43 @@ test_expect_success 'with a branch tip that was cherry-picked already' '
 	EOF
 '
 
+test_expect_success 'refs/rewritten/* is worktree-local' '
+	git worktree add wt &&
+	cat >wt/script-from-scratch <<-\EOF &&
+	label xyz
+	exec GIT_DIR=../.git git rev-parse --verify refs/rewritten/xyz >a || :
+	exec git rev-parse --verify refs/rewritten/xyz >b
+	EOF
+
+	test_config -C wt sequence.editor \""$PWD"/replace-editor.sh\" &&
+	git -C wt rebase -i HEAD &&
+	test_must_be_empty wt/a &&
+	test_cmp_rev HEAD "$(cat wt/b)"
+'
+
+test_expect_success 'post-rewrite hook and fixups work for merges' '
+	git checkout -b post-rewrite &&
+	test_commit same1 &&
+	git reset --hard HEAD^ &&
+	test_commit same2 &&
+	git merge -m "to fix up" same1 &&
+	echo same old same old >same2.t &&
+	test_tick &&
+	git commit --fixup HEAD same2.t &&
+	fixup="$(git rev-parse HEAD)" &&
+
+	mkdir -p .git/hooks &&
+	test_when_finished "rm .git/hooks/post-rewrite" &&
+	echo "cat >actual" | write_script .git/hooks/post-rewrite &&
+
+	test_tick &&
+	git rebase -i --autosquash --recreate-merges HEAD^^^ &&
+	printf "%s %s\n%s %s\n%s %s\n%s %s\n" >expect $(git rev-parse \
+		$fixup^^2 HEAD^2 \
+		$fixup^^ HEAD^ \
+		$fixup^ HEAD \
+		$fixup HEAD) &&
+	test_cmp expect actual
+'
+
 test_done
-- 
2.15.1.windows.2.1430.ga56c4f9e2a9



^ permalink raw reply	[flat|nested] 276+ messages in thread

* [PATCH 7/8] pull: accept --rebase=recreate to recreate the branch topology
  2018-01-18 15:35 [PATCH 0/8] rebase -i: offer to recreate merge commits Johannes Schindelin
                   ` (5 preceding siblings ...)
  2018-01-18 15:35 ` [PATCH 6/8] sequencer: handle autosquash and post-rewrite for merge commands Johannes Schindelin
@ 2018-01-18 15:36 ` Johannes Schindelin
  2018-01-18 15:36 ` [PATCH 8/8] rebase -i: introduce --recreate-merges=no-rebase-cousins Johannes Schindelin
                   ` (5 subsequent siblings)
  12 siblings, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-01-18 15:36 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Jacob Keller

Similar to the `preserve` mode simply passing the `--preserve-merges`
option to the `rebase` command, the `recreate` mode simply passes the
`--recreate-merges` option.

This will allow users to conveniently rebase non-trivial commit
topologies when pulling new commits, without flattening them.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 Documentation/config.txt               |  8 ++++++++
 Documentation/git-pull.txt             |  5 ++++-
 builtin/pull.c                         | 14 ++++++++++----
 builtin/remote.c                       |  2 ++
 contrib/completion/git-completion.bash |  2 +-
 5 files changed, 25 insertions(+), 6 deletions(-)

diff --git a/Documentation/config.txt b/Documentation/config.txt
index 0e25b2c92b3..da41ab246dc 100644
--- a/Documentation/config.txt
+++ b/Documentation/config.txt
@@ -1058,6 +1058,10 @@ branch.<name>.rebase::
 	"git pull" is run. See "pull.rebase" for doing this in a non
 	branch-specific manner.
 +
+When recreate, also pass `--recreate-merges` along to 'git rebase'
+so that locally committed merge commits will not be flattened
+by running 'git pull'.
++
 When preserve, also pass `--preserve-merges` along to 'git rebase'
 so that locally committed merge commits will not be flattened
 by running 'git pull'.
@@ -2607,6 +2611,10 @@ pull.rebase::
 	pull" is run. See "branch.<name>.rebase" for setting this on a
 	per-branch basis.
 +
+When recreate, also pass `--recreate-merges` along to 'git rebase'
+so that locally committed merge commits will not be flattened
+by running 'git pull'.
++
 When preserve, also pass `--preserve-merges` along to 'git rebase'
 so that locally committed merge commits will not be flattened
 by running 'git pull'.
diff --git a/Documentation/git-pull.txt b/Documentation/git-pull.txt
index ce05b7a5b13..b4f9f057ea9 100644
--- a/Documentation/git-pull.txt
+++ b/Documentation/git-pull.txt
@@ -101,13 +101,16 @@ Options related to merging
 include::merge-options.txt[]
 
 -r::
---rebase[=false|true|preserve|interactive]::
+--rebase[=false|true|recreate|preserve|interactive]::
 	When true, rebase the current branch on top of the upstream
 	branch after fetching. If there is a remote-tracking branch
 	corresponding to the upstream branch and the upstream branch
 	was rebased since last fetched, the rebase uses that information
 	to avoid rebasing non-local changes.
 +
+When set to recreate, rebase with the `--recreate-merges` option passed
+to `git rebase` so that locally created merge commits will not be flattened.
++
 When set to preserve, rebase with the `--preserve-merges` option passed
 to `git rebase` so that locally created merge commits will not be flattened.
 +
diff --git a/builtin/pull.c b/builtin/pull.c
index 511dbbe0f6e..e33c84e0345 100644
--- a/builtin/pull.c
+++ b/builtin/pull.c
@@ -27,14 +27,16 @@ enum rebase_type {
 	REBASE_FALSE = 0,
 	REBASE_TRUE,
 	REBASE_PRESERVE,
+	REBASE_RECREATE,
 	REBASE_INTERACTIVE
 };
 
 /**
  * Parses the value of --rebase. If value is a false value, returns
  * REBASE_FALSE. If value is a true value, returns REBASE_TRUE. If value is
- * "preserve", returns REBASE_PRESERVE. If value is a invalid value, dies with
- * a fatal error if fatal is true, otherwise returns REBASE_INVALID.
+ * "recreate", returns REBASE_RECREATE. If value is "preserve", returns
+ * REBASE_PRESERVE. If value is a invalid value, dies with a fatal error if
+ * fatal is true, otherwise returns REBASE_INVALID.
  */
 static enum rebase_type parse_config_rebase(const char *key, const char *value,
 		int fatal)
@@ -47,6 +49,8 @@ static enum rebase_type parse_config_rebase(const char *key, const char *value,
 		return REBASE_TRUE;
 	else if (!strcmp(value, "preserve"))
 		return REBASE_PRESERVE;
+	else if (!strcmp(value, "recreate"))
+		return REBASE_RECREATE;
 	else if (!strcmp(value, "interactive"))
 		return REBASE_INTERACTIVE;
 
@@ -130,7 +134,7 @@ static struct option pull_options[] = {
 	/* Options passed to git-merge or git-rebase */
 	OPT_GROUP(N_("Options related to merging")),
 	{ OPTION_CALLBACK, 'r', "rebase", &opt_rebase,
-	  "false|true|preserve|interactive",
+	  "false|true|recreate|preserve|interactive",
 	  N_("incorporate changes by rebasing rather than merging"),
 	  PARSE_OPT_OPTARG, parse_opt_rebase },
 	OPT_PASSTHRU('n', NULL, &opt_diffstat, NULL,
@@ -798,7 +802,9 @@ static int run_rebase(const struct object_id *curr_head,
 	argv_push_verbosity(&args);
 
 	/* Options passed to git-rebase */
-	if (opt_rebase == REBASE_PRESERVE)
+	if (opt_rebase == REBASE_RECREATE)
+		argv_array_push(&args, "--recreate-merges");
+	else if (opt_rebase == REBASE_PRESERVE)
 		argv_array_push(&args, "--preserve-merges");
 	else if (opt_rebase == REBASE_INTERACTIVE)
 		argv_array_push(&args, "--interactive");
diff --git a/builtin/remote.c b/builtin/remote.c
index d95bf904c3b..b7d0f7ce596 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -306,6 +306,8 @@ static int config_read_branches(const char *key, const char *value, void *cb)
 				info->rebase = v;
 			else if (!strcmp(value, "preserve"))
 				info->rebase = NORMAL_REBASE;
+			else if (!strcmp(value, "recreate"))
+				info->rebase = NORMAL_REBASE;
 			else if (!strcmp(value, "interactive"))
 				info->rebase = INTERACTIVE_REBASE;
 		}
diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash
index 6893c3adabc..6f98c96fee9 100644
--- a/contrib/completion/git-completion.bash
+++ b/contrib/completion/git-completion.bash
@@ -2182,7 +2182,7 @@ _git_config ()
 		return
 		;;
 	branch.*.rebase)
-		__gitcomp "false true preserve interactive"
+		__gitcomp "false true recreate preserve interactive"
 		return
 		;;
 	remote.pushdefault)
-- 
2.15.1.windows.2.1430.ga56c4f9e2a9



^ permalink raw reply	[flat|nested] 276+ messages in thread

* [PATCH 8/8] rebase -i: introduce --recreate-merges=no-rebase-cousins
  2018-01-18 15:35 [PATCH 0/8] rebase -i: offer to recreate merge commits Johannes Schindelin
                   ` (6 preceding siblings ...)
  2018-01-18 15:36 ` [PATCH 7/8] pull: accept --rebase=recreate to recreate the branch topology Johannes Schindelin
@ 2018-01-18 15:36 ` Johannes Schindelin
  2018-01-18 22:00   ` Philip Oakley
  2018-01-20  1:09   ` Eric Sunshine
  2018-01-18 16:49 ` [PATCH 0/8] rebase -i: offer to recreate merge commits Jacob Keller
                   ` (4 subsequent siblings)
  12 siblings, 2 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-01-18 15:36 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Jacob Keller

This one is a bit tricky to explain, so let's try with a diagram:

        C
      /   \
A - B - E - F
  \   /
    D

To illustrate what this new mode is all about, let's consider what
happens upon `git rebase -i --recreate-merges B`, in particular to
the commit `D`. In the default mode, the new branch structure is:

       --- C' --
      /         \
A - B ------ E' - F'
      \    /
        D'

This is not really preserving the branch topology from before! The
reason is that the commit `D` does not have `B` as ancestor, and
therefore it gets rebased onto `B`.

However, when recreating branch structure, there are legitimate use
cases where one might want to preserve the branch points of commits that
do not descend from the <upstream> commit that was passed to the rebase
command, e.g. when a branch from core Git's `next` was merged into Git
for Windows' master we will not want to rebase those commits on top of a
Windows-specific commit. In the example above, the desired outcome would
look like this:

       --- C' --
      /         \
A - B ------ E' - F'
  \        /
   -- D' --

Let's introduce the term "cousins" for such commits ("D" in the
example), and the "no-rebase-cousins" mode of the merge-recreating
rebase, to help those use cases.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 Documentation/git-rebase.txt      |  7 ++++++-
 builtin/rebase--helper.c          |  9 ++++++++-
 git-rebase--interactive.sh        |  1 +
 git-rebase.sh                     | 12 +++++++++++-
 sequencer.c                       |  4 ++++
 sequencer.h                       |  8 ++++++++
 t/t3430-rebase-recreate-merges.sh | 23 +++++++++++++++++++++++
 7 files changed, 61 insertions(+), 3 deletions(-)

diff --git a/Documentation/git-rebase.txt b/Documentation/git-rebase.txt
index 1d061373288..ac07a5c3fc9 100644
--- a/Documentation/git-rebase.txt
+++ b/Documentation/git-rebase.txt
@@ -368,10 +368,15 @@ The commit list format can be changed by setting the configuration option
 rebase.instructionFormat.  A customized instruction format will automatically
 have the long commit hash prepended to the format.
 
---recreate-merges::
+--recreate-merges[=(rebase-cousins|no-rebase-cousins)]::
 	Recreate merge commits instead of flattening the history by replaying
 	merges. Merge conflict resolutions or manual amendments to merge
 	commits are not preserved.
++
+By default, or when `rebase-cousins` was specified, commits which do not have
+`<upstream>` as direct ancestor are rebased onto `<upstream>` (or `<onto>`,
+if specified). If the `rebase-cousins` mode is turned off, such commits will
+retain their original branch point.
 
 -p::
 --preserve-merges::
diff --git a/builtin/rebase--helper.c b/builtin/rebase--helper.c
index a34ab5c0655..ef08fef4d14 100644
--- a/builtin/rebase--helper.c
+++ b/builtin/rebase--helper.c
@@ -13,7 +13,7 @@ int cmd_rebase__helper(int argc, const char **argv, const char *prefix)
 {
 	struct replay_opts opts = REPLAY_OPTS_INIT;
 	unsigned flags = 0, keep_empty = 0, recreate_merges = 0;
-	int abbreviate_commands = 0;
+	int abbreviate_commands = 0, no_rebase_cousins = -1;
 	enum {
 		CONTINUE = 1, ABORT, MAKE_SCRIPT, SHORTEN_OIDS, EXPAND_OIDS,
 		CHECK_TODO_LIST, SKIP_UNNECESSARY_PICKS, REARRANGE_SQUASH,
@@ -23,6 +23,8 @@ int cmd_rebase__helper(int argc, const char **argv, const char *prefix)
 		OPT_BOOL(0, "ff", &opts.allow_ff, N_("allow fast-forward")),
 		OPT_BOOL(0, "keep-empty", &keep_empty, N_("keep empty commits")),
 		OPT_BOOL(0, "recreate-merges", &recreate_merges, N_("recreate merge commits")),
+		OPT_BOOL(0, "no-rebase-cousins", &no_rebase_cousins,
+			 N_("keep original branch points of cousins")),
 		OPT_CMDMODE(0, "continue", &command, N_("continue rebase"),
 				CONTINUE),
 		OPT_CMDMODE(0, "abort", &command, N_("abort rebase"),
@@ -57,8 +59,13 @@ int cmd_rebase__helper(int argc, const char **argv, const char *prefix)
 	flags |= keep_empty ? TODO_LIST_KEEP_EMPTY : 0;
 	flags |= abbreviate_commands ? TODO_LIST_ABBREVIATE_CMDS : 0;
 	flags |= recreate_merges ? TODO_LIST_RECREATE_MERGES : 0;
+	flags |= no_rebase_cousins > 0 ? TODO_LIST_NO_REBASE_COUSINS : 0;
 	flags |= command == SHORTEN_OIDS ? TODO_LIST_SHORTEN_IDS : 0;
 
+	if (no_rebase_cousins >= 0&& !recreate_merges)
+		warning(_("--[no-]rebase-cousins has no effect without "
+			  "--recreate-merges"));
+
 	if (command == CONTINUE && argc == 1)
 		return !!sequencer_continue(&opts);
 	if (command == ABORT && argc == 1)
diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh
index 3459ec5a018..23184c77e88 100644
--- a/git-rebase--interactive.sh
+++ b/git-rebase--interactive.sh
@@ -901,6 +901,7 @@ if test t != "$preserve_merges"
 then
 	git rebase--helper --make-script ${keep_empty:+--keep-empty} \
 		${recreate_merges:+--recreate-merges} \
+		${no_rebase_cousins:+--no-rebase-cousins} \
 		$revisions ${restrict_revision+^$restrict_revision} >"$todo" ||
 	die "$(gettext "Could not generate todo list")"
 else
diff --git a/git-rebase.sh b/git-rebase.sh
index d69bc7d0e0d..3403b1416a8 100755
--- a/git-rebase.sh
+++ b/git-rebase.sh
@@ -17,7 +17,7 @@ q,quiet!           be quiet. implies --no-stat
 autostash          automatically stash/stash pop before and after
 fork-point         use 'merge-base --fork-point' to refine upstream
 onto=!             rebase onto given branch instead of upstream
-recreate-merges!   try to recreate merges instead of skipping them
+recreate-merges?   try to recreate merges instead of skipping them
 p,preserve-merges! try to recreate merges instead of ignoring them
 s,strategy=!       use the given merge strategy
 no-ff!             cherry-pick all commits, even if unchanged
@@ -88,6 +88,7 @@ state_dir=
 # One of {'', continue, skip, abort}, as parsed from command line
 action=
 recreate_merges=
+no_rebase_cousins=
 preserve_merges=
 autosquash=
 keep_empty=
@@ -268,6 +269,15 @@ do
 		recreate_merges=t
 		test -z "$interactive_rebase" && interactive_rebase=implied
 		;;
+	--recreate-merges=*)
+		recreate_merges=t
+		case "${1#*=}" in
+		rebase-cousins) no_rebase_cousins=;;
+		no-rebase-cousins) no_rebase_cousins=t;;
+		*) die "Unknown mode: $1";;
+		esac
+		test -z "$interactive_rebase" && interactive_rebase=implied
+		;;
 	--preserve-merges)
 		preserve_merges=t
 		test -z "$interactive_rebase" && interactive_rebase=implied
diff --git a/sequencer.c b/sequencer.c
index b63bfb9a141..2b4e6b12321 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -2905,6 +2905,7 @@ static int make_script_with_merges(struct pretty_print_context *pp,
 				   unsigned flags)
 {
 	int keep_empty = flags & TODO_LIST_KEEP_EMPTY;
+	int no_rebase_cousins = flags & TODO_LIST_NO_REBASE_COUSINS;
 	struct strbuf buf = STRBUF_INIT, oneline = STRBUF_INIT;
 	struct strbuf label = STRBUF_INIT;
 	struct commit_list *commits = NULL, **tail = &commits, *iter;
@@ -3078,6 +3079,9 @@ static int make_script_with_merges(struct pretty_print_context *pp,
 					   &commit->object.oid);
 			if (entry)
 				to = entry->string;
+			else if (no_rebase_cousins)
+				to = label_oid(&commit->object.oid, NULL,
+					       &state);
 
 			if (!to || !strcmp("onto", to))
 				fprintf(out, "%s\n", b);
diff --git a/sequencer.h b/sequencer.h
index 11d1ac925ef..9530dba3cba 100644
--- a/sequencer.h
+++ b/sequencer.h
@@ -49,6 +49,14 @@ int sequencer_remove_state(struct replay_opts *opts);
 #define TODO_LIST_SHORTEN_IDS (1U << 1)
 #define TODO_LIST_ABBREVIATE_CMDS (1U << 2)
 #define TODO_LIST_RECREATE_MERGES (1U << 3)
+/*
+ * When recreating merges, commits that do have the base commit as ancestor
+ * ("cousins") are rebased onto the new base by default. If those commits
+ * should keep their original branch point, this flag needs to be passed.
+ *
+ * This flag only makes sense when <base> and <onto> are different.
+ */
+#define TODO_LIST_NO_REBASE_COUSINS (1U << 4)
 int sequencer_make_script(FILE *out, int argc, const char **argv,
 			  unsigned flags);
 
diff --git a/t/t3430-rebase-recreate-merges.sh b/t/t3430-rebase-recreate-merges.sh
index 76e615bd7c1..22930e470a4 100755
--- a/t/t3430-rebase-recreate-merges.sh
+++ b/t/t3430-rebase-recreate-merges.sh
@@ -143,6 +143,29 @@ test_expect_success 'with a branch tip that was cherry-picked already' '
 	EOF
 '
 
+test_expect_success 'rebase cousins unless told not to' '
+	write_script copy-editor.sh <<-\EOF &&
+	cp "$1" "$(git rev-parse --git-path ORIGINAL-TODO)"
+	EOF
+
+	test_config sequence.editor \""$PWD"/copy-editor.sh\" &&
+	git checkout -b cousins master &&
+	before="$(git rev-parse --verify HEAD)" &&
+	test_tick &&
+	git rebase -i --recreate-merges=no-rebase-cousins HEAD^ &&
+	test_cmp_rev HEAD $before &&
+	test_tick &&
+	git rebase -i --recreate-merges HEAD^ &&
+	test_cmp_graph HEAD^.. <<-\EOF
+	*   Merge the topic branch '\''onebranch'\''
+	|\
+	| * D
+	| * G
+	|/
+	o H
+	EOF
+'
+
 test_expect_success 'refs/rewritten/* is worktree-local' '
 	git worktree add wt &&
 	cat >wt/script-from-scratch <<-\EOF &&
-- 
2.15.1.windows.2.1430.ga56c4f9e2a9

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 1/8] sequencer: introduce new commands to reset the revision
  2018-01-18 15:35 ` [PATCH 1/8] sequencer: introduce new commands to reset the revision Johannes Schindelin
@ 2018-01-18 16:25   ` Jacob Keller
  2018-01-18 21:13     ` Johannes Schindelin
                       ` (2 more replies)
  2018-01-19  8:59   ` Eric Sunshine
  2018-01-19 12:24   ` [PATCH 1/8] sequencer: introduce new commands to resettherevision Phillip Wood
  2 siblings, 3 replies; 276+ messages in thread
From: Jacob Keller @ 2018-01-18 16:25 UTC (permalink / raw)
  To: Johannes Schindelin; +Cc: Git mailing list, Junio C Hamano

On Thu, Jan 18, 2018 at 7:35 AM, Johannes Schindelin
<johannes.schindelin@gmx.de> wrote:
> This commit implements the commands to label, and to reset to, given
> revisions. The syntax is:
>
>         label <name>
>         reset <name>
>
> As a convenience shortcut, also to improve readability of the generated
> todo list, a third command is introduced: bud. It simply resets to the
> "onto" revision, i.e. the commit onto which we currently rebase.
>

The code looks good, but I'm a little wary of adding bud which
hard-codes a specific label. I suppose it does grant a bit of
readability to the resulting script... ? It doesn't seem that
important compared to use using "reset onto"? At least when
documenting this it should be made clear that the "onto" label is
special.

Thanks,
Jake.

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 2/8] sequencer: introduce the `merge` command
  2018-01-18 15:35 ` [PATCH 2/8] sequencer: introduce the `merge` command Johannes Schindelin
@ 2018-01-18 16:31   ` Jacob Keller
  2018-01-18 21:22     ` Johannes Schindelin
  2018-01-19  9:54   ` Eric Sunshine
                     ` (2 subsequent siblings)
  3 siblings, 1 reply; 276+ messages in thread
From: Jacob Keller @ 2018-01-18 16:31 UTC (permalink / raw)
  To: Johannes Schindelin; +Cc: Git mailing list, Junio C Hamano

On Thu, Jan 18, 2018 at 7:35 AM, Johannes Schindelin
<johannes.schindelin@gmx.de> wrote:
> This patch is part of the effort to reimplement `--preserve-merges` with
> a substantially improved design, a design that has been developed in the
> Git for Windows project to maintain the dozens of Windows-specific patch
> series on top of upstream Git.
>
> The previous patch implemented the `label`, `bud` and `reset` commands
> to label commits and to reset to a labeled commits. This patch adds the
> `merge` command, with the following syntax:
>
>         merge <commit> <rev> <oneline>
>
> The <commit> parameter in this instance is the *original* merge commit,
> whose author and message will be used for the to-be-created merge
> commit.
>
> The <rev> parameter refers to the (possibly rewritten) revision to
> merge. Let's see an example of a todo list:
>
>         label onto
>
>         # Branch abc
>         bud
>         pick deadbeef Hello, world!
>         label abc
>
>         bud
>         pick cafecafe And now for something completely different
>         merge baaabaaa abc Merge the branch 'abc' into master
>
> To support creating *new* merges, i.e. without copying the commit
> message from an existing commit, use the special value `-` as <commit>
> parameter (in which case the text after the <rev> parameter is used as
> commit message):
>
>         merge - abc This will be the actual commit message of the merge
>
> This comes in handy when splitting a branch into two or more branches.
>

Would it be possible to open the editor with the supplied text when
there's no commit? The text after <rev> must be oneline only..

It's difficult to reword merges because of the nature of rebase
interactive, you can't just re-run the rebase command and use
"reword".

I suppose you could cheat by putting in an "edit" command that let you
create an empty commit with a message...

Thanks,
Jake

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 6/8] sequencer: handle autosquash and post-rewrite for merge commands
  2018-01-18 15:35 ` [PATCH 6/8] sequencer: handle autosquash and post-rewrite for merge commands Johannes Schindelin
@ 2018-01-18 16:43   ` Jacob Keller
  2018-01-18 21:27     ` Johannes Schindelin
  2018-01-23 20:27     ` Junio C Hamano
  0 siblings, 2 replies; 276+ messages in thread
From: Jacob Keller @ 2018-01-18 16:43 UTC (permalink / raw)
  To: Johannes Schindelin; +Cc: Git mailing list, Junio C Hamano

On Thu, Jan 18, 2018 at 7:35 AM, Johannes Schindelin
<johannes.schindelin@gmx.de> wrote:
> In the previous patches, we implemented the basic functionality of the
> `git rebase -i --recreate-merges` command, in particular the `merge`
> command to create merge commits in the sequencer.
>
> The interactive rebase is a lot more these days, though, than a simple
> cherry-pick in a loop. For example, it calls the post-rewrite hook (if
> any) after rebasing with a mapping of the old->new commits. And the
> interactive rebase also supports the autosquash mode, where commits
> whose oneline is of the form `fixup! <oneline>` or `squash! <oneline>`
> are rearranged to amend commits whose oneline they match.
>
> This patch implements the post-rewrite and autosquash handling for the
> `merge` command we just introduced. The other commands that were added
> recently (`label`, `reset` and `bud`) do not create new commits,
> therefore post-rewrite & autosquash do not need to handle them.
>
> Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
> ---
>  refs.c                            |  3 ++-
>  sequencer.c                       | 10 +++++++---
>  t/t3430-rebase-recreate-merges.sh | 39 +++++++++++++++++++++++++++++++++++++++
>  3 files changed, 48 insertions(+), 4 deletions(-)
>
> diff --git a/refs.c b/refs.c
> index 20ba82b4343..e8b84c189ff 100644
> --- a/refs.c
> +++ b/refs.c
> @@ -600,7 +600,8 @@ int dwim_log(const char *str, int len, struct object_id *oid, char **log)
>  static int is_per_worktree_ref(const char *refname)
>  {
>         return !strcmp(refname, "HEAD") ||
> -               starts_with(refname, "refs/bisect/");
> +               starts_with(refname, "refs/bisect/") ||
> +               starts_with(refname, "refs/rewritten/");
>  }

Would this part make more sense to move into the commit that
introduces writing these refs, or does it only matter once you start
this step here?

>
>  static int is_pseudoref_syntax(const char *refname)
> diff --git a/sequencer.c b/sequencer.c
> index 1bef16647b4..b63bfb9a141 100644
> --- a/sequencer.c
> +++ b/sequencer.c
> @@ -2413,10 +2413,13 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts)
>                         res = do_reset(item->arg, item->arg_len);
>                 else if (item->command == TODO_BUD)
>                         res = do_reset("onto", 4);
> -               else if (item->command == TODO_MERGE)
> +               else if (item->command == TODO_MERGE) {
>                         res = do_merge(item->commit,
>                                        item->arg, item->arg_len, opts);
> -               else if (!is_noop(item->command))
> +                       if (item->commit)
> +                               record_in_rewritten(&item->commit->object.oid,
> +                                                   peek_command(todo_list, 1));
> +               } else if (!is_noop(item->command))
>                         return error(_("unknown command %d"), item->command);
>
>                 todo_list->current++;
> @@ -3556,7 +3559,8 @@ int rearrange_squash(void)
>                 struct subject2item_entry *entry;
>
>                 next[i] = tail[i] = -1;
> -               if (item->command >= TODO_EXEC) {
> +               if (item->command >= TODO_EXEC &&
> +                   (item->command != TODO_MERGE || !item->commit)) {
>                         subjects[i] = NULL;
>                         continue;
>                 }
> diff --git a/t/t3430-rebase-recreate-merges.sh b/t/t3430-rebase-recreate-merges.sh
> index 46ae52f88b3..76e615bd7c1 100755
> --- a/t/t3430-rebase-recreate-merges.sh
> +++ b/t/t3430-rebase-recreate-merges.sh
> @@ -143,4 +143,43 @@ test_expect_success 'with a branch tip that was cherry-picked already' '
>         EOF
>  '
>
> +test_expect_success 'refs/rewritten/* is worktree-local' '
> +       git worktree add wt &&
> +       cat >wt/script-from-scratch <<-\EOF &&
> +       label xyz
> +       exec GIT_DIR=../.git git rev-parse --verify refs/rewritten/xyz >a || :
> +       exec git rev-parse --verify refs/rewritten/xyz >b
> +       EOF
> +
> +       test_config -C wt sequence.editor \""$PWD"/replace-editor.sh\" &&
> +       git -C wt rebase -i HEAD &&
> +       test_must_be_empty wt/a &&
> +       test_cmp_rev HEAD "$(cat wt/b)"
> +'
> +

Same for the test here, I can't figure out why this is necessary in
this patch as opposed to the patch which first introduced the
refs/rewritten/<label> refs.

> +test_expect_success 'post-rewrite hook and fixups work for merges' '
> +       git checkout -b post-rewrite &&
> +       test_commit same1 &&
> +       git reset --hard HEAD^ &&
> +       test_commit same2 &&
> +       git merge -m "to fix up" same1 &&
> +       echo same old same old >same2.t &&
> +       test_tick &&
> +       git commit --fixup HEAD same2.t &&
> +       fixup="$(git rev-parse HEAD)" &&
> +
> +       mkdir -p .git/hooks &&
> +       test_when_finished "rm .git/hooks/post-rewrite" &&
> +       echo "cat >actual" | write_script .git/hooks/post-rewrite &&
> +
> +       test_tick &&
> +       git rebase -i --autosquash --recreate-merges HEAD^^^ &&
> +       printf "%s %s\n%s %s\n%s %s\n%s %s\n" >expect $(git rev-parse \
> +               $fixup^^2 HEAD^2 \
> +               $fixup^^ HEAD^ \
> +               $fixup^ HEAD \
> +               $fixup HEAD) &&
> +       test_cmp expect actual
> +'
> +
>  test_done
> --
> 2.15.1.windows.2.1430.ga56c4f9e2a9
>
>

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 0/8] rebase -i: offer to recreate merge commits
  2018-01-18 15:35 [PATCH 0/8] rebase -i: offer to recreate merge commits Johannes Schindelin
                   ` (7 preceding siblings ...)
  2018-01-18 15:36 ` [PATCH 8/8] rebase -i: introduce --recreate-merges=no-rebase-cousins Johannes Schindelin
@ 2018-01-18 16:49 ` Jacob Keller
  2018-01-18 18:36 ` [PATCH 9, 10/8] interactive rebase feedback Stefan Beller
                   ` (3 subsequent siblings)
  12 siblings, 0 replies; 276+ messages in thread
From: Jacob Keller @ 2018-01-18 16:49 UTC (permalink / raw)
  To: Johannes Schindelin; +Cc: Git mailing list, Junio C Hamano

On Thu, Jan 18, 2018 at 7:35 AM, Johannes Schindelin
<johannes.schindelin@gmx.de> wrote:
> Once upon a time, I dreamt of an interactive rebase that would not
> flatten branch structure, but instead recreate the commit topology
> faithfully.
>
> My original attempt was --preserve-merges, but that design was so
> limited that I did not even enable it in interactive mode.
>
> Subsequently, it *was* enabled in interactive mode, with the predictable
> consequences: as the --preserve-merges design does not allow for
> specifying the parents of merge commits explicitly, all the new commits'
> parents are defined *implicitly* by the previous commit history, and
> hence it is *not possible to even reorder commits*.
>
> This design flaw cannot be fixed. Not without a complete re-design, at
> least. This patch series offers such a re-design.
>
> Think of --recreate-merges as "--preserve-merges done right". It
> introduces new verbs for the todo list, `label`, `reset` and `merge`.
> For a commit topology like this:
>
>             A - B - C
>               \   /
>                 D
>
> the generated todo list would look like this:
>
>             # branch D
>             pick 0123 A
>             label branch-point
>             pick 1234 D
>             label D
>
>             reset branch-point
>             pick 2345 B
>             merge 3456 D C
>
> There are more patches in the pipeline, based on this patch series, but
> left for later in the interest of reviewable patch series: one mini
> series to use the sequencer even for `git rebase -i --root`, and another
> one to add support for octopus merges to --recreate-merges.
>
>

I've been looking forward to seeing this hit the list! Overall I don't
think I have any major complaints, except to make sure the special
label "onto" is documented.

I think it's possible to "reword" or "edit" the merge commit by
inserting "x false" after the merge commnd to halt the rebase and let
the user perform commands to edit, so that should work well.

Thanks for taking the time to make this a reality!

Thanks,
Jake

> Johannes Schindelin (8):
>   sequencer: introduce new commands to reset the revision
>   sequencer: introduce the `merge` command
>   sequencer: fast-forward merge commits, if possible
>   rebase-helper --make-script: introduce a flag to recreate merges
>   rebase: introduce the --recreate-merges option
>   sequencer: handle autosquash and post-rewrite for merge commands
>   pull: accept --rebase=recreate to recreate the branch topology
>   rebase -i: introduce --recreate-merges=no-rebase-cousins
>
>  Documentation/config.txt               |   8 +
>  Documentation/git-pull.txt             |   5 +-
>  Documentation/git-rebase.txt           |  13 +-
>  builtin/pull.c                         |  14 +-
>  builtin/rebase--helper.c               |  13 +-
>  builtin/remote.c                       |   2 +
>  contrib/completion/git-completion.bash |   4 +-
>  git-rebase--interactive.sh             |   6 +
>  git-rebase.sh                          |  16 +
>  refs.c                                 |   3 +-
>  sequencer.c                            | 697 ++++++++++++++++++++++++++++++++-
>  sequencer.h                            |   9 +
>  t/t3430-rebase-recreate-merges.sh      | 208 ++++++++++
>  13 files changed, 977 insertions(+), 21 deletions(-)
>  create mode 100755 t/t3430-rebase-recreate-merges.sh
>
>
> base-commit: 2512f15446149235156528dafbe75930c712b29e
> Published-As: https://github.com/dscho/git/releases/tag/recreate-merges-v1
> Fetch-It-Via: git fetch https://github.com/dscho/git recreate-merges-v1
> --
> 2.15.1.windows.2.1430.ga56c4f9e2a9
>

^ permalink raw reply	[flat|nested] 276+ messages in thread

* [PATCH 9, 10/8] interactive rebase feedback
  2018-01-18 15:35 [PATCH 0/8] rebase -i: offer to recreate merge commits Johannes Schindelin
                   ` (8 preceding siblings ...)
  2018-01-18 16:49 ` [PATCH 0/8] rebase -i: offer to recreate merge commits Jacob Keller
@ 2018-01-18 18:36 ` Stefan Beller
  2018-01-18 18:36   ` [PATCH 9/8] [DO NOT APPLY, but squash?] git-rebase--interactive: clarify arguments Stefan Beller
  2018-01-18 18:36   ` [PATCH 10/8] [DO NOT APPLY, but improve?] rebase--interactive: introduce "stop" command Stefan Beller
  2018-01-19 20:25 ` [PATCH 0/8] rebase -i: offer to recreate merge commits Junio C Hamano
                   ` (2 subsequent siblings)
  12 siblings, 2 replies; 276+ messages in thread
From: Stefan Beller @ 2018-01-18 18:36 UTC (permalink / raw)
  To: johannes.schindelin; +Cc: git, gitster, jacob.keller, Stefan Beller

I thought of writing a reply, but instead I wrote it in patch form.

Stefan Beller (2):
  [DO NOT APPLY, but squash?] git-rebase--interactive: clarify arguments
  [DO NOT APPLY, but improve?] rebase--interactive: introduce "stop"
    command

 git-rebase--interactive.sh | 23 ++++++++++++-----------
 sequencer.c                | 10 ++++++++++
 2 files changed, 22 insertions(+), 11 deletions(-)

-- 
2.16.0.rc1.238.g530d649a79-goog


^ permalink raw reply	[flat|nested] 276+ messages in thread

* [PATCH 9/8] [DO NOT APPLY, but squash?] git-rebase--interactive: clarify arguments
  2018-01-18 18:36 ` [PATCH 9, 10/8] interactive rebase feedback Stefan Beller
@ 2018-01-18 18:36   ` Stefan Beller
  2018-01-18 21:18     ` Jacob Keller
  2018-01-18 21:36     ` Johannes Schindelin
  2018-01-18 18:36   ` [PATCH 10/8] [DO NOT APPLY, but improve?] rebase--interactive: introduce "stop" command Stefan Beller
  1 sibling, 2 replies; 276+ messages in thread
From: Stefan Beller @ 2018-01-18 18:36 UTC (permalink / raw)
  To: johannes.schindelin; +Cc: git, gitster, jacob.keller, Stefan Beller

Up to now each command took a commit as its first argument and ignored
the rest of the line (usually the subject of the commit)

Now that we have commands that take different arguments, clarify each
command by giving the argument list.

Signed-off-by: Stefan Beller <sbeller@google.com>
---
 git-rebase--interactive.sh | 22 +++++++++++-----------
 1 file changed, 11 insertions(+), 11 deletions(-)

diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh
index 23184c77e8..3cd7446d0b 100644
--- a/git-rebase--interactive.sh
+++ b/git-rebase--interactive.sh
@@ -155,17 +155,17 @@ reschedule_last_action () {
 append_todo_help () {
 	gettext "
 Commands:
-p, pick = use commit
-r, reword = use commit, but edit the commit message
-e, edit = use commit, but stop for amending
-s, squash = use commit, but meld into previous commit
-f, fixup = like \"squash\", but discard this commit's log message
-x, exec = run command (the rest of the line) using shell
-d, drop = remove commit
-l, label = label current HEAD with a name
-t, reset = reset HEAD to a label
-b, bud = reset HEAD to the revision labeled 'onto'
-m, merge = create a merge commit using a given commit's message
+p, pick <commit> = use commit
+r, reword <commit> = use commit, but edit the commit message
+e, edit <commit> = use commit, but stop for amending
+s, squash <commit> = use commit, but meld into previous commit
+f, fixup <commit> = like \"squash\", but discard this commit's log message
+x, exec <commit> = run command (the rest of the line) using shell
+d, drop <commit> = remove commit
+l, label <label>= label current HEAD with a name
+t, reset <label> = reset HEAD to a label
+b, bud = reset HEAD to the revision labeled 'onto', no arguments
+m, merge [<label-or-commit>]* = create a merge commit using a given commit's message
 
 These lines can be re-ordered; they are executed from top to bottom.
 " | git stripspace --comment-lines >>"$todo"
-- 
2.16.0.rc1.238.g530d649a79-goog


^ permalink raw reply	[flat|nested] 276+ messages in thread

* [PATCH 10/8] [DO NOT APPLY, but improve?] rebase--interactive: introduce "stop" command
  2018-01-18 18:36 ` [PATCH 9, 10/8] interactive rebase feedback Stefan Beller
  2018-01-18 18:36   ` [PATCH 9/8] [DO NOT APPLY, but squash?] git-rebase--interactive: clarify arguments Stefan Beller
@ 2018-01-18 18:36   ` Stefan Beller
  2018-01-18 21:20     ` Jacob Keller
  2018-01-18 22:00     ` Johannes Schindelin
  1 sibling, 2 replies; 276+ messages in thread
From: Stefan Beller @ 2018-01-18 18:36 UTC (permalink / raw)
  To: johannes.schindelin; +Cc: git, gitster, jacob.keller, Stefan Beller

Jake suggested using "x false" instead of "edit" for some corner cases.

I do prefer using "x false" for all kinds of things such as stopping
before a commit (edit only let's you stop after a commit), and the
knowledge that "x false" does the least amount of actions behind my back.

We should have that command as well, maybe?

Signed-off-by: Stefan Beller <sbeller@google.com>
---
 git-rebase--interactive.sh |  1 +
 sequencer.c                | 10 ++++++++++
 2 files changed, 11 insertions(+)

diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh
index 3cd7446d0b..9eac53f0c5 100644
--- a/git-rebase--interactive.sh
+++ b/git-rebase--interactive.sh
@@ -166,6 +166,7 @@ l, label <label>= label current HEAD with a name
 t, reset <label> = reset HEAD to a label
 b, bud = reset HEAD to the revision labeled 'onto', no arguments
 m, merge [<label-or-commit>]* = create a merge commit using a given commit's message
+y, stay = stop for  shortcut for
 
 These lines can be re-ordered; they are executed from top to bottom.
 " | git stripspace --comment-lines >>"$todo"
diff --git a/sequencer.c b/sequencer.c
index 2b4e6b1232..4b3b9fe59d 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -782,6 +782,7 @@ enum todo_command {
 	TODO_RESET,
 	TODO_BUD,
 	TODO_MERGE,
+	TODO_STOP,
 	/* commands that do nothing but are counted for reporting progress */
 	TODO_NOOP,
 	TODO_DROP,
@@ -803,6 +804,7 @@ static struct {
 	{ 'l', "label" },
 	{ 't', "reset" },
 	{ 'b', "bud" },
+	{ 'y', "stay" },
 	{ 'm', "merge" },
 	{ 0,   "noop" },
 	{ 'd', "drop" },
@@ -1307,6 +1309,12 @@ static int parse_insn_line(struct todo_item *item, const char *bol, char *eol)
 		return 0;
 	}
 
+	if (item->command == TODO_STOP) {
+		item->commit = NULL;
+		item->arg = "";
+		item->arg_len = 0;
+	}
+
 	end_of_object_name = (char *) bol + strcspn(bol, " \t\n");
 	item->arg = end_of_object_name + strspn(end_of_object_name, " \t");
 	item->arg_len = (int)(eol - item->arg);
@@ -2407,6 +2415,8 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts)
 				/* `current` will be incremented below */
 				todo_list->current = -1;
 			}
+		} else if (item->command == TODO_STOP) {
+			todo_list->current = -1;
 		} else if (item->command == TODO_LABEL)
 			res = do_label(item->arg, item->arg_len);
 		else if (item->command == TODO_RESET)
-- 
2.16.0.rc1.238.g530d649a79-goog


^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 1/8] sequencer: introduce new commands to reset the revision
  2018-01-18 16:25   ` Jacob Keller
@ 2018-01-18 21:13     ` Johannes Schindelin
  2018-01-18 21:21       ` Jacob Keller
  2018-01-18 21:24     ` Philip Oakley
  2018-01-22 21:25     ` Junio C Hamano
  2 siblings, 1 reply; 276+ messages in thread
From: Johannes Schindelin @ 2018-01-18 21:13 UTC (permalink / raw)
  To: Jacob Keller; +Cc: Git mailing list, Junio C Hamano

Hi Jake,

On Thu, 18 Jan 2018, Jacob Keller wrote:

> On Thu, Jan 18, 2018 at 7:35 AM, Johannes Schindelin
> <johannes.schindelin@gmx.de> wrote:
> > This commit implements the commands to label, and to reset to, given
> > revisions. The syntax is:
> >
> >         label <name>
> >         reset <name>
> >
> > As a convenience shortcut, also to improve readability of the generated
> > todo list, a third command is introduced: bud. It simply resets to the
> > "onto" revision, i.e. the commit onto which we currently rebase.
> >
> 
> The code looks good, but I'm a little wary of adding bud which
> hard-codes a specific label. I suppose it does grant a bit of
> readability to the resulting script... ? It doesn't seem that
> important compared to use using "reset onto"? At least when
> documenting this it should be made clear that the "onto" label is
> special.

Indeed, `bud` helped me more in the earlier times of the Git garden shears
when I had to *introduce* branch structure into the long list of Git for
Windows' patches.

If there are no voices in favor of `bud` other than mine, I will remove
support for it in the next iteration.

Ciao,
Dscho

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 9/8] [DO NOT APPLY, but squash?] git-rebase--interactive: clarify arguments
  2018-01-18 18:36   ` [PATCH 9/8] [DO NOT APPLY, but squash?] git-rebase--interactive: clarify arguments Stefan Beller
@ 2018-01-18 21:18     ` Jacob Keller
  2018-01-18 21:36     ` Johannes Schindelin
  1 sibling, 0 replies; 276+ messages in thread
From: Jacob Keller @ 2018-01-18 21:18 UTC (permalink / raw)
  To: Stefan Beller; +Cc: Johannes Schindelin, Git mailing list, Junio C Hamano

On Thu, Jan 18, 2018 at 10:36 AM, Stefan Beller <sbeller@google.com> wrote:
> Up to now each command took a commit as its first argument and ignored
> the rest of the line (usually the subject of the commit)
>
> Now that we have commands that take different arguments, clarify each
> command by giving the argument list.
>
> Signed-off-by: Stefan Beller <sbeller@google.com>
> ---
>  git-rebase--interactive.sh | 22 +++++++++++-----------
>  1 file changed, 11 insertions(+), 11 deletions(-)
>
> diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh
> index 23184c77e8..3cd7446d0b 100644
> --- a/git-rebase--interactive.sh
> +++ b/git-rebase--interactive.sh
> @@ -155,17 +155,17 @@ reschedule_last_action () {
>  append_todo_help () {
>         gettext "
>  Commands:
> -p, pick = use commit
> -r, reword = use commit, but edit the commit message
> -e, edit = use commit, but stop for amending
> -s, squash = use commit, but meld into previous commit
> -f, fixup = like \"squash\", but discard this commit's log message
> -x, exec = run command (the rest of the line) using shell
> -d, drop = remove commit
> -l, label = label current HEAD with a name
> -t, reset = reset HEAD to a label
> -b, bud = reset HEAD to the revision labeled 'onto'
> -m, merge = create a merge commit using a given commit's message
> +p, pick <commit> = use commit
> +r, reword <commit> = use commit, but edit the commit message
> +e, edit <commit> = use commit, but stop for amending
> +s, squash <commit> = use commit, but meld into previous commit
> +f, fixup <commit> = like \"squash\", but discard this commit's log message
> +x, exec <commit> = run command (the rest of the line) using shell
> +d, drop <commit> = remove commit
> +l, label <label>= label current HEAD with a name
> +t, reset <label> = reset HEAD to a label
> +b, bud = reset HEAD to the revision labeled 'onto', no arguments
> +m, merge [<label-or-commit>]* = create a merge commit using a given commit's message

The merge arguments are complicated, i'm not sure how best to add
clarification here.. I think it might need its own paragraph, since it
takes something like: <commit> <parents to merge>* <possibly message>?

Thanks,
Jake

>
>  These lines can be re-ordered; they are executed from top to bottom.
>  " | git stripspace --comment-lines >>"$todo"
> --
> 2.16.0.rc1.238.g530d649a79-goog
>

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 10/8] [DO NOT APPLY, but improve?] rebase--interactive: introduce "stop" command
  2018-01-18 18:36   ` [PATCH 10/8] [DO NOT APPLY, but improve?] rebase--interactive: introduce "stop" command Stefan Beller
@ 2018-01-18 21:20     ` Jacob Keller
  2018-01-18 22:08       ` Philip Oakley
  2018-01-18 22:00     ` Johannes Schindelin
  1 sibling, 1 reply; 276+ messages in thread
From: Jacob Keller @ 2018-01-18 21:20 UTC (permalink / raw)
  To: Stefan Beller; +Cc: Johannes Schindelin, Git mailing list, Junio C Hamano

On Thu, Jan 18, 2018 at 10:36 AM, Stefan Beller <sbeller@google.com> wrote:
> Jake suggested using "x false" instead of "edit" for some corner cases.
>
> I do prefer using "x false" for all kinds of things such as stopping
> before a commit (edit only let's you stop after a commit), and the
> knowledge that "x false" does the least amount of actions behind my back.
>
> We should have that command as well, maybe?
>


I agree. I use "x false" very often, and I think stop is probably a
better solution since it avoids spawning an extra shell that will just
fail. Not sure if stop implies too much about "stop the whole thing"
as opposed to "stop here and let me do something manual", but I think
it's clear enough.

Thanks,
Jake

> Signed-off-by: Stefan Beller <sbeller@google.com>
> ---
>  git-rebase--interactive.sh |  1 +
>  sequencer.c                | 10 ++++++++++
>  2 files changed, 11 insertions(+)
>
> diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh
> index 3cd7446d0b..9eac53f0c5 100644
> --- a/git-rebase--interactive.sh
> +++ b/git-rebase--interactive.sh
> @@ -166,6 +166,7 @@ l, label <label>= label current HEAD with a name
>  t, reset <label> = reset HEAD to a label
>  b, bud = reset HEAD to the revision labeled 'onto', no arguments
>  m, merge [<label-or-commit>]* = create a merge commit using a given commit's message
> +y, stay = stop for  shortcut for
>
>  These lines can be re-ordered; they are executed from top to bottom.
>  " | git stripspace --comment-lines >>"$todo"
> diff --git a/sequencer.c b/sequencer.c
> index 2b4e6b1232..4b3b9fe59d 100644
> --- a/sequencer.c
> +++ b/sequencer.c
> @@ -782,6 +782,7 @@ enum todo_command {
>         TODO_RESET,
>         TODO_BUD,
>         TODO_MERGE,
> +       TODO_STOP,
>         /* commands that do nothing but are counted for reporting progress */
>         TODO_NOOP,
>         TODO_DROP,
> @@ -803,6 +804,7 @@ static struct {
>         { 'l', "label" },
>         { 't', "reset" },
>         { 'b', "bud" },
> +       { 'y', "stay" },
>         { 'm', "merge" },
>         { 0,   "noop" },
>         { 'd', "drop" },
> @@ -1307,6 +1309,12 @@ static int parse_insn_line(struct todo_item *item, const char *bol, char *eol)
>                 return 0;
>         }
>
> +       if (item->command == TODO_STOP) {
> +               item->commit = NULL;
> +               item->arg = "";
> +               item->arg_len = 0;
> +       }
> +
>         end_of_object_name = (char *) bol + strcspn(bol, " \t\n");
>         item->arg = end_of_object_name + strspn(end_of_object_name, " \t");
>         item->arg_len = (int)(eol - item->arg);
> @@ -2407,6 +2415,8 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts)
>                                 /* `current` will be incremented below */
>                                 todo_list->current = -1;
>                         }
> +               } else if (item->command == TODO_STOP) {
> +                       todo_list->current = -1;
>                 } else if (item->command == TODO_LABEL)
>                         res = do_label(item->arg, item->arg_len);
>                 else if (item->command == TODO_RESET)
> --
> 2.16.0.rc1.238.g530d649a79-goog
>

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 1/8] sequencer: introduce new commands to reset the revision
  2018-01-18 21:13     ` Johannes Schindelin
@ 2018-01-18 21:21       ` Jacob Keller
  0 siblings, 0 replies; 276+ messages in thread
From: Jacob Keller @ 2018-01-18 21:21 UTC (permalink / raw)
  To: Johannes Schindelin; +Cc: Git mailing list, Junio C Hamano

On Thu, Jan 18, 2018 at 1:13 PM, Johannes Schindelin
<Johannes.Schindelin@gmx.de> wrote:
> Hi Jake,
>
> On Thu, 18 Jan 2018, Jacob Keller wrote:
>
>> On Thu, Jan 18, 2018 at 7:35 AM, Johannes Schindelin
>> <johannes.schindelin@gmx.de> wrote:
>> > This commit implements the commands to label, and to reset to, given
>> > revisions. The syntax is:
>> >
>> >         label <name>
>> >         reset <name>
>> >
>> > As a convenience shortcut, also to improve readability of the generated
>> > todo list, a third command is introduced: bud. It simply resets to the
>> > "onto" revision, i.e. the commit onto which we currently rebase.
>> >
>>
>> The code looks good, but I'm a little wary of adding bud which
>> hard-codes a specific label. I suppose it does grant a bit of
>> readability to the resulting script... ? It doesn't seem that
>> important compared to use using "reset onto"? At least when
>> documenting this it should be made clear that the "onto" label is
>> special.
>
> Indeed, `bud` helped me more in the earlier times of the Git garden shears
> when I had to *introduce* branch structure into the long list of Git for
> Windows' patches.
>
> If there are no voices in favor of `bud` other than mine, I will remove
> support for it in the next iteration.
>
> Ciao,
> Dscho

After I saw more examples, I don't mind it. I do think it's not
strictly necessary, but it seemed to help me with the readability. My
only main concern is that it's limited to a special label.

Thanks,
Jake

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 2/8] sequencer: introduce the `merge` command
  2018-01-18 16:31   ` Jacob Keller
@ 2018-01-18 21:22     ` Johannes Schindelin
  2018-01-18 21:26       ` Jacob Keller
  0 siblings, 1 reply; 276+ messages in thread
From: Johannes Schindelin @ 2018-01-18 21:22 UTC (permalink / raw)
  To: Jacob Keller; +Cc: Git mailing list, Junio C Hamano

Hi Jake,

On Thu, 18 Jan 2018, Jacob Keller wrote:

> On Thu, Jan 18, 2018 at 7:35 AM, Johannes Schindelin
> <johannes.schindelin@gmx.de> wrote:
> > This patch is part of the effort to reimplement `--preserve-merges` with
> > a substantially improved design, a design that has been developed in the
> > Git for Windows project to maintain the dozens of Windows-specific patch
> > series on top of upstream Git.
> >
> > The previous patch implemented the `label`, `bud` and `reset` commands
> > to label commits and to reset to a labeled commits. This patch adds the
> > `merge` command, with the following syntax:
> >
> >         merge <commit> <rev> <oneline>
> >
> > The <commit> parameter in this instance is the *original* merge commit,
> > whose author and message will be used for the to-be-created merge
> > commit.
> >
> > The <rev> parameter refers to the (possibly rewritten) revision to
> > merge. Let's see an example of a todo list:
> >
> >         label onto
> >
> >         # Branch abc
> >         bud
> >         pick deadbeef Hello, world!
> >         label abc
> >
> >         bud
> >         pick cafecafe And now for something completely different
> >         merge baaabaaa abc Merge the branch 'abc' into master
> >
> > To support creating *new* merges, i.e. without copying the commit
> > message from an existing commit, use the special value `-` as <commit>
> > parameter (in which case the text after the <rev> parameter is used as
> > commit message):
> >
> >         merge - abc This will be the actual commit message of the merge
> >
> > This comes in handy when splitting a branch into two or more branches.
> >
> 
> Would it be possible to open the editor with the supplied text when
> there's no commit?  The text after <rev> must be oneline only..

I actually want to avoid that because my main use case is fire-and-forget,
i.e. I want to edit only the todo list and then (barring any merge
conflicts) I do not want to edit anything anymore.

But I guess we could special-case the thing where `-` is specified as
"merge commit message provider" and an empty oneline is provided?

> It's difficult to reword merges because of the nature of rebase
> interactive, you can't just re-run the rebase command and use
> "reword".
> 
> I suppose you could cheat by putting in an "edit" command that let you
> create an empty commit with a message...

Or you could "cheat" by adding `exec git commit --amend`...

Seriously again, I have no good idea how to provide an equivalent to the
`reword` verb that would work on merge commits...

Anyone?

Ciao,
Dscho

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 1/8] sequencer: introduce new commands to reset the revision
  2018-01-18 16:25   ` Jacob Keller
  2018-01-18 21:13     ` Johannes Schindelin
@ 2018-01-18 21:24     ` Philip Oakley
  2018-01-18 21:28       ` Jacob Keller
  2018-01-29 20:28       ` Johannes Schindelin
  2018-01-22 21:25     ` Junio C Hamano
  2 siblings, 2 replies; 276+ messages in thread
From: Philip Oakley @ 2018-01-18 21:24 UTC (permalink / raw)
  To: Jacob Keller, Johannes Schindelin; +Cc: Git mailing list, Junio C Hamano

From: "Jacob Keller" <jacob.keller@gmail.com>
> On Thu, Jan 18, 2018 at 7:35 AM, Johannes Schindelin
> <johannes.schindelin@gmx.de> wrote:
>> This commit implements the commands to label, and to reset to, given
>> revisions. The syntax is:
>>
>>         label <name>
>>         reset <name>
>>
>> As a convenience shortcut, also to improve readability of the generated
>> todo list, a third command is introduced: bud. It simply resets to the
>> "onto" revision, i.e. the commit onto which we currently rebase.
>>
>
> The code looks good, but I'm a little wary of adding bud which
> hard-codes a specific label. I suppose it does grant a bit of
> readability to the resulting script... ? It doesn't seem that
> important compared to use using "reset onto"? At least when
> documenting this it should be made clear that the "onto" label is
> special.
>
> Thanks,
> Jake.

I'd agree.

The special 'onto' label should be fully documented, and the commit message 
should indicate which patch actually defines it (and all its corner cases 
and fall backs if --onto isn't explicitly given..)

Likewise the choice of 'bud' should be explained with some nice phraseology 
indicating that we are growing the new flowering from the bud, otherwise the 
word is a bit too short and sudden for easy explanation.

Philip 


^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 2/8] sequencer: introduce the `merge` command
  2018-01-18 21:22     ` Johannes Schindelin
@ 2018-01-18 21:26       ` Jacob Keller
  0 siblings, 0 replies; 276+ messages in thread
From: Jacob Keller @ 2018-01-18 21:26 UTC (permalink / raw)
  To: Johannes Schindelin; +Cc: Git mailing list, Junio C Hamano

On Thu, Jan 18, 2018 at 1:22 PM, Johannes Schindelin
<Johannes.Schindelin@gmx.de> wrote:
>> Would it be possible to open the editor with the supplied text when
>> there's no commit?  The text after <rev> must be oneline only..
>
> I actually want to avoid that because my main use case is fire-and-forget,
> i.e. I want to edit only the todo list and then (barring any merge
> conflicts) I do not want to edit anything anymore.
>

Agreed, for the case where we copy a commit message, I do not want the
editor either.

> But I guess we could special-case the thing where `-` is specified as
> "merge commit message provider" and an empty oneline is provided?
>

It's for when there is a new merge, for when we are creating a new one
using "-", yes.

>> It's difficult to reword merges because of the nature of rebase
>> interactive, you can't just re-run the rebase command and use
>> "reword".
>>
>> I suppose you could cheat by putting in an "edit" command that let you
>> create an empty commit with a message...
>
> Or you could "cheat" by adding `exec git commit --amend`...
>
> Seriously again, I have no good idea how to provide an equivalent to the
> `reword` verb that would work on merge commits...
>

Given that there is a work around, and I doubt it's that common, I'm
not sure we need one, plus i have no idea what verb to use....

We could allow reword on its own to simply reword the top commit?

That being said, since there's a simple-ish workaruond using "stop",
or "exec git commit --amend" I don't see this as being important
enough to worry about now.

Thanks,
Jake

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 6/8] sequencer: handle autosquash and post-rewrite for merge commands
  2018-01-18 16:43   ` Jacob Keller
@ 2018-01-18 21:27     ` Johannes Schindelin
  2018-01-18 21:29       ` Jacob Keller
  2018-01-23 20:27     ` Junio C Hamano
  1 sibling, 1 reply; 276+ messages in thread
From: Johannes Schindelin @ 2018-01-18 21:27 UTC (permalink / raw)
  To: Jacob Keller; +Cc: Git mailing list, Junio C Hamano

Hi Jake,

On Thu, 18 Jan 2018, Jacob Keller wrote:

> On Thu, Jan 18, 2018 at 7:35 AM, Johannes Schindelin
> <johannes.schindelin@gmx.de> wrote:
> > In the previous patches, we implemented the basic functionality of the
> > `git rebase -i --recreate-merges` command, in particular the `merge`
> > command to create merge commits in the sequencer.
> >
> > The interactive rebase is a lot more these days, though, than a simple
> > cherry-pick in a loop. For example, it calls the post-rewrite hook (if
> > any) after rebasing with a mapping of the old->new commits. And the
> > interactive rebase also supports the autosquash mode, where commits
> > whose oneline is of the form `fixup! <oneline>` or `squash! <oneline>`
> > are rearranged to amend commits whose oneline they match.
> >
> > This patch implements the post-rewrite and autosquash handling for the
> > `merge` command we just introduced. The other commands that were added
> > recently (`label`, `reset` and `bud`) do not create new commits,
> > therefore post-rewrite & autosquash do not need to handle them.
> >
> > Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
> > ---
> >  refs.c                            |  3 ++-
> >  sequencer.c                       | 10 +++++++---
> >  t/t3430-rebase-recreate-merges.sh | 39 +++++++++++++++++++++++++++++++++++++++
> >  3 files changed, 48 insertions(+), 4 deletions(-)
> >
> > diff --git a/refs.c b/refs.c
> > index 20ba82b4343..e8b84c189ff 100644
> > --- a/refs.c
> > +++ b/refs.c
> > @@ -600,7 +600,8 @@ int dwim_log(const char *str, int len, struct object_id *oid, char **log)
> >  static int is_per_worktree_ref(const char *refname)
> >  {
> >         return !strcmp(refname, "HEAD") ||
> > -               starts_with(refname, "refs/bisect/");
> > +               starts_with(refname, "refs/bisect/") ||
> > +               starts_with(refname, "refs/rewritten/");
> >  }
> 
> Would this part make more sense to move into the commit that
> introduces writing these refs, or does it only matter once you start
> this step here?
> 
> >
> >  static int is_pseudoref_syntax(const char *refname)
> > diff --git a/sequencer.c b/sequencer.c
> > index 1bef16647b4..b63bfb9a141 100644
> > --- a/sequencer.c
> > +++ b/sequencer.c
> > @@ -2413,10 +2413,13 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts)
> >                         res = do_reset(item->arg, item->arg_len);
> >                 else if (item->command == TODO_BUD)
> >                         res = do_reset("onto", 4);
> > -               else if (item->command == TODO_MERGE)
> > +               else if (item->command == TODO_MERGE) {
> >                         res = do_merge(item->commit,
> >                                        item->arg, item->arg_len, opts);
> > -               else if (!is_noop(item->command))
> > +                       if (item->commit)
> > +                               record_in_rewritten(&item->commit->object.oid,
> > +                                                   peek_command(todo_list, 1));
> > +               } else if (!is_noop(item->command))
> >                         return error(_("unknown command %d"), item->command);
> >
> >                 todo_list->current++;
> > @@ -3556,7 +3559,8 @@ int rearrange_squash(void)
> >                 struct subject2item_entry *entry;
> >
> >                 next[i] = tail[i] = -1;
> > -               if (item->command >= TODO_EXEC) {
> > +               if (item->command >= TODO_EXEC &&
> > +                   (item->command != TODO_MERGE || !item->commit)) {
> >                         subjects[i] = NULL;
> >                         continue;
> >                 }
> > diff --git a/t/t3430-rebase-recreate-merges.sh b/t/t3430-rebase-recreate-merges.sh
> > index 46ae52f88b3..76e615bd7c1 100755
> > --- a/t/t3430-rebase-recreate-merges.sh
> > +++ b/t/t3430-rebase-recreate-merges.sh
> > @@ -143,4 +143,43 @@ test_expect_success 'with a branch tip that was cherry-picked already' '
> >         EOF
> >  '
> >
> > +test_expect_success 'refs/rewritten/* is worktree-local' '
> > +       git worktree add wt &&
> > +       cat >wt/script-from-scratch <<-\EOF &&
> > +       label xyz
> > +       exec GIT_DIR=../.git git rev-parse --verify refs/rewritten/xyz >a || :
> > +       exec git rev-parse --verify refs/rewritten/xyz >b
> > +       EOF
> > +
> > +       test_config -C wt sequence.editor \""$PWD"/replace-editor.sh\" &&
> > +       git -C wt rebase -i HEAD &&
> > +       test_must_be_empty wt/a &&
> > +       test_cmp_rev HEAD "$(cat wt/b)"
> > +'
> > +
> 
> Same for the test here, I can't figure out why this is necessary in
> this patch as opposed to the patch which first introduced the
> refs/rewritten/<label> refs.

Woops. This was its own commit, and must have been accidentally squashed
during one of my rebases. Will re-introduce it; this is roughly what it
will look like:

-- snipsnap --
Author: Johannes Schindelin <johannes.schindelin@gmx.de>
Subject: sequencer: make refs generated by the `label` command worktree-local

This allows for rebases to be run in parallel in separate worktrees
(think: interrupted in the middle of one rebase, being asked to perform
a different rebase, adding a separate worktree just for that job).

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>

diff --git a/refs.c b/refs.c
index 20ba82b4343..e8b84c189ff 100644
--- a/refs.c
+++ b/refs.c
@@ -600,7 +600,8 @@ int dwim_log(const char *str, int len, struct object_id *oid, char **log)
 static int is_per_worktree_ref(const char *refname)
 {
 	return !strcmp(refname, "HEAD") ||
-		starts_with(refname, "refs/bisect/");
+		starts_with(refname, "refs/bisect/") ||
+		starts_with(refname, "refs/rewritten/");
 }
 
 static int is_pseudoref_syntax(const char *refname)
diff --git a/t/t3430-rebase-recreate-merges.sh b/t/t3430-rebase-recreate-merges.sh
index f5e476d948b..50a8eefc81e 100755
--- a/t/t3430-rebase-recreate-merges.sh
+++ b/t/t3430-rebase-recreate-merges.sh
@@ -115,4 +115,18 @@ test_expect_success 'generate correct todo list' '
 	test_cmp expect output
 '
 
+test_expect_success 'refs/rewritten/* is worktree-local' '
+	git worktree add wt &&
+	cat >wt/script-from-scratch <<-\EOF &&
+	label xyz
+	exec GIT_DIR=../.git git rev-parse --verify refs/rewritten/xyz >a || :
+	exec git rev-parse --verify refs/rewritten/xyz >b
+	EOF
+
+	test_config -C wt sequence.editor \""$PWD"/replace-editor.sh\" &&
+	git -C wt rebase -i HEAD &&
+	test_must_be_empty wt/a &&
+	test_cmp_rev HEAD "$(cat wt/b)"
+'
+
 test_done

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 1/8] sequencer: introduce new commands to reset the revision
  2018-01-18 21:24     ` Philip Oakley
@ 2018-01-18 21:28       ` Jacob Keller
  2018-01-29 20:28       ` Johannes Schindelin
  1 sibling, 0 replies; 276+ messages in thread
From: Jacob Keller @ 2018-01-18 21:28 UTC (permalink / raw)
  To: Philip Oakley; +Cc: Johannes Schindelin, Git mailing list, Junio C Hamano

On Thu, Jan 18, 2018 at 1:24 PM, Philip Oakley <philipoakley@iee.org> wrote:
> From: "Jacob Keller" <jacob.keller@gmail.com>
>
>> On Thu, Jan 18, 2018 at 7:35 AM, Johannes Schindelin
>> <johannes.schindelin@gmx.de> wrote:
>>>
>>> This commit implements the commands to label, and to reset to, given
>>> revisions. The syntax is:
>>>
>>>         label <name>
>>>         reset <name>
>>>
>>> As a convenience shortcut, also to improve readability of the generated
>>> todo list, a third command is introduced: bud. It simply resets to the
>>> "onto" revision, i.e. the commit onto which we currently rebase.
>>>
>>
>> The code looks good, but I'm a little wary of adding bud which
>> hard-codes a specific label. I suppose it does grant a bit of
>> readability to the resulting script... ? It doesn't seem that
>> important compared to use using "reset onto"? At least when
>> documenting this it should be made clear that the "onto" label is
>> special.
>>
>> Thanks,
>> Jake.
>
>
> I'd agree.
>
> The special 'onto' label should be fully documented, and the commit message
> should indicate which patch actually defines it (and all its corner cases
> and fall backs if --onto isn't explicitly given..)

I don't think it actually relates to "--onto" but rather to simply
using "label onto" in your sequencer script allows bud to work, and
simply shortens the overall work necessary. It's equivalent to "reset
onto" if I understand.

>
> Likewise the choice of 'bud' should be explained with some nice phraseology
> indicating that we are growing the new flowering from the bud, otherwise the
> word is a bit too short and sudden for easy explanation.
>
> Philip

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 6/8] sequencer: handle autosquash and post-rewrite for merge commands
  2018-01-18 21:27     ` Johannes Schindelin
@ 2018-01-18 21:29       ` Jacob Keller
  0 siblings, 0 replies; 276+ messages in thread
From: Jacob Keller @ 2018-01-18 21:29 UTC (permalink / raw)
  To: Johannes Schindelin; +Cc: Git mailing list, Junio C Hamano

>> Same for the test here, I can't figure out why this is necessary in
>> this patch as opposed to the patch which first introduced the
>> refs/rewritten/<label> refs.
>
> Woops. This was its own commit, and must have been accidentally squashed
> during one of my rebases. Will re-introduce it;

Yep that makes sense!

Thanks,
Jake

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 9/8] [DO NOT APPLY, but squash?] git-rebase--interactive: clarify arguments
  2018-01-18 18:36   ` [PATCH 9/8] [DO NOT APPLY, but squash?] git-rebase--interactive: clarify arguments Stefan Beller
  2018-01-18 21:18     ` Jacob Keller
@ 2018-01-18 21:36     ` Johannes Schindelin
  2018-01-18 21:58       ` Stefan Beller
  2018-01-19 20:30       ` Junio C Hamano
  1 sibling, 2 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-01-18 21:36 UTC (permalink / raw)
  To: Stefan Beller; +Cc: git, gitster, jacob.keller

Hi Stefan,

On Thu, 18 Jan 2018, Stefan Beller wrote:

> Up to now each command took a commit as its first argument and ignored
> the rest of the line (usually the subject of the commit)
> 
> Now that we have commands that take different arguments, clarify each
> command by giving the argument list.
> 
> Signed-off-by: Stefan Beller <sbeller@google.com>
> ---
>  git-rebase--interactive.sh | 22 +++++++++++-----------
>  1 file changed, 11 insertions(+), 11 deletions(-)
> 
> diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh
> index 23184c77e8..3cd7446d0b 100644
> --- a/git-rebase--interactive.sh
> +++ b/git-rebase--interactive.sh
> @@ -155,17 +155,17 @@ reschedule_last_action () {
>  append_todo_help () {
>  	gettext "
>  Commands:
> -p, pick = use commit
> -r, reword = use commit, but edit the commit message
> -e, edit = use commit, but stop for amending
> -s, squash = use commit, but meld into previous commit
> -f, fixup = like \"squash\", but discard this commit's log message
> -x, exec = run command (the rest of the line) using shell
> -d, drop = remove commit
> -l, label = label current HEAD with a name
> -t, reset = reset HEAD to a label
> -b, bud = reset HEAD to the revision labeled 'onto'
> -m, merge = create a merge commit using a given commit's message
> +p, pick <commit> = use commit
> +r, reword <commit> = use commit, but edit the commit message
> +e, edit <commit> = use commit, but stop for amending
> +s, squash <commit> = use commit, but meld into previous commit
> +f, fixup <commit> = like \"squash\", but discard this commit's log message
> +x, exec <commit> = run command (the rest of the line) using shell
> +d, drop <commit> = remove commit
> +l, label <label>= label current HEAD with a name
> +t, reset <label> = reset HEAD to a label
> +b, bud = reset HEAD to the revision labeled 'onto', no arguments
> +m, merge [<label-or-commit>]* = create a merge commit using a given commit's message

Good idea! I would rather do it as an introductory patch (that only
converts the existing list).

As to `merge`: it is a bit more complicated ;-)

	m, merge <original-merge-commit> ( <label> | "<label>..." ) [<oneline>]
		create a merge commit using the original merge commit's
		message (or the oneline, if "-" is given). Use a quoted
		list of commits to be merged for octopus merges.

Thanks,
Dscho

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 4/8] rebase-helper --make-script: introduce a flag to recreate merges
  2018-01-18 15:35 ` [PATCH 4/8] rebase-helper --make-script: introduce a flag to recreate merges Johannes Schindelin
@ 2018-01-18 21:39   ` Philip Oakley
  2018-01-19 10:34   ` Eric Sunshine
  2018-01-23 20:03   ` Junio C Hamano
  2 siblings, 0 replies; 276+ messages in thread
From: Philip Oakley @ 2018-01-18 21:39 UTC (permalink / raw)
  To: Johannes Schindelin, git; +Cc: Junio C Hamano, Jacob Keller

From: "Johannes Schindelin" <johannes.schindelin@gmx.de>
> The sequencer just learned a new commands intended to recreate branch
> structure (similar in spirit to --preserve-merges, but with a
> substantially less-broken design).
>
> Let's allow the rebase--helper to generate todo lists making use of
> these commands, triggered by the new --recreate-merges option. For a
> commit topology like this:
>
> A - B - C
>   \   /
>     D

Could the topology include the predecessor for context. Alo it is easy for 
readers to become confused between the arcs of the graphs and the nodes of 
the graphs, such that we confuse 'commits as patches' with 'commits as 
snapshots'. It might need an 'Aa' distinction between the two types, 
especially around merges and potential evilness.

>
> the generated todo list would look like this:
>
> # branch D
> pick 0123 A
> label branch-point
> pick 1234 D
> label D
>
> reset branch-point
> pick 2345 B
> merge 3456 D C
>
> To keep things simple, we first only implement support for merge commits
> with exactly two parents, leaving support for octopus merges to a later
> patch in this patch series.
>
> Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
> ---
> builtin/rebase--helper.c |   4 +-
> sequencer.c              | 343 
> ++++++++++++++++++++++++++++++++++++++++++++++-
> sequencer.h              |   1 +
> 3 files changed, 345 insertions(+), 3 deletions(-)
>
> diff --git a/builtin/rebase--helper.c b/builtin/rebase--helper.c
> index 7daee544b7b..a34ab5c0655 100644
> --- a/builtin/rebase--helper.c
> +++ b/builtin/rebase--helper.c
> @@ -12,7 +12,7 @@ static const char * const builtin_rebase_helper_usage[] 
> = {
> int cmd_rebase__helper(int argc, const char **argv, const char *prefix)
> {
>  struct replay_opts opts = REPLAY_OPTS_INIT;
> - unsigned flags = 0, keep_empty = 0;
> + unsigned flags = 0, keep_empty = 0, recreate_merges = 0;
>  int abbreviate_commands = 0;
>  enum {
>  CONTINUE = 1, ABORT, MAKE_SCRIPT, SHORTEN_OIDS, EXPAND_OIDS,
> @@ -22,6 +22,7 @@ int cmd_rebase__helper(int argc, const char **argv, 
> const char *prefix)
>  struct option options[] = {
>  OPT_BOOL(0, "ff", &opts.allow_ff, N_("allow fast-forward")),
>  OPT_BOOL(0, "keep-empty", &keep_empty, N_("keep empty commits")),
> + OPT_BOOL(0, "recreate-merges", &recreate_merges, N_("recreate merge 
> commits")),
>  OPT_CMDMODE(0, "continue", &command, N_("continue rebase"),
>  CONTINUE),
>  OPT_CMDMODE(0, "abort", &command, N_("abort rebase"),
> @@ -55,6 +56,7 @@ int cmd_rebase__helper(int argc, const char **argv, 
> const char *prefix)
>
>  flags |= keep_empty ? TODO_LIST_KEEP_EMPTY : 0;
>  flags |= abbreviate_commands ? TODO_LIST_ABBREVIATE_CMDS : 0;
> + flags |= recreate_merges ? TODO_LIST_RECREATE_MERGES : 0;
>  flags |= command == SHORTEN_OIDS ? TODO_LIST_SHORTEN_IDS : 0;
>
>  if (command == CONTINUE && argc == 1)
> diff --git a/sequencer.c b/sequencer.c
> index a96255426e7..1bef16647b4 100644
> --- a/sequencer.c
> +++ b/sequencer.c
> @@ -23,6 +23,8 @@
> #include "hashmap.h"
> #include "unpack-trees.h"
> #include "worktree.h"
> +#include "oidmap.h"
> +#include "oidset.h"
>
> #define GIT_REFLOG_ACTION "GIT_REFLOG_ACTION"
>
> @@ -2785,6 +2787,335 @@ void append_signoff(struct strbuf *msgbuf, int 
> ignore_footer, unsigned flag)
>  strbuf_release(&sob);
> }
>
> +struct labels_entry {
> + struct hashmap_entry entry;
> + char label[FLEX_ARRAY];
> +};
> +
> +static int labels_cmp(const void *fndata, const struct labels_entry *a,
> +       const struct labels_entry *b, const void *key)
> +{
> + return key ? strcmp(a->label, key) : strcmp(a->label, b->label);
> +}
> +
> +struct string_entry {
> + struct oidmap_entry entry;
> + char string[FLEX_ARRAY];
> +};
> +
> +struct label_state {
> + struct oidmap commit2label;
> + struct hashmap labels;
> + struct strbuf buf;
> +};
> +
> +static const char *label_oid(struct object_id *oid, const char *label,
> +      struct label_state *state)
> +{
> + struct labels_entry *labels_entry;
> + struct string_entry *string_entry;
> + struct object_id dummy;
> + size_t len;
> + int i;
> +
> + string_entry = oidmap_get(&state->commit2label, oid);
> + if (string_entry)
> + return string_entry->string;
> +
> + /*
> + * For "uninteresting" commits, i.e. commits that are not to be
> + * rebased, and which can therefore not be labeled, we use a unique
> + * abbreviation of the commit name. This is slightly more complicated
> + * than calling find_unique_abbrev() because we also need to make
> + * sure that the abbreviation does not conflict with any other
> + * label.
> + *
> + * We disallow "interesting" commits to be labeled by a string that
> + * is a valid full-length hash, to ensure that we always can find an
> + * abbreviation for any uninteresting commit's names that does not
> + * clash with any other label.
> + */
> + if (!label) {
> + char *p;
> +
> + strbuf_reset(&state->buf);
> + strbuf_grow(&state->buf, GIT_SHA1_HEXSZ);
> + label = p = state->buf.buf;
> +
> + find_unique_abbrev_r(p, oid->hash, default_abbrev);
> +
> + /*
> + * We may need to extend the abbreviated hash so that there is
> + * no conflicting label.
> + */
> + if (hashmap_get_from_hash(&state->labels, strihash(p), p)) {
> + size_t i = strlen(p) + 1;
> +
> + oid_to_hex_r(p, oid);
> + for (; i < GIT_SHA1_HEXSZ; i++) {
> + char save = p[i];
> + p[i] = '\0';
> + if (!hashmap_get_from_hash(&state->labels,
> +    strihash(p), p))
> + break;
> + p[i] = save;
> + }
> + }
> + } else if (((len = strlen(label)) == GIT_SHA1_RAWSZ &&
> +     !get_oid_hex(label, &dummy)) ||
> +    hashmap_get_from_hash(&state->labels,
> + strihash(label), label)) {
> + /*
> + * If the label already exists, or if the label is a valid full
> + * OID, we append a dash and a number to make it unique.
> + */
> + struct strbuf *buf = &state->buf;
> +
> + strbuf_reset(buf);
> + strbuf_add(buf, label, len);
> +
> + for (i = 2; ; i++) {
> + strbuf_setlen(buf, len);
> + strbuf_addf(buf, "-%d", i);
> + if (!hashmap_get_from_hash(&state->labels,
> +    strihash(buf->buf),
> +    buf->buf))
> + break;
> + }
> +
> + label = buf->buf;
> + }
> +
> + FLEX_ALLOC_STR(labels_entry, label, label);
> + hashmap_entry_init(labels_entry, strihash(label));
> + hashmap_add(&state->labels, labels_entry);
> +
> + FLEX_ALLOC_STR(string_entry, string, label);
> + oidcpy(&string_entry->entry.oid, oid);
> + oidmap_put(&state->commit2label, string_entry);
> +
> + return string_entry->string;
> +}
> +
> +static int make_script_with_merges(struct pretty_print_context *pp,
> +    struct rev_info *revs, FILE *out,
> +    unsigned flags)
> +{
> + int keep_empty = flags & TODO_LIST_KEEP_EMPTY;
> + struct strbuf buf = STRBUF_INIT, oneline = STRBUF_INIT;
> + struct strbuf label = STRBUF_INIT;
> + struct commit_list *commits = NULL, **tail = &commits, *iter;
> + struct commit_list *tips = NULL, **tips_tail = &tips;
> + struct commit *commit;
> + struct oidmap commit2todo = OIDMAP_INIT;
> + struct string_entry *entry;
> + struct oidset interesting = OIDSET_INIT, child_seen = OIDSET_INIT,
> + shown = OIDSET_INIT;
> + struct label_state state = { OIDMAP_INIT, { NULL }, STRBUF_INIT };
> +
> + int abbr = flags & TODO_LIST_ABBREVIATE_CMDS;
> + const char *p = abbr ? "p" : "pick", *l = abbr ? "l" : "label",
> + *t = abbr ? "t" : "reset", *b = abbr ? "b" : "bud",
> + *m = abbr ? "m" : "merge";
> +
> + oidmap_init(&commit2todo, 0);
> + oidmap_init(&state.commit2label, 0);
> + hashmap_init(&state.labels, (hashmap_cmp_fn) labels_cmp, NULL, 0);
> + strbuf_init(&state.buf, 32);
> +
> + if (revs->cmdline.nr && (revs->cmdline.rev[0].flags & BOTTOM)) {
> + struct object_id *oid = &revs->cmdline.rev[0].item->oid;
> + FLEX_ALLOC_STR(entry, string, "onto");

"onto" Needs documentation / commit message?


> + oidcpy(&entry->entry.oid, oid);
> + oidmap_put(&state.commit2label, entry);
> + }
> +
> + /*
> + * First phase:
> + * - get onelines for all commits
> + * - gather all branch tips (i.e. 2nd or later parents of merges)
> + * - label all branch tips
> + */
> + while ((commit = get_revision(revs))) {
> + struct commit_list *to_merge;
> + int is_octopus;
> + const char *p1, *p2;
> + struct object_id *oid;
> +
> + tail = &commit_list_insert(commit, tail)->next;
> + oidset_insert(&interesting, &commit->object.oid);
> +
> + if ((commit->object.flags & PATCHSAME))
> + continue;
> +
> + strbuf_reset(&oneline);
> + pretty_print_commit(pp, commit, &oneline);
> +
> + to_merge = commit->parents ? commit->parents->next : NULL;
> + if (!to_merge) {
> + /* non-merge commit: easy case */
> + strbuf_reset(&buf);
> + if (!keep_empty && is_original_commit_empty(commit))
> + strbuf_addf(&buf, "%c ", comment_line_char);
> + strbuf_addf(&buf, "%s %s %s", p,
> +     oid_to_hex(&commit->object.oid),
> +     oneline.buf);
> +
> + FLEX_ALLOC_STR(entry, string, buf.buf);
> + oidcpy(&entry->entry.oid, &commit->object.oid);
> + oidmap_put(&commit2todo, entry);
> +
> + continue;
> + }
> +
> + is_octopus = to_merge && to_merge->next;
> +
> + if (is_octopus)
> + BUG("Octopus merges not yet supported");
> +
> + /* Create a label */
> + strbuf_reset(&label);
> + if (skip_prefix(oneline.buf, "Merge ", &p1) &&
> +     (p1 = strchr(p1, '\'')) &&
> +     (p2 = strchr(++p1, '\'')))
> + strbuf_add(&label, p1, p2 - p1);
> + else if (skip_prefix(oneline.buf, "Merge pull request ",
> +      &p1) &&
> + (p1 = strstr(p1, " from ")))
> + strbuf_addstr(&label, p1 + strlen(" from "));
> + else
> + strbuf_addbuf(&label, &oneline);
> +
> + for (p1 = label.buf; *p1; p1++)
> + if (isspace(*p1))
> + *(char *)p1 = '-';
> +
> + strbuf_reset(&buf);
> + strbuf_addf(&buf, "%s %s", m, oid_to_hex(&commit->object.oid));
> +
> + /* label the tip of merged branch */
> + oid = &to_merge->item->object.oid;
> + strbuf_addch(&buf, ' ');
> +
> + if (!oidset_contains(&interesting, oid))
> + strbuf_addstr(&buf, label_oid(oid, NULL, &state));
> + else {
> + tips_tail = &commit_list_insert(to_merge->item,
> + tips_tail)->next;
> +
> + strbuf_addstr(&buf, label_oid(oid, label.buf, &state));
> + }
> + strbuf_addf(&buf, " %s", oneline.buf);
> +
> + FLEX_ALLOC_STR(entry, string, buf.buf);
> + oidcpy(&entry->entry.oid, &commit->object.oid);
> + oidmap_put(&commit2todo, entry);
> + }
> +
> + /*
> + * Second phase:
> + * - label branch points
> + * - add HEAD to the branch tips
> + */
> + for (iter = commits; iter; iter = iter->next) {
> + struct commit_list *parent = iter->item->parents;
> + for (; parent; parent = parent->next) {
> + struct object_id *oid = &parent->item->object.oid;
> + if (!oidset_contains(&interesting, oid))
> + continue;
> + if (!oidset_contains(&child_seen, oid))
> + oidset_insert(&child_seen, oid);
> + else
> + label_oid(oid, "branch-point", &state);
> + }
> +
> + /* Add HEAD as implict "tip of branch" */
> + if (!iter->next)
> + tips_tail = &commit_list_insert(iter->item,
> + tips_tail)->next;
> + }
> +
> + /*
> + * Third phase: output the todo list. This is a bit tricky, as we
> + * want to avoid jumping back and forth between revisions. To
> + * accomplish that goal, we walk backwards from the branch tips,
> + * gathering commits not yet shown, reversing the list on the fly,
> + * then outputting that list (labeling revisions as needed).
> + */
> + fprintf(out, "%s onto\n", l);
> + for (iter = tips; iter; iter = iter->next) {
> + struct commit_list *list = NULL, *iter2;
> +
> + commit = iter->item;
> + if (oidset_contains(&shown, &commit->object.oid))
> + continue;
> + entry = oidmap_get(&state.commit2label, &commit->object.oid);
> +
> + if (entry)
> + fprintf(out, "\n# Branch %s\n", entry->string);
> + else
> + fprintf(out, "\n");
> +
> + while (oidset_contains(&interesting, &commit->object.oid) &&
> +        !oidset_contains(&shown, &commit->object.oid)) {
> + commit_list_insert(commit, &list);
> + if (!commit->parents) {
> + commit = NULL;
> + break;
> + }
> + commit = commit->parents->item;
> + }
> +
> + if (!commit)
> + fprintf(out, "%s\n", b);
> + else {
> + const char *to = NULL;
> +
> + entry = oidmap_get(&state.commit2label,
> +    &commit->object.oid);
> + if (entry)
> + to = entry->string;
> +
> + if (!to || !strcmp("onto", to))
> + fprintf(out, "%s\n", b);
> + else {
> + strbuf_reset(&oneline);
> + pretty_print_commit(pp, commit, &oneline);
> + fprintf(out, "%s %s %s\n",
> + t, to, oneline.buf);
> + }
> + }
> +
> + for (iter2 = list; iter2; iter2 = iter2->next) {
> + struct object_id *oid = &iter2->item->object.oid;
> + entry = oidmap_get(&commit2todo, oid);
> + /* only show if not already upstream */
> + if (entry)
> + fprintf(out, "%s\n", entry->string);
> + entry = oidmap_get(&state.commit2label, oid);
> + if (entry)
> + fprintf(out, "%s %s\n", l, entry->string);
> + oidset_insert(&shown, oid);
> + }
> +
> + free_commit_list(list);
> + }
> +
> + free_commit_list(commits);
> + free_commit_list(tips);
> +
> + strbuf_release(&label);
> + strbuf_release(&oneline);
> + strbuf_release(&buf);
> +
> + oidmap_free(&commit2todo, 1);
> + oidmap_free(&state.commit2label, 1);
> + hashmap_free(&state.labels, 1);
> + strbuf_release(&state.buf);
> +
> + return 0;
> +}
> +
> int sequencer_make_script(FILE *out, int argc, const char **argv,
>    unsigned flags)
> {
> @@ -2795,11 +3126,16 @@ int sequencer_make_script(FILE *out, int argc, 
> const char **argv,
>  struct commit *commit;
>  int keep_empty = flags & TODO_LIST_KEEP_EMPTY;
>  const char *insn = flags & TODO_LIST_ABBREVIATE_CMDS ? "p" : "pick";
> + int recreate_merges = flags & TODO_LIST_RECREATE_MERGES;
>
>  init_revisions(&revs, NULL);
>  revs.verbose_header = 1;
> - revs.max_parents = 1;
> - revs.cherry_pick = 1;
> + if (recreate_merges)
> + revs.cherry_mark = 1;
> + else {
> + revs.max_parents = 1;
> + revs.cherry_pick = 1;
> + }
>  revs.limited = 1;
>  revs.reverse = 1;
>  revs.right_only = 1;
> @@ -2823,6 +3159,9 @@ int sequencer_make_script(FILE *out, int argc, const 
> char **argv,
>  if (prepare_revision_walk(&revs) < 0)
>  return error(_("make_script: error preparing revisions"));
>
> + if (recreate_merges)
> + return make_script_with_merges(&pp, &revs, out, flags);
> +
>  while ((commit = get_revision(&revs))) {
>  strbuf_reset(&buf);
>  if (!keep_empty && is_original_commit_empty(commit))
> diff --git a/sequencer.h b/sequencer.h
> index 81f6d7d393f..11d1ac925ef 100644
> --- a/sequencer.h
> +++ b/sequencer.h
> @@ -48,6 +48,7 @@ int sequencer_remove_state(struct replay_opts *opts);
> #define TODO_LIST_KEEP_EMPTY (1U << 0)
> #define TODO_LIST_SHORTEN_IDS (1U << 1)
> #define TODO_LIST_ABBREVIATE_CMDS (1U << 2)
> +#define TODO_LIST_RECREATE_MERGES (1U << 3)
> int sequencer_make_script(FILE *out, int argc, const char **argv,
>    unsigned flags);
>
> -- 
> 2.15.1.windows.2.1430.ga56c4f9e2a9
>
> 


^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 9/8] [DO NOT APPLY, but squash?] git-rebase--interactive: clarify arguments
  2018-01-18 21:36     ` Johannes Schindelin
@ 2018-01-18 21:58       ` Stefan Beller
  2018-01-19 20:30       ` Junio C Hamano
  1 sibling, 0 replies; 276+ messages in thread
From: Stefan Beller @ 2018-01-18 21:58 UTC (permalink / raw)
  To: Johannes Schindelin; +Cc: git, Junio C Hamano, Jacob Keller

On Thu, Jan 18, 2018 at 1:36 PM, Johannes Schindelin
<Johannes.Schindelin@gmx.de> wrote:

> Good idea! I would rather do it as an introductory patch (that only
> converts the existing list).

That makes sense.

>
> As to `merge`: it is a bit more complicated ;-)
>
>         m, merge <original-merge-commit> ( <label> | "<label>..." ) [<oneline>]
>                 create a merge commit using the original merge commit's
>                 message (or the oneline, if "-" is given). Use a quoted
>                 list of commits to be merged for octopus merges.

That makes sense. Thanks for being precise at the the user facing UX here.

Thanks,
Stefan

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 10/8] [DO NOT APPLY, but improve?] rebase--interactive: introduce "stop" command
  2018-01-18 18:36   ` [PATCH 10/8] [DO NOT APPLY, but improve?] rebase--interactive: introduce "stop" command Stefan Beller
  2018-01-18 21:20     ` Jacob Keller
@ 2018-01-18 22:00     ` Johannes Schindelin
  2018-01-18 22:09       ` Stefan Beller
  1 sibling, 1 reply; 276+ messages in thread
From: Johannes Schindelin @ 2018-01-18 22:00 UTC (permalink / raw)
  To: Stefan Beller; +Cc: git, gitster, jacob.keller

Hi Stefan,

On Thu, 18 Jan 2018, Stefan Beller wrote:

> Jake suggested using "x false" instead of "edit" for some corner cases.
> 
> I do prefer using "x false" for all kinds of things such as stopping
> before a commit (edit only let's you stop after a commit), and the
> knowledge that "x false" does the least amount of actions behind my back.
> 
> We should have that command as well, maybe?
> 
> Signed-off-by: Stefan Beller <sbeller@google.com>
> ---
>  git-rebase--interactive.sh |  1 +
>  sequencer.c                | 10 ++++++++++
>  2 files changed, 11 insertions(+)
> 
> diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh
> index 3cd7446d0b..9eac53f0c5 100644
> --- a/git-rebase--interactive.sh
> +++ b/git-rebase--interactive.sh
> @@ -166,6 +166,7 @@ l, label <label>= label current HEAD with a name
>  t, reset <label> = reset HEAD to a label
>  b, bud = reset HEAD to the revision labeled 'onto', no arguments
>  m, merge [<label-or-commit>]* = create a merge commit using a given commit's message
> +y, stay = stop for  shortcut for
>  
>  These lines can be re-ordered; they are executed from top to bottom.
>  " | git stripspace --comment-lines >>"$todo"
> diff --git a/sequencer.c b/sequencer.c
> index 2b4e6b1232..4b3b9fe59d 100644
> --- a/sequencer.c
> +++ b/sequencer.c
> @@ -782,6 +782,7 @@ enum todo_command {
>  	TODO_RESET,
>  	TODO_BUD,
>  	TODO_MERGE,
> +	TODO_STOP,

I see that your original idea was "stop", but then you probably realized
that there would be no good abbreviation for that, and changed your mind.

Personally, I would have called it `break`...

> @@ -2407,6 +2415,8 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts)
>  				/* `current` will be incremented below */
>  				todo_list->current = -1;
>  			}
> +		} else if (item->command == TODO_STOP) {
> +			todo_list->current = -1;

That is incorrect, it will most likely write an unexpected `done` file.

Did you mean `return 0` instead?

Ciao,
Dscho

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 8/8] rebase -i: introduce --recreate-merges=no-rebase-cousins
  2018-01-18 15:36 ` [PATCH 8/8] rebase -i: introduce --recreate-merges=no-rebase-cousins Johannes Schindelin
@ 2018-01-18 22:00   ` Philip Oakley
  2018-01-29 20:42     ` Johannes Schindelin
  2018-01-20  1:09   ` Eric Sunshine
  1 sibling, 1 reply; 276+ messages in thread
From: Philip Oakley @ 2018-01-18 22:00 UTC (permalink / raw)
  To: Johannes Schindelin, git; +Cc: Junio C Hamano, Jacob Keller

From: "Johannes Schindelin" <johannes.schindelin@gmx.de>
> This one is a bit tricky to explain, so let's try with a diagram:
>
>        C
>      /   \
> A - B - E - F
>  \   /
>    D
>
> To illustrate what this new mode is all about, let's consider what
> happens upon `git rebase -i --recreate-merges B`, in particular to
> the commit `D`. In the default mode, the new branch structure is:
>
>       --- C' --
>      /         \
> A - B ------ E' - F'
>      \    /
>        D'
>
> This is not really preserving the branch topology from before! The
> reason is that the commit `D` does not have `B` as ancestor, and
> therefore it gets rebased onto `B`.
>
> However, when recreating branch structure, there are legitimate use
> cases where one might want to preserve the branch points of commits that
> do not descend from the <upstream> commit that was passed to the rebase
> command, e.g. when a branch from core Git's `next` was merged into Git
> for Windows' master we will not want to rebase those commits on top of a
> Windows-specific commit. In the example above, the desired outcome would
> look like this:
>
>       --- C' --
>      /         \
> A - B ------ E' - F'
>  \        /
>   -- D' --

I'm not understanding this. I see that D properly starts from A, but don't 
see why it is now D'. Surely it's unchanged.
Maybe it's the arc/node confusion. Maybe even spell out that the rebased 
commits from the command are B..HEAD, but that includes D, which may not be 
what folk had expected. (not even sure if the reflog comes into determining 
merge-bases here..)

I do think an exact definition is needed (e.g. via --ancestry-path or its 
equivalent?).

>
> Let's introduce the term "cousins" for such commits ("D" in the
> example), and the "no-rebase-cousins" mode of the merge-recreating
> rebase, to help those use cases.
>
> Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
> ---
> Documentation/git-rebase.txt      |  7 ++++++-
> builtin/rebase--helper.c          |  9 ++++++++-
> git-rebase--interactive.sh        |  1 +
> git-rebase.sh                     | 12 +++++++++++-
> sequencer.c                       |  4 ++++
> sequencer.h                       |  8 ++++++++
> t/t3430-rebase-recreate-merges.sh | 23 +++++++++++++++++++++++
> 7 files changed, 61 insertions(+), 3 deletions(-)
>
> diff --git a/Documentation/git-rebase.txt b/Documentation/git-rebase.txt
> index 1d061373288..ac07a5c3fc9 100644
> --- a/Documentation/git-rebase.txt
> +++ b/Documentation/git-rebase.txt
> @@ -368,10 +368,15 @@ The commit list format can be changed by setting the 
> configuration option
> rebase.instructionFormat.  A customized instruction format will 
> automatically
> have the long commit hash prepended to the format.
>
> ---recreate-merges::
> +--recreate-merges[=(rebase-cousins|no-rebase-cousins)]::
>  Recreate merge commits instead of flattening the history by replaying
>  merges. Merge conflict resolutions or manual amendments to merge
>  commits are not preserved.
> ++
> +By default, or when `rebase-cousins` was specified, commits which do not 
> have
> +`<upstream>` as direct ancestor are rebased onto `<upstream>` (or 
> `<onto>`,
> +if specified). If the `rebase-cousins` mode is turned off, such commits 
> will
> +retain their original branch point.
>
> -p::
> --preserve-merges::
> diff --git a/builtin/rebase--helper.c b/builtin/rebase--helper.c
> index a34ab5c0655..ef08fef4d14 100644
> --- a/builtin/rebase--helper.c
> +++ b/builtin/rebase--helper.c
> @@ -13,7 +13,7 @@ int cmd_rebase__helper(int argc, const char **argv, 
> const char *prefix)
> {
>  struct replay_opts opts = REPLAY_OPTS_INIT;
>  unsigned flags = 0, keep_empty = 0, recreate_merges = 0;
> - int abbreviate_commands = 0;
> + int abbreviate_commands = 0, no_rebase_cousins = -1;
>  enum {
>  CONTINUE = 1, ABORT, MAKE_SCRIPT, SHORTEN_OIDS, EXPAND_OIDS,
>  CHECK_TODO_LIST, SKIP_UNNECESSARY_PICKS, REARRANGE_SQUASH,
> @@ -23,6 +23,8 @@ int cmd_rebase__helper(int argc, const char **argv, 
> const char *prefix)
>  OPT_BOOL(0, "ff", &opts.allow_ff, N_("allow fast-forward")),
>  OPT_BOOL(0, "keep-empty", &keep_empty, N_("keep empty commits")),
>  OPT_BOOL(0, "recreate-merges", &recreate_merges, N_("recreate merge 
> commits")),
> + OPT_BOOL(0, "no-rebase-cousins", &no_rebase_cousins,
> + N_("keep original branch points of cousins")),
>  OPT_CMDMODE(0, "continue", &command, N_("continue rebase"),
>  CONTINUE),
>  OPT_CMDMODE(0, "abort", &command, N_("abort rebase"),
> @@ -57,8 +59,13 @@ int cmd_rebase__helper(int argc, const char **argv, 
> const char *prefix)
>  flags |= keep_empty ? TODO_LIST_KEEP_EMPTY : 0;
>  flags |= abbreviate_commands ? TODO_LIST_ABBREVIATE_CMDS : 0;
>  flags |= recreate_merges ? TODO_LIST_RECREATE_MERGES : 0;
> + flags |= no_rebase_cousins > 0 ? TODO_LIST_NO_REBASE_COUSINS : 0;
>  flags |= command == SHORTEN_OIDS ? TODO_LIST_SHORTEN_IDS : 0;
>
> + if (no_rebase_cousins >= 0&& !recreate_merges)
> + warning(_("--[no-]rebase-cousins has no effect without "
> +   "--recreate-merges"));
> +
>  if (command == CONTINUE && argc == 1)
>  return !!sequencer_continue(&opts);
>  if (command == ABORT && argc == 1)
> diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh
> index 3459ec5a018..23184c77e88 100644
> --- a/git-rebase--interactive.sh
> +++ b/git-rebase--interactive.sh
> @@ -901,6 +901,7 @@ if test t != "$preserve_merges"
> then
>  git rebase--helper --make-script ${keep_empty:+--keep-empty} \
>  ${recreate_merges:+--recreate-merges} \
> + ${no_rebase_cousins:+--no-rebase-cousins} \
>  $revisions ${restrict_revision+^$restrict_revision} >"$todo" ||
>  die "$(gettext "Could not generate todo list")"
> else
> diff --git a/git-rebase.sh b/git-rebase.sh
> index d69bc7d0e0d..3403b1416a8 100755
> --- a/git-rebase.sh
> +++ b/git-rebase.sh
> @@ -17,7 +17,7 @@ q,quiet!           be quiet. implies --no-stat
> autostash          automatically stash/stash pop before and after
> fork-point         use 'merge-base --fork-point' to refine upstream
> onto=!             rebase onto given branch instead of upstream
> -recreate-merges!   try to recreate merges instead of skipping them
> +recreate-merges?   try to recreate merges instead of skipping them
> p,preserve-merges! try to recreate merges instead of ignoring them
> s,strategy=!       use the given merge strategy
> no-ff!             cherry-pick all commits, even if unchanged
> @@ -88,6 +88,7 @@ state_dir=
> # One of {'', continue, skip, abort}, as parsed from command line
> action=
> recreate_merges=
> +no_rebase_cousins=
> preserve_merges=
> autosquash=
> keep_empty=
> @@ -268,6 +269,15 @@ do
>  recreate_merges=t
>  test -z "$interactive_rebase" && interactive_rebase=implied
>  ;;
> + --recreate-merges=*)
> + recreate_merges=t
> + case "${1#*=}" in
> + rebase-cousins) no_rebase_cousins=;;
> + no-rebase-cousins) no_rebase_cousins=t;;
> + *) die "Unknown mode: $1";;
> + esac
> + test -z "$interactive_rebase" && interactive_rebase=implied
> + ;;
>  --preserve-merges)
>  preserve_merges=t
>  test -z "$interactive_rebase" && interactive_rebase=implied
> diff --git a/sequencer.c b/sequencer.c
> index b63bfb9a141..2b4e6b12321 100644
> --- a/sequencer.c
> +++ b/sequencer.c
> @@ -2905,6 +2905,7 @@ static int make_script_with_merges(struct 
> pretty_print_context *pp,
>     unsigned flags)
> {
>  int keep_empty = flags & TODO_LIST_KEEP_EMPTY;
> + int no_rebase_cousins = flags & TODO_LIST_NO_REBASE_COUSINS;
>  struct strbuf buf = STRBUF_INIT, oneline = STRBUF_INIT;
>  struct strbuf label = STRBUF_INIT;
>  struct commit_list *commits = NULL, **tail = &commits, *iter;
> @@ -3078,6 +3079,9 @@ static int make_script_with_merges(struct 
> pretty_print_context *pp,
>     &commit->object.oid);
>  if (entry)
>  to = entry->string;
> + else if (no_rebase_cousins)
> + to = label_oid(&commit->object.oid, NULL,
> +        &state);
>
>  if (!to || !strcmp("onto", to))
>  fprintf(out, "%s\n", b);
> diff --git a/sequencer.h b/sequencer.h
> index 11d1ac925ef..9530dba3cba 100644
> --- a/sequencer.h
> +++ b/sequencer.h
> @@ -49,6 +49,14 @@ int sequencer_remove_state(struct replay_opts *opts);
> #define TODO_LIST_SHORTEN_IDS (1U << 1)
> #define TODO_LIST_ABBREVIATE_CMDS (1U << 2)
> #define TODO_LIST_RECREATE_MERGES (1U << 3)
> +/*
> + * When recreating merges, commits that do have the base commit as 
> ancestor
> + * ("cousins") are rebased onto the new base by default. If those commits
> + * should keep their original branch point, this flag needs to be passed.
> + *
> + * This flag only makes sense when <base> and <onto> are different.
> + */
> +#define TODO_LIST_NO_REBASE_COUSINS (1U << 4)
> int sequencer_make_script(FILE *out, int argc, const char **argv,
>    unsigned flags);
>
> diff --git a/t/t3430-rebase-recreate-merges.sh 
> b/t/t3430-rebase-recreate-merges.sh
> index 76e615bd7c1..22930e470a4 100755
> --- a/t/t3430-rebase-recreate-merges.sh
> +++ b/t/t3430-rebase-recreate-merges.sh
> @@ -143,6 +143,29 @@ test_expect_success 'with a branch tip that was 
> cherry-picked already' '
>  EOF
> '
>
> +test_expect_success 'rebase cousins unless told not to' '
> + write_script copy-editor.sh <<-\EOF &&
> + cp "$1" "$(git rev-parse --git-path ORIGINAL-TODO)"
> + EOF
> +
> + test_config sequence.editor \""$PWD"/copy-editor.sh\" &&
> + git checkout -b cousins master &&
> + before="$(git rev-parse --verify HEAD)" &&
> + test_tick &&
> + git rebase -i --recreate-merges=no-rebase-cousins HEAD^ &&
> + test_cmp_rev HEAD $before &&
> + test_tick &&
> + git rebase -i --recreate-merges HEAD^ &&
> + test_cmp_graph HEAD^.. <<-\EOF
> + *   Merge the topic branch '\''onebranch'\''
> + |\
> + | * D
> + | * G
> + |/
> + o H
> + EOF
> +'
> +
> test_expect_success 'refs/rewritten/* is worktree-local' '
>  git worktree add wt &&
>  cat >wt/script-from-scratch <<-\EOF &&
> -- 
> 2.15.1.windows.2.1430.ga56c4f9e2a9 


^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 10/8] [DO NOT APPLY, but improve?] rebase--interactive: introduce "stop" command
  2018-01-18 21:20     ` Jacob Keller
@ 2018-01-18 22:08       ` Philip Oakley
  2018-01-18 22:09         ` Jacob Keller
  0 siblings, 1 reply; 276+ messages in thread
From: Philip Oakley @ 2018-01-18 22:08 UTC (permalink / raw)
  To: Jacob Keller, Stefan Beller; +Cc: Johannes Schindelin, Git mailing list, Junio C Hamano

From: "Jacob Keller" <jacob.keller@gmail.com>
> On Thu, Jan 18, 2018 at 10:36 AM, Stefan Beller <sbeller@google.com> 
> wrote:
>> Jake suggested using "x false" instead of "edit" for some corner cases.
>>
>> I do prefer using "x false" for all kinds of things such as stopping
>> before a commit (edit only let's you stop after a commit), and the
>> knowledge that "x false" does the least amount of actions behind my back.
>>
>> We should have that command as well, maybe?
>>
>
>
> I agree. I use "x false" very often, and I think stop is probably a
> better solution since it avoids spawning an extra shell that will just
> fail. Not sure if stop implies too much about "stop the whole thing"
> as opposed to "stop here and let me do something manual", but I think
> it's clear enough.
>
'hold' or 'pause' maybe options (leads to 
http://www.thesaurus.com/browse/put+on+hold offering procastinate etc.)
'adjourn'.

>
>> Signed-off-by: Stefan Beller <sbeller@google.com>
>> ---
>>  git-rebase--interactive.sh |  1 +
>>  sequencer.c                | 10 ++++++++++
>>  2 files changed, 11 insertions(+)
>>
>> diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh
>> index 3cd7446d0b..9eac53f0c5 100644
>> --- a/git-rebase--interactive.sh
>> +++ b/git-rebase--interactive.sh
>> @@ -166,6 +166,7 @@ l, label <label>= label current HEAD with a name
>>  t, reset <label> = reset HEAD to a label
>>  b, bud = reset HEAD to the revision labeled 'onto', no arguments
>>  m, merge [<label-or-commit>]* = create a merge commit using a given 
>> commit's message
>> +y, stay = stop for  shortcut for
>>
>>  These lines can be re-ordered; they are executed from top to bottom.
>>  " | git stripspace --comment-lines >>"$todo"
>> diff --git a/sequencer.c b/sequencer.c
>> index 2b4e6b1232..4b3b9fe59d 100644
>> --- a/sequencer.c
>> +++ b/sequencer.c
>> @@ -782,6 +782,7 @@ enum todo_command {
>>         TODO_RESET,
>>         TODO_BUD,
>>         TODO_MERGE,
>> +       TODO_STOP,
>>         /* commands that do nothing but are counted for reporting 
>> progress */
>>         TODO_NOOP,
>>         TODO_DROP,
>> @@ -803,6 +804,7 @@ static struct {
>>         { 'l', "label" },
>>         { 't', "reset" },
>>         { 'b', "bud" },
>> +       { 'y', "stay" },
>>         { 'm', "merge" },
>>         { 0,   "noop" },
>>         { 'd', "drop" },
>> @@ -1307,6 +1309,12 @@ static int parse_insn_line(struct todo_item *item, 
>> const char *bol, char *eol)
>>                 return 0;
>>         }
>>
>> +       if (item->command == TODO_STOP) {
>> +               item->commit = NULL;
>> +               item->arg = "";
>> +               item->arg_len = 0;
>> +       }
>> +
>>         end_of_object_name = (char *) bol + strcspn(bol, " \t\n");
>>         item->arg = end_of_object_name + strspn(end_of_object_name, " 
>> \t");
>>         item->arg_len = (int)(eol - item->arg);
>> @@ -2407,6 +2415,8 @@ static int pick_commits(struct todo_list 
>> *todo_list, struct replay_opts *opts)
>>                                 /* `current` will be incremented below */
>>                                 todo_list->current = -1;
>>                         }
>> +               } else if (item->command == TODO_STOP) {
>> +                       todo_list->current = -1;
>>                 } else if (item->command == TODO_LABEL)
>>                         res = do_label(item->arg, item->arg_len);
>>                 else if (item->command == TODO_RESET)
>> --
>> 2.16.0.rc1.238.g530d649a79-goog
>> 


^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 10/8] [DO NOT APPLY, but improve?] rebase--interactive: introduce "stop" command
  2018-01-18 22:00     ` Johannes Schindelin
@ 2018-01-18 22:09       ` Stefan Beller
  0 siblings, 0 replies; 276+ messages in thread
From: Stefan Beller @ 2018-01-18 22:09 UTC (permalink / raw)
  To: Johannes Schindelin; +Cc: git, Junio C Hamano, Jacob Keller

On Thu, Jan 18, 2018 at 2:00 PM, Johannes Schindelin
<Johannes.Schindelin@gmx.de> wrote:

>> +     TODO_STOP,
>
> I see that your original idea was "stop", but then you probably realized
> that there would be no good abbreviation for that, and changed your mind.
>
> Personally, I would have called it `break`...

I was looking at a synonym list of stop to find a word that contained a letter
which was not already taken. 'break' would allow for 'a', or 'k', assuming 'bud'
takes 'b' (or can that go to 'u'? Are there people out there with muscle memory
on these letters already?)

Any word (of stop, break, stay, control) sounds good to me, though 'break' might
be the clearest.

>
>> @@ -2407,6 +2415,8 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts)
>>                               /* `current` will be incremented below */
>>                               todo_list->current = -1;
>>                       }
>> +             } else if (item->command == TODO_STOP) {
>> +                     todo_list->current = -1;
>
> That is incorrect, it will most likely write an unexpected `done` file.
>
> Did you mean `return 0` instead?

I guess. I did not compile or test the patch, I was merely writing down enough
to convey the idea, hopefully.

While talking about this idea of exploding the number of keywords,
maybe we can also have 'abort', which does the same as deleting all lines
(every time I want to abort I still get shivers if I just drop all
patches instead
of aborting, so maybe typing 'abort-and-restore' as the first thing in the file
would convey a safer feeling to users?)

Thanks for taking these additional considerations into mind while I don't
review the actual patches,

Stefan

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 10/8] [DO NOT APPLY, but improve?] rebase--interactive: introduce "stop" command
  2018-01-18 22:08       ` Philip Oakley
@ 2018-01-18 22:09         ` Jacob Keller
  0 siblings, 0 replies; 276+ messages in thread
From: Jacob Keller @ 2018-01-18 22:09 UTC (permalink / raw)
  To: Philip Oakley; +Cc: Stefan Beller, Johannes Schindelin, Git mailing list, Junio C Hamano

On Thu, Jan 18, 2018 at 2:08 PM, Philip Oakley <philipoakley@iee.org> wrote:
> From: "Jacob Keller" <jacob.keller@gmail.com>
>>
>> On Thu, Jan 18, 2018 at 10:36 AM, Stefan Beller <sbeller@google.com>
>> wrote:
>>>
>>> Jake suggested using "x false" instead of "edit" for some corner cases.
>>>
>>> I do prefer using "x false" for all kinds of things such as stopping
>>> before a commit (edit only let's you stop after a commit), and the
>>> knowledge that "x false" does the least amount of actions behind my back.
>>>
>>> We should have that command as well, maybe?
>>>
>>
>>
>> I agree. I use "x false" very often, and I think stop is probably a
>> better solution since it avoids spawning an extra shell that will just
>> fail. Not sure if stop implies too much about "stop the whole thing"
>> as opposed to "stop here and let me do something manual", but I think
>> it's clear enough.
>>
> 'hold' or 'pause' maybe options (leads to
> http://www.thesaurus.com/browse/put+on+hold offering procastinate etc.)
> 'adjourn'.
>
>

I like break, as suggested by Dscho. That also works well for
abbreviation if we drop the "bud" command.

Thanks,
Jake

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 1/8] sequencer: introduce new commands to reset the revision
  2018-01-18 15:35 ` [PATCH 1/8] sequencer: introduce new commands to reset the revision Johannes Schindelin
  2018-01-18 16:25   ` Jacob Keller
@ 2018-01-19  8:59   ` Eric Sunshine
  2018-01-24 22:01     ` Junio C Hamano
  2018-01-29 20:50     ` Johannes Schindelin
  2018-01-19 12:24   ` [PATCH 1/8] sequencer: introduce new commands to resettherevision Phillip Wood
  2 siblings, 2 replies; 276+ messages in thread
From: Eric Sunshine @ 2018-01-19  8:59 UTC (permalink / raw)
  To: Johannes Schindelin; +Cc: Git List, Junio C Hamano, Jacob Keller

On Thu, Jan 18, 2018 at 10:35 AM, Johannes Schindelin
<johannes.schindelin@gmx.de> wrote:
> [...]
> This commit implements the commands to label, and to reset to, given
> revisions. The syntax is:
>
>         label <name>
>         reset <name>
> [...]
>
> Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
> ---
> diff --git a/sequencer.c b/sequencer.c
> @@ -1919,6 +1936,139 @@ static int do_exec(const char *command_line)
> +static int safe_append(const char *filename, const char *fmt, ...)
> +{
> +       [...]
> +       if (commit_lock_file(&lock) < 0) {
> +               rollback_lock_file(&lock);
> +               return error(_("failed to finalize '%s'."), filename);

s/\.//

> +       }
> +
> +       return 0;
> +}
> +
> +static int do_reset(const char *name, int len)
> +{
> +       [...]
> +       if (hold_locked_index(&lock, LOCK_REPORT_ON_ERROR) < 0)
> +               return -1;
> +
> +       for (i = 0; i < len; i++)
> +               if (isspace(name[i]))
> +                       len = i;

What is the purpose of this loop? I could imagine that it's trying to
strip all whitespace from the end of 'name', however, to do that it
would iterate backward, not forward. (Or perhaps it's trying to
truncate at the first space, but then it would need to invert the
condition or use 'break'.) Am I missing something obvious?

> +       read_cache_unmerged();
> +       if (!fill_tree_descriptor(&desc, &oid)) {
> +               error(_("Failed to find tree of %s."), oid_to_hex(&oid));

s/Failed/failed/
s/\.//

> +               rollback_lock_file(&lock);
> +               free((void *)desc.buffer);
> +               strbuf_release(&ref_name);
> +               return -1;
> +       }

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 2/8] sequencer: introduce the `merge` command
  2018-01-18 15:35 ` [PATCH 2/8] sequencer: introduce the `merge` command Johannes Schindelin
  2018-01-18 16:31   ` Jacob Keller
@ 2018-01-19  9:54   ` Eric Sunshine
  2018-01-19 14:45   ` Phillip Wood
  2018-01-22 22:12   ` Junio C Hamano
  3 siblings, 0 replies; 276+ messages in thread
From: Eric Sunshine @ 2018-01-19  9:54 UTC (permalink / raw)
  To: Johannes Schindelin; +Cc: Git List, Junio C Hamano, Jacob Keller

On Thu, Jan 18, 2018 at 10:35 AM, Johannes Schindelin
<johannes.schindelin@gmx.de> wrote:
> [...]
> Note: this patch only adds support for recursive merges, to keep things
> simple. Support for octopus merges will be added later in this patch
> series, support for merges using strategies other than the recursive
> merge is left for future contributions.

The above paragraph...

> The design of the `merge` command as introduced by this patch only
> supports creating new merge commits with exactly two parents, i.e. it
> adds no support for octopus merges.
>
> We will introduce support for octopus merges in a later commit.

and these two sentences say the same thing. I suppose one or the other
was meant to be dropped(?).

> Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
> ---
> diff --git a/sequencer.c b/sequencer.c
> @@ -2069,6 +2077,132 @@ static int do_reset(const char *name, int len)
> +static int do_merge(struct commit *commit, const char *arg, int arg_len,
> +                   struct replay_opts *opts)
> +{
> +       [...]
> +               if (write_message(body, len, git_path_merge_msg(), 0) < 0) {
> +                       error_errno(_("Could not write '%s'"),

s/Could/could/

> +               if (write_message(p, len, git_path_merge_msg(), 0) < 0) {
> +                       error_errno(_("Could not write '%s'"),

Ditto.

> +       if (!head_commit) {
> +               rollback_lock_file(&lock);
> +               return error(_("Cannot merge without a current revision"));

s/Cannot/cannot/

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 4/8] rebase-helper --make-script: introduce a flag to recreate merges
  2018-01-18 15:35 ` [PATCH 4/8] rebase-helper --make-script: introduce a flag to recreate merges Johannes Schindelin
  2018-01-18 21:39   ` Philip Oakley
@ 2018-01-19 10:34   ` Eric Sunshine
  2018-01-23 20:13     ` Junio C Hamano
  2018-01-29 21:05     ` Johannes Schindelin
  2018-01-23 20:03   ` Junio C Hamano
  2 siblings, 2 replies; 276+ messages in thread
From: Eric Sunshine @ 2018-01-19 10:34 UTC (permalink / raw)
  To: Johannes Schindelin; +Cc: Git List, Junio C Hamano, Jacob Keller

On Thu, Jan 18, 2018 at 10:35 AM, Johannes Schindelin
<johannes.schindelin@gmx.de> wrote:
> The sequencer just learned a new commands intended to recreate branch

s/a //

> structure (similar in spirit to --preserve-merges, but with a
> substantially less-broken design).
> [...]
> Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
> ---
> diff --git a/sequencer.c b/sequencer.c
> @@ -2785,6 +2787,335 @@ void append_signoff(struct strbuf *msgbuf, int ignore_footer, unsigned flag)
> +static const char *label_oid(struct object_id *oid, const char *label,
> +                            struct label_state *state)
> +{
> +       [...]
> +       } else if (((len = strlen(label)) == GIT_SHA1_RAWSZ &&
> +                   !get_oid_hex(label, &dummy)) ||
> +                  hashmap_get_from_hash(&state->labels,
> +                                        strihash(label), label)) {
> +               /*
> +                * If the label already exists, or if the label is a valid full
> +                * OID, we append a dash and a number to make it unique.
> +                */
> +               [...]
> +               for (i = 2; ; i++) {

Why '2'? Is there some non-obvious significance to this value?

> +                       strbuf_setlen(buf, len);
> +                       strbuf_addf(buf, "-%d", i);
> +                       if (!hashmap_get_from_hash(&state->labels,
> +                                                  strihash(buf->buf),
> +                                                  buf->buf))
> +                               break;
> +               }
> +
> +static int make_script_with_merges(struct pretty_print_context *pp,
> +                                  struct rev_info *revs, FILE *out,
> +                                  unsigned flags)
> +{
> +       [...]
> +               is_octopus = to_merge && to_merge->next;
> +
> +               if (is_octopus)
> +                       BUG("Octopus merges not yet supported");

Is this a situation which the end-user can trigger by specifying a
merge with more than two parents? If so, shouldn't this be just a
normal error message rather than a (developer) bug message? Or, am I
misunderstanding?

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 5/8] rebase: introduce the --recreate-merges option
  2018-01-18 15:35 ` [PATCH 5/8] rebase: introduce the --recreate-merges option Johannes Schindelin
@ 2018-01-19 10:55   ` Eric Sunshine
  2018-01-29 21:09     ` Johannes Schindelin
  2018-01-23 20:22   ` Junio C Hamano
                     ` (2 subsequent siblings)
  3 siblings, 1 reply; 276+ messages in thread
From: Eric Sunshine @ 2018-01-19 10:55 UTC (permalink / raw)
  To: Johannes Schindelin; +Cc: Git List, Junio C Hamano, Jacob Keller

On Thu, Jan 18, 2018 at 10:35 AM, Johannes Schindelin
<johannes.schindelin@gmx.de> wrote:
> [...]
> With this patch, the goodness of the Git garden shears comes to `git
> rebase -i` itself. Passing the `--recreate-merges` option will generate
> a todo list that can be understood readily, and where it is obvious
> how to reorder commits. New branches can be introduced by inserting
> `label` commands and calling `merge - <label> <oneline>`. And once this
> mode has become stable and universally accepted, we can deprecate the
> design mistake that was `--preserve-merges`.
>
> Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
> ---
> diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh
> @@ -900,6 +900,7 @@ fi
>  if test t != "$preserve_merges"
>  then
>         git rebase--helper --make-script ${keep_empty:+--keep-empty} \
> +               ${recreate_merges:+--recreate-merges} \

If the user specifies both --preserve-merges and --recreate-merges, it
looks like --preserve-merges will take precedence.

Should git-rebase.sh have a mutual-exclusion check and error out if
both are specified?

>                 $revisions ${restrict_revision+^$restrict_revision} >"$todo" ||
>         die "$(gettext "Could not generate todo list")"
> diff --git a/git-rebase.sh b/git-rebase.sh
> @@ -262,6 +264,10 @@ do
> +       --recreate-merges)
> +               recreate_merges=t
> +               test -z "$interactive_rebase" && interactive_rebase=implied
> +               ;;
>         --preserve-merges)
>                 preserve_merges=t
>                 test -z "$interactive_rebase" && interactive_rebase=implied

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 1/8] sequencer: introduce new commands to resettherevision
  2018-01-18 15:35 ` [PATCH 1/8] sequencer: introduce new commands to reset the revision Johannes Schindelin
  2018-01-18 16:25   ` Jacob Keller
  2018-01-19  8:59   ` Eric Sunshine
@ 2018-01-19 12:24   ` Phillip Wood
  2018-01-19 18:55     ` Phillip Wood
  2018-01-29 21:23     ` Johannes Schindelin
  2 siblings, 2 replies; 276+ messages in thread
From: Phillip Wood @ 2018-01-19 12:24 UTC (permalink / raw)
  To: Johannes Schindelin, git; +Cc: Junio C Hamano, Jacob Keller, Philip Oakley, Eric Sunshine


On 18/01/18 15:35, Johannes Schindelin wrote:
> 
> In the upcoming commits, we will teach the sequencer to recreate merges.
> This will be done in a very different way from the unfortunate design of
> `git rebase --preserve-merges` (which does not allow for reordering
> commits, or changing the branch topology).
> 
> The main idea is to introduce new todo list commands, to support
> labeling the current revision with a given name, resetting the current
> revision to a previous state, merging labeled revisions.

I think this would be a great improvement to rebase -i, thanks for
working on it.

> This idea was developed in Git for Windows' Git garden shears (that are
> used to maintain the "thicket of branches" on top of upstream Git), and
> this patch is part of the effort to make it available to a wider
> audience, as well as to make the entire process more robust (by
> implementing it in a safe and portable language rather than a Unix shell
> script).
> 
> This commit implements the commands to label, and to reset to, given
> revisions. The syntax is:
> 
> 	label <name>
> 	reset <name>

If I've understood the code below correctly then reset will clobber
untracked files, this is the opposite behaviour to what happens when
tries to checkout <onto> at the start of a rebase - then it will fail if
untracked files would be overwritten.

> As a convenience shortcut, also to improve readability of the generated
> todo list, a third command is introduced: bud. It simply resets to the
> "onto" revision, i.e. the commit onto which we currently rebase.

I found the whole bud business bewildering at first, reading the other
replies it seems I wasn't the only one to be befuddled by it. Having
seen an example I can see what it's trying to do but I still think it
adds more confusion than value.

> Internally, the `label <name>` command creates the ref
> `refs/rewritten/<name>`. This makes it possible to work with the labeled
> revisions interactively, or in a scripted fashion (e.g. via the todo
> list command `exec`).

If a user has two work trees and runs a rebase in each with the same
label name, they'll clobber each other. I'd suggest storing them under
refs/rewritten/<branch-name or detached HEAD SHA> instead. If the user
tries to rebase a second worktree with the same detached HEAD as an
existing rebase then refuse to start.

> Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
> ---
>  git-rebase--interactive.sh |   3 +
>  sequencer.c                | 181 ++++++++++++++++++++++++++++++++++++++++++++-
>  2 files changed, 180 insertions(+), 4 deletions(-)
> 
> diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh
> index d47bd29593a..3d2cd19d65a 100644
> --- a/git-rebase--interactive.sh
> +++ b/git-rebase--interactive.sh
> @@ -162,6 +162,9 @@ s, squash = use commit, but meld into previous commit
>  f, fixup = like \"squash\", but discard this commit's log message
>  x, exec = run command (the rest of the line) using shell
>  d, drop = remove commit
> +l, label = label current HEAD with a name
> +t, reset = reset HEAD to a label
> +b, bud = reset HEAD to the revision labeled 'onto'
>  
>  These lines can be re-ordered; they are executed from top to bottom.
>  " | git stripspace --comment-lines >>"$todo"
> diff --git a/sequencer.c b/sequencer.c
> index 4d3f60594cb..91cc55a002f 100644
> --- a/sequencer.c
> +++ b/sequencer.c
> @@ -21,6 +21,8 @@
>  #include "log-tree.h"
>  #include "wt-status.h"
>  #include "hashmap.h"
> +#include "unpack-trees.h"
> +#include "worktree.h"
>  
>  #define GIT_REFLOG_ACTION "GIT_REFLOG_ACTION"
>  
> @@ -116,6 +118,13 @@ static GIT_PATH_FUNC(rebase_path_stopped_sha, "rebase-merge/stopped-sha")
>  static GIT_PATH_FUNC(rebase_path_rewritten_list, "rebase-merge/rewritten-list")
>  static GIT_PATH_FUNC(rebase_path_rewritten_pending,
>  	"rebase-merge/rewritten-pending")
> +
> +/*
> + * The path of the file listing refs that need to be deleted after the rebase
> + * finishes. This is used by the `merge` command.
> + */
> +static GIT_PATH_FUNC(rebase_path_refs_to_delete, "rebase-merge/refs-to-delete")
> +
>  /*
>   * The following files are written by git-rebase just after parsing the
>   * command-line (and are only consumed, not modified, by the sequencer).
> @@ -767,6 +776,9 @@ enum todo_command {
>  	TODO_SQUASH,
>  	/* commands that do something else than handling a single commit */
>  	TODO_EXEC,
> +	TODO_LABEL,
> +	TODO_RESET,
> +	TODO_BUD,
>  	/* commands that do nothing but are counted for reporting progress */
>  	TODO_NOOP,
>  	TODO_DROP,
> @@ -785,6 +797,9 @@ static struct {
>  	{ 'f', "fixup" },
>  	{ 's', "squash" },
>  	{ 'x', "exec" },
> +	{ 'l', "label" },
> +	{ 't', "reset" },
> +	{ 'b', "bud" },
>  	{ 0,   "noop" },
>  	{ 'd', "drop" },
>  	{ 0,   NULL }
> @@ -1253,7 +1268,8 @@ static int parse_insn_line(struct todo_item *item, const char *bol, char *eol)
>  		if (skip_prefix(bol, todo_command_info[i].str, &bol)) {
>  			item->command = i;
>  			break;
> -		} else if (bol[1] == ' ' && *bol == todo_command_info[i].c) {
> +		} else if ((bol + 1 == eol || bol[1] == ' ') &&
> +			   *bol == todo_command_info[i].c) {
>  			bol++;
>  			item->command = i;
>  			break;
> @@ -1265,7 +1281,7 @@ static int parse_insn_line(struct todo_item *item, const char *bol, char *eol)
>  	padding = strspn(bol, " \t");
>  	bol += padding;
>  
> -	if (item->command == TODO_NOOP) {
> +	if (item->command == TODO_NOOP || item->command == TODO_BUD) {
>  		if (bol != eol)
>  			return error(_("%s does not accept arguments: '%s'"),
>  				     command_to_string(item->command), bol);
> @@ -1279,7 +1295,8 @@ static int parse_insn_line(struct todo_item *item, const char *bol, char *eol)
>  		return error(_("missing arguments for %s"),
>  			     command_to_string(item->command));
>  
> -	if (item->command == TODO_EXEC) {
> +	if (item->command == TODO_EXEC || item->command == TODO_LABEL ||
> +	    item->command == TODO_RESET) {
>  		item->commit = NULL;
>  		item->arg = bol;
>  		item->arg_len = (int)(eol - bol);
> @@ -1919,6 +1936,139 @@ static int do_exec(const char *command_line)
>  	return status;
>  }
>  
> +static int safe_append(const char *filename, const char *fmt, ...)
> +{
> +	va_list ap;
> +	struct lock_file lock = LOCK_INIT;
> +	int fd = hold_lock_file_for_update(&lock, filename, 0);
> +	struct strbuf buf = STRBUF_INIT;
> +
> +	if (fd < 0)
> +		return error_errno(_("could not lock '%s'"), filename);
> +
> +	if (strbuf_read_file(&buf, filename, 0) < 0 && errno != ENOENT)
> +		return error_errno(_("could not read '%s'"), filename);
> +	strbuf_complete(&buf, '\n');
> +	va_start(ap, fmt);
> +	strbuf_vaddf(&buf, fmt, ap);
> +	va_end(ap);
> +
> +	if (write_in_full(fd, buf.buf, buf.len) < 0) {
> +		rollback_lock_file(&lock);
> +		return error_errno(_("could not write to '%s'"), filename);
> +	}
> +	if (commit_lock_file(&lock) < 0) {
> +		rollback_lock_file(&lock);
> +		return error(_("failed to finalize '%s'."), filename);
> +	}
> +
> +	return 0;
> +}
> +
> +static int do_label(const char *name, int len)
> +{
> +	struct ref_store *refs = get_main_ref_store();
> +	struct ref_transaction *transaction;
> +	struct strbuf ref_name = STRBUF_INIT, err = STRBUF_INIT;
> +	struct strbuf msg = STRBUF_INIT;
> +	int ret = 0;
> +	struct object_id head_oid;
> +
> +	strbuf_addf(&ref_name, "refs/rewritten/%.*s", len, name);
> +	strbuf_addf(&msg, "label '%.*s'", len, name);

The other reflog messages below have a (rebase -i) prefix

> +
> +	transaction = ref_store_transaction_begin(refs, &err);
> +	if (!transaction ||
> +	    get_oid("HEAD", &head_oid) ||
> +	    ref_transaction_update(transaction, ref_name.buf, &head_oid, NULL,
> +				   0, msg.buf, &err) < 0 ||
> +	    ref_transaction_commit(transaction, &err)) {
> +		error("%s", err.buf);

if get_oid() fails then err is empty so there wont be an message after
the 'error: '

> +		ret = -1;
> +	}
> +	ref_transaction_free(transaction);
> +	strbuf_release(&err);
> +	strbuf_release(&msg);
> +
> +	if (!ret)
> +		ret = safe_append(rebase_path_refs_to_delete(),
> +				  "%s\n", ref_name.buf);
> +	strbuf_release(&ref_name);
> +
> +	return ret;
> +}
> +
> +static int do_reset(const char *name, int len)
> +{
> +	struct strbuf ref_name = STRBUF_INIT;
> +	struct object_id oid;
> +	struct lock_file lock = LOCK_INIT;
> +	struct tree_desc desc;
> +	struct tree *tree;
> +	struct unpack_trees_options opts;
> +	int ret = 0, i;
> +
> +	if (hold_locked_index(&lock, LOCK_REPORT_ON_ERROR) < 0)
> +		return -1;
> +
> +	for (i = 0; i < len; i++)
> +		if (isspace(name[i]))
> +			len = i;

If name starts with any white space then I think this effectively
truncates name to a bunch of white space which doesn't sound right. I'm
not sure how this is being called, but it might be better to clean up
name when the to-do list is parsed instead.

> +
> +	strbuf_addf(&ref_name, "refs/rewritten/%.*s", len, name);
> +	if (get_oid(ref_name.buf, &oid) &&
> +	    get_oid(ref_name.buf + strlen("refs/rewritten/"), &oid)) {
> +		error(_("could not read '%s'"), ref_name.buf);
> +		rollback_lock_file(&lock);
> +		strbuf_release(&ref_name);
> +		return -1;
> +	}
> +
> +	memset(&opts, 0, sizeof(opts));
> +	opts.head_idx = 1;
> +	opts.src_index = &the_index;
> +	opts.dst_index = &the_index;
> +	opts.fn = oneway_merge;
> +	opts.merge = 1;
> +	opts.update = 1;
> +	opts.reset = 1;
> +
> +	read_cache_unmerged();
> +	if (!fill_tree_descriptor(&desc, &oid)) {
> +		error(_("Failed to find tree of %s."), oid_to_hex(&oid));
> +		rollback_lock_file(&lock);
> +		free((void *)desc.buffer);
> +		strbuf_release(&ref_name);
> +		return -1;
> +	}
> +
> +	if (unpack_trees(1, &desc, &opts)) {
> +		rollback_lock_file(&lock);
> +		free((void *)desc.buffer);
> +		strbuf_release(&ref_name);
> +		return -1;
> +	}
> +
> +	tree = parse_tree_indirect(&oid);
> +	prime_cache_tree(&the_index, tree);
> +
> +	if (write_locked_index(&the_index, &lock, COMMIT_LOCK) < 0)
> +		ret = error(_("could not write index"));
> +	free((void *)desc.buffer);
> +
> +	if (!ret) {
> +		struct strbuf msg = STRBUF_INIT;
> +
> +		strbuf_addf(&msg, "(rebase -i) reset '%.*s'", len, name);
> +		ret = update_ref(msg.buf, "HEAD", &oid, NULL, 0,
> +				 UPDATE_REFS_MSG_ON_ERR);
> +		strbuf_release(&msg);
> +	}
> +
> +	strbuf_release(&ref_name);
> +	return ret;
> +}
> +
>  static int is_final_fixup(struct todo_list *todo_list)
>  {
>  	int i = todo_list->current;
> @@ -2102,7 +2252,13 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts)
>  				/* `current` will be incremented below */
>  				todo_list->current = -1;
>  			}
> -		} else if (!is_noop(item->command))
> +		} else if (item->command == TODO_LABEL)
> +			res = do_label(item->arg, item->arg_len);
> +		else if (item->command == TODO_RESET)
> +			res = do_reset(item->arg, item->arg_len);
> +		else if (item->command == TODO_BUD)
> +			res = do_reset("onto", 4);
> +		else if (!is_noop(item->command))
>  			return error(_("unknown command %d"), item->command);
>  
>  		todo_list->current++;
> @@ -2207,6 +2363,23 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts)
>  		}
>  		apply_autostash(opts);
>  
> +		strbuf_reset(&buf);
> +		if (strbuf_read_file(&buf, rebase_path_refs_to_delete(), 0)
> +		    > 0) {
> +			char *p = buf.buf;
> +			while (*p) {
> +				char *eol = strchr(p, '\n');
> +				if (eol)
> +					*eol = '\0';
> +				if (delete_ref("(rebase -i) cleanup",
> +					       p, NULL, 0) < 0)
> +					warning(_("could not delete '%s'"), p);
> +				if (!eol)
> +					break;
> +				p = eol + 1;
> +			}
> +		}
> +
>  		fprintf(stderr, "Successfully rebased and updated %s.\n",
>  			head_ref.buf);
>  
> 


^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 2/8] sequencer: introduce the `merge` command
  2018-01-18 15:35 ` [PATCH 2/8] sequencer: introduce the `merge` command Johannes Schindelin
  2018-01-18 16:31   ` Jacob Keller
  2018-01-19  9:54   ` Eric Sunshine
@ 2018-01-19 14:45   ` Phillip Wood
  2018-01-20  9:18     ` Jacob Keller
  2018-01-22 22:12   ` Junio C Hamano
  3 siblings, 1 reply; 276+ messages in thread
From: Phillip Wood @ 2018-01-19 14:45 UTC (permalink / raw)
  To: Johannes Schindelin, git; +Cc: Junio C Hamano, Jacob Keller, Eric Sunshine

On 18/01/18 15:35, Johannes Schindelin wrote:
> 
> This patch is part of the effort to reimplement `--preserve-merges` with
> a substantially improved design, a design that has been developed in the
> Git for Windows project to maintain the dozens of Windows-specific patch
> series on top of upstream Git.
> 
> The previous patch implemented the `label`, `bud` and `reset` commands
> to label commits and to reset to a labeled commits. This patch adds the
> `merge` command, with the following syntax:
> 
> 	merge <commit> <rev> <oneline>

I'm concerned that this will be confusing for users. All of the other
rebase commands replay the changes in the commit hash immediately
following the command name. This command instead uses the first commit
to specify the message which is different to both 'git merge' and the
existing rebase commands. I wonder if it would be clearer to have 'merge
-C <commit> <rev> ...' instead so it's clear which argument specifies
the message and which the remote head to merge. It would also allow for
'merge -c <commit> <rev> ...' in the future for rewording an existing
merge message and also avoid the slightly odd 'merge - <rev> ...'. Where
it's creating new merges I'm not sure it's a good idea to encourage
people to only have oneline commit messages by making it harder to edit
them, perhaps it could take another argument to mean open the editor or
not, though as Jake said I guess it's not that common.

One thought that just struck me - if a merge or reset command specifies
an invalid label is it rescheduled so that it's still in the to-do list
when the user edits it after rebase stops?

In the future it might be nice if the label, reset and merge commands
were validated when the to-do list is parsed so that the user gets
immediate feedback if they try to create a label that is not a valid ref
name or that they have a typo in a name given to reset or merge rather
than the rebase stopping later.

> The <commit> parameter in this instance is the *original* merge commit,
> whose author and message will be used for the to-be-created merge
> commit.
> 
> The <rev> parameter refers to the (possibly rewritten) revision to
> merge. Let's see an example of a todo list:
> 
> 	label onto
> 
> 	# Branch abc
> 	bud
> 	pick deadbeef Hello, world!
> 	label abc
> 
> 	bud
> 	pick cafecafe And now for something completely different
> 	merge baaabaaa abc Merge the branch 'abc' into master
> 
> To support creating *new* merges, i.e. without copying the commit
> message from an existing commit, use the special value `-` as <commit>
> parameter (in which case the text after the <rev> parameter is used as
> commit message):
> 
> 	merge - abc This will be the actual commit message of the merge
> 
> This comes in handy when splitting a branch into two or more branches.
> 
> Note: this patch only adds support for recursive merges, to keep things
> simple. Support for octopus merges will be added later in this patch
> series, support for merges using strategies other than the recursive
> merge is left for future contributions.
> 
> The design of the `merge` command as introduced by this patch only
> supports creating new merge commits with exactly two parents, i.e. it
> adds no support for octopus merges.
> 
> We will introduce support for octopus merges in a later commit.
> 
> Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
> ---
>  git-rebase--interactive.sh |   1 +
>  sequencer.c                | 146 +++++++++++++++++++++++++++++++++++++++++++--
>  2 files changed, 143 insertions(+), 4 deletions(-)
> 
> diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh
> index 3d2cd19d65a..5bf1ea3781f 100644
> --- a/git-rebase--interactive.sh
> +++ b/git-rebase--interactive.sh
> @@ -165,6 +165,7 @@ d, drop = remove commit
>  l, label = label current HEAD with a name
>  t, reset = reset HEAD to a label
>  b, bud = reset HEAD to the revision labeled 'onto'
> +m, merge = create a merge commit using a given commit's message
>  
>  These lines can be re-ordered; they are executed from top to bottom.
>  " | git stripspace --comment-lines >>"$todo"
> diff --git a/sequencer.c b/sequencer.c
> index 91cc55a002f..567cfcbbe8b 100644
> --- a/sequencer.c
> +++ b/sequencer.c
> @@ -779,6 +779,7 @@ enum todo_command {
>  	TODO_LABEL,
>  	TODO_RESET,
>  	TODO_BUD,
> +	TODO_MERGE,
>  	/* commands that do nothing but are counted for reporting progress */
>  	TODO_NOOP,
>  	TODO_DROP,
> @@ -800,6 +801,7 @@ static struct {
>  	{ 'l', "label" },
>  	{ 't', "reset" },
>  	{ 'b', "bud" },
> +	{ 'm', "merge" },
>  	{ 0,   "noop" },
>  	{ 'd', "drop" },
>  	{ 0,   NULL }
> @@ -1304,14 +1306,20 @@ static int parse_insn_line(struct todo_item *item, const char *bol, char *eol)
>  	}
>  
>  	end_of_object_name = (char *) bol + strcspn(bol, " \t\n");
> +	item->arg = end_of_object_name + strspn(end_of_object_name, " \t");
> +	item->arg_len = (int)(eol - item->arg);
> +
>  	saved = *end_of_object_name;
> +	if (item->command == TODO_MERGE && *bol == '-' &&
> +	    bol + 1 == end_of_object_name) {
> +		item->commit = NULL;
> +		return 0;
> +	}
> +
>  	*end_of_object_name = '\0';
>  	status = get_oid(bol, &commit_oid);
>  	*end_of_object_name = saved;
>  
> -	item->arg = end_of_object_name + strspn(end_of_object_name, " \t");
> -	item->arg_len = (int)(eol - item->arg);
> -
>  	if (status < 0)
>  		return -1;
>  
> @@ -2069,6 +2077,132 @@ static int do_reset(const char *name, int len)
>  	return ret;
>  }
>  
> +static int do_merge(struct commit *commit, const char *arg, int arg_len,
> +		    struct replay_opts *opts)
> +{
> +	int merge_arg_len;
> +	struct strbuf ref_name = STRBUF_INIT;
> +	struct commit *head_commit, *merge_commit, *i;
> +	struct commit_list *common, *j, *reversed = NULL;
> +	struct merge_options o;
> +	int ret;
> +	static struct lock_file lock;
> +
> +	for (merge_arg_len = 0; merge_arg_len < arg_len; merge_arg_len++)
> +		if (isspace(arg[merge_arg_len]))
> +			break;
> +
> +	if (hold_locked_index(&lock, LOCK_REPORT_ON_ERROR) < 0)
> +		return -1;
> +
> +	if (commit) {
> +		const char *message = get_commit_buffer(commit, NULL);
> +		const char *body;
> +		int len;
> +
> +		if (!message) {
> +			rollback_lock_file(&lock);
> +			return error(_("could not get commit message of '%s'"),
> +				     oid_to_hex(&commit->object.oid));
> +		}
> +		write_author_script(message);
> +		find_commit_subject(message, &body);
> +		len = strlen(body);
> +		if (write_message(body, len, git_path_merge_msg(), 0) < 0) {
> +			error_errno(_("Could not write '%s'"),
> +				    git_path_merge_msg());
> +			unuse_commit_buffer(commit, message);
> +			rollback_lock_file(&lock);
> +			return -1;
> +		}
> +		unuse_commit_buffer(commit, message);
> +	} else {
> +		const char *p = arg + merge_arg_len;
> +		struct strbuf buf = STRBUF_INIT;
> +		int len;
> +
> +		strbuf_addf(&buf, "author %s", git_author_info(0));
> +		write_author_script(buf.buf);
> +		strbuf_reset(&buf);
> +
> +		p += strspn(p, " \t");
> +		if (*p)
> +			len = strlen(p);
> +		else {
> +			strbuf_addf(&buf, "Merge branch '%.*s'",
> +				    merge_arg_len, arg);
> +			p = buf.buf;
> +			len = buf.len;
> +		}
> +
> +		if (write_message(p, len, git_path_merge_msg(), 0) < 0) {
> +			error_errno(_("Could not write '%s'"),
> +				    git_path_merge_msg());
> +			strbuf_release(&buf);
> +			rollback_lock_file(&lock);
> +			return -1;
> +		}
> +		strbuf_release(&buf);
> +	}
> +
> +	head_commit = lookup_commit_reference_by_name("HEAD");
> +	if (!head_commit) {
> +		rollback_lock_file(&lock);
> +		return error(_("Cannot merge without a current revision"));
> +	}
> +
> +	strbuf_addf(&ref_name, "refs/rewritten/%.*s", merge_arg_len, arg);
> +	merge_commit = lookup_commit_reference_by_name(ref_name.buf);
> +	if (!merge_commit) {
> +		/* fall back to non-rewritten ref or commit */
> +		strbuf_splice(&ref_name, 0, strlen("refs/rewritten/"), "", 0);
> +		merge_commit = lookup_commit_reference_by_name(ref_name.buf);
> +	}
> +	if (!merge_commit) {
> +		error(_("could not resolve '%s'"), ref_name.buf);
> +		strbuf_release(&ref_name);
> +		rollback_lock_file(&lock);
> +		return -1;
> +	}
> +	write_message(oid_to_hex(&merge_commit->object.oid), GIT_SHA1_HEXSZ,
> +		      git_path_merge_head(), 0);
> +	write_message("no-ff", 5, git_path_merge_mode(), 0);
> +
> +	common = get_merge_bases(head_commit, merge_commit);
> +	for (j = common; j; j = j->next)
> +		commit_list_insert(j->item, &reversed);
> +	free_commit_list(common);
> +
> +	read_cache();
> +	init_merge_options(&o);
> +	o.branch1 = "HEAD";
> +	o.branch2 = ref_name.buf;
> +	o.buffer_output = 2;
> +
> +	ret = merge_recursive(&o, head_commit, merge_commit, reversed, &i);
> +	if (ret <= 0)
> +		fputs(o.obuf.buf, stdout);
> +	strbuf_release(&o.obuf);
> +	if (ret < 0) {
> +		strbuf_release(&ref_name);
> +		rollback_lock_file(&lock);
> +		return error(_("conflicts while merging '%.*s'"),
> +			     merge_arg_len, arg);
> +	}
> +
> +	if (active_cache_changed &&
> +	    write_locked_index(&the_index, &lock, COMMIT_LOCK)) {
> +		strbuf_release(&ref_name);
> +		return error(_("merge: Unable to write new index file"));
> +	}
> +	rollback_lock_file(&lock);
> +
> +	ret = run_git_commit(git_path_merge_msg(), opts, 0);
> +	strbuf_release(&ref_name);
> +
> +	return ret;
> +}
> +
>  static int is_final_fixup(struct todo_list *todo_list)
>  {
>  	int i = todo_list->current;
> @@ -2258,6 +2392,9 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts)
>  			res = do_reset(item->arg, item->arg_len);
>  		else if (item->command == TODO_BUD)
>  			res = do_reset("onto", 4);
> +		else if (item->command == TODO_MERGE)
> +			res = do_merge(item->commit,
> +				       item->arg, item->arg_len, opts);
>  		else if (!is_noop(item->command))
>  			return error(_("unknown command %d"), item->command);
>  
> @@ -2757,7 +2894,8 @@ int transform_todos(unsigned flags)
>  					  oid_to_hex(&item->commit->object.oid);
>  
>  			strbuf_addf(&buf, " %s", oid);
> -		}
> +		} else if (item->command == TODO_MERGE)
> +			strbuf_addstr(&buf, " -");
>  		/* add all the rest */
>  		if (!item->arg_len)
>  			strbuf_addch(&buf, '\n');
> 


^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 3/8] sequencer: fast-forward merge commits, if possible
  2018-01-18 15:35 ` [PATCH 3/8] sequencer: fast-forward merge commits, if possible Johannes Schindelin
@ 2018-01-19 14:53   ` Phillip Wood
  2018-01-23 19:12     ` Junio C Hamano
  2018-01-29 21:47     ` Johannes Schindelin
  2018-01-23 18:51   ` Junio C Hamano
  1 sibling, 2 replies; 276+ messages in thread
From: Phillip Wood @ 2018-01-19 14:53 UTC (permalink / raw)
  To: Johannes Schindelin, git; +Cc: Junio C Hamano, Jacob Keller

On 18/01/18 15:35, Johannes Schindelin wrote:
> 
> Just like with regular `pick` commands, if we are trying to recreate a
> merge commit, we now test whether the parents of said commit match HEAD
> and the commits to be merged, and fast-forward if possible.
> 
> This is not only faster, but also avoids unnecessary proliferation of
> new objects.

I might have missed something but shouldn't this be checking opts->allow_ff?

Another possible optimization is that if the parent branches have only
reworded commits or some commits that have been squashed but no other
changes then their trees will be the same as in the original merge
commit and so could be reused without calling merge_recursive().

> Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
> ---
>  sequencer.c | 21 ++++++++++++++++++++-
>  1 file changed, 20 insertions(+), 1 deletion(-)
> 
> diff --git a/sequencer.c b/sequencer.c
> index 567cfcbbe8b..a96255426e7 100644
> --- a/sequencer.c
> +++ b/sequencer.c
> @@ -2085,7 +2085,7 @@ static int do_merge(struct commit *commit, const char *arg, int arg_len,
>  	struct commit *head_commit, *merge_commit, *i;
>  	struct commit_list *common, *j, *reversed = NULL;
>  	struct merge_options o;
> -	int ret;
> +	int can_fast_forward, ret;
>  	static struct lock_file lock;
>  
>  	for (merge_arg_len = 0; merge_arg_len < arg_len; merge_arg_len++)
> @@ -2151,6 +2151,14 @@ static int do_merge(struct commit *commit, const char *arg, int arg_len,
>  		return error(_("Cannot merge without a current revision"));
>  	}
>  
> +	/*
> +	 * If HEAD is not identical to the parent of the original merge commit,
> +	 * we cannot fast-forward.
> +	 */
> +	can_fast_forward = commit && commit->parents &&
> +		!oidcmp(&commit->parents->item->object.oid,
> +			&head_commit->object.oid);
> +
>  	strbuf_addf(&ref_name, "refs/rewritten/%.*s", merge_arg_len, arg);
>  	merge_commit = lookup_commit_reference_by_name(ref_name.buf);
>  	if (!merge_commit) {
> @@ -2164,6 +2172,17 @@ static int do_merge(struct commit *commit, const char *arg, int arg_len,
>  		rollback_lock_file(&lock);
>  		return -1;
>  	}
> +
> +	if (can_fast_forward && commit->parents->next &&
> +	    !commit->parents->next->next &&
> +	    !oidcmp(&commit->parents->next->item->object.oid,
> +		    &merge_commit->object.oid)) {
> +		strbuf_release(&ref_name);
> +		rollback_lock_file(&lock);
> +		return fast_forward_to(&commit->object.oid,
> +				       &head_commit->object.oid, 0, opts);
> +	}
> +
>  	write_message(oid_to_hex(&merge_commit->object.oid), GIT_SHA1_HEXSZ,
>  		      git_path_merge_head(), 0);
>  	write_message("no-ff", 5, git_path_merge_mode(), 0);
> 


^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 1/8] sequencer: introduce new commands to resettherevision
  2018-01-19 12:24   ` [PATCH 1/8] sequencer: introduce new commands to resettherevision Phillip Wood
@ 2018-01-19 18:55     ` Phillip Wood
  2018-01-19 18:59       ` Jacob Keller
  2018-01-29 21:23     ` Johannes Schindelin
  1 sibling, 1 reply; 276+ messages in thread
From: Phillip Wood @ 2018-01-19 18:55 UTC (permalink / raw)
  To: Johannes Schindelin, git; +Cc: Junio C Hamano, Jacob Keller, Philip Oakley, Eric Sunshine

On 19/01/18 12:24, Phillip Wood wrote:
> 
> On 18/01/18 15:35, Johannes Schindelin wrote:
>>
>> Internally, the `label <name>` command creates the ref
>> `refs/rewritten/<name>`. This makes it possible to work with the labeled
>> revisions interactively, or in a scripted fashion (e.g. via the todo
>> list command `exec`).
> 
> If a user has two work trees and runs a rebase in each with the same
> label name, they'll clobber each other. I'd suggest storing them under
> refs/rewritten/<branch-name or detached HEAD SHA> instead. If the user
> tries to rebase a second worktree with the same detached HEAD as an
> existing rebase then refuse to start.
> 

Ah this isn't a concern after all as patch 5 makes refs/rewritten local
to the worktree. Perhaps you could move that part of patch 5 here or add
a note to the commit message that it will become worktree local later in
the series

Best Wishes

Phillip

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 1/8] sequencer: introduce new commands to resettherevision
  2018-01-19 18:55     ` Phillip Wood
@ 2018-01-19 18:59       ` Jacob Keller
  2018-01-29 21:25         ` Johannes Schindelin
  0 siblings, 1 reply; 276+ messages in thread
From: Jacob Keller @ 2018-01-19 18:59 UTC (permalink / raw)
  To: Phillip Wood; +Cc: Johannes Schindelin, Git mailing list, Junio C Hamano, Philip Oakley, Eric Sunshine

On Fri, Jan 19, 2018 at 10:55 AM, Phillip Wood
<phillip.wood@talktalk.net> wrote:
> On 19/01/18 12:24, Phillip Wood wrote:
>>
>> On 18/01/18 15:35, Johannes Schindelin wrote:
>>>
>>> Internally, the `label <name>` command creates the ref
>>> `refs/rewritten/<name>`. This makes it possible to work with the labeled
>>> revisions interactively, or in a scripted fashion (e.g. via the todo
>>> list command `exec`).
>>
>> If a user has two work trees and runs a rebase in each with the same
>> label name, they'll clobber each other. I'd suggest storing them under
>> refs/rewritten/<branch-name or detached HEAD SHA> instead. If the user
>> tries to rebase a second worktree with the same detached HEAD as an
>> existing rebase then refuse to start.
>>
>
> Ah this isn't a concern after all as patch 5 makes refs/rewritten local
> to the worktree. Perhaps you could move that part of patch 5 here or add
> a note to the commit message that it will become worktree local later in
> the series
>
> Best Wishes
>
> Phillip

I'd rather it be included here as well.

Thanks,
Jake

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 0/8] rebase -i: offer to recreate merge commits
  2018-01-18 15:35 [PATCH 0/8] rebase -i: offer to recreate merge commits Johannes Schindelin
                   ` (9 preceding siblings ...)
  2018-01-18 18:36 ` [PATCH 9, 10/8] interactive rebase feedback Stefan Beller
@ 2018-01-19 20:25 ` Junio C Hamano
  2018-01-29 21:53   ` Johannes Schindelin
  2018-01-23 20:29 ` Junio C Hamano
  2018-01-29 22:54 ` [PATCH v2 00/10] " Johannes Schindelin
  12 siblings, 1 reply; 276+ messages in thread
From: Junio C Hamano @ 2018-01-19 20:25 UTC (permalink / raw)
  To: Johannes Schindelin; +Cc: git, Jacob Keller

Johannes Schindelin <johannes.schindelin@gmx.de> writes:

> Think of --recreate-merges as "--preserve-merges done right". It
> introduces new verbs for the todo list, `label`, `reset` and `merge`.
> For a commit topology like this:
>
>             A - B - C
>               \   /
>                 D
>
> the generated todo list would look like this:
>
>             # branch D
>             pick 0123 A
>             label branch-point
>             pick 1234 D
>             label D
>
>             reset branch-point
>             pick 2345 B
>             merge 3456 D C

Yup.  I've seen this design talked about on list in the past, and
I've always felt that this is "sequencer done right".

At the first glance, it may feel somewhat unsatisfying that "merge"
has to say effects of which commits should be reflected in the
result and which commot to take the log message from, i.e.
(recreated)D is merged to form the resulting tree, and 3456=C is
used for the log, to recreate C in the above example, while "pick"
always uses the same commit for both, i.e. recreated B inherits both
the changes and log message from the original B=2345 (or depending
on the readers' point of view, "merge" is allowed to use two
different commits, while "pick" is always limited to the same one).

But I think this distinction is probably fundamental and I am not
opposed to it at all.  The result of "pick" has only one parent, and
the parent is determined only by the previous actions and not by
anything on the "pick" line in the todo list.  But the result of
"merge" has to record all the other parents, and only the first
parent is determined implicitly by the previous actions.  We need to
tell the "merge" command about "3456=C" in order to recreate the
effect of original merge commit (i.e. changes between B and C) as
well as its log message, and we also need to tell it about label "D"
that it is the "other parent" that need to be recorded.

Obviously "merge" command syntax should allow recreating an octopus,
so whenever I said "two" in the above, I meant "N".  The original
merge commit is needed so that the effect to replay (roughly: a
patch going to the original merge result from its first parent) can
be learned from the existing history, and all the other "N-1"
parents needs to be given (and they must have been already created
in the todo list) so that the resulting recreated merge can be
recorded with them as parents (in addition to the first parent that
is implicitly given as the result of all the previous steps).

One interesting (and probably useful) thing to notice is that if A
were not rebased in the above sample picture, and only B were the
one that was tweaked, then a recreated C may use the same original D
as its side parent, and the mechanism outlined above naturally can
support it by allowing an un-rewritten commit to be given as a side
parent when "merge" is redoing C.

I probably won't have time to actually look at the code for a few
days, but I am reasonably excited about the topic ;-)

Thanks.

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 9/8] [DO NOT APPLY, but squash?] git-rebase--interactive: clarify arguments
  2018-01-18 21:36     ` Johannes Schindelin
  2018-01-18 21:58       ` Stefan Beller
@ 2018-01-19 20:30       ` Junio C Hamano
  2018-01-20  9:14         ` Jacob Keller
  1 sibling, 1 reply; 276+ messages in thread
From: Junio C Hamano @ 2018-01-19 20:30 UTC (permalink / raw)
  To: Johannes Schindelin; +Cc: Stefan Beller, git, jacob.keller

Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:

> Good idea! I would rather do it as an introductory patch (that only
> converts the existing list).
>
> As to `merge`: it is a bit more complicated ;-)
>
> 	m, merge <original-merge-commit> ( <label> | "<label>..." ) [<oneline>]
> 		create a merge commit using the original merge commit's
> 		message (or the oneline, if "-" is given). Use a quoted
> 		list of commits to be merged for octopus merges.

Is it just the message that is being reused?  

Aren't the trees of the original commit and its parents participate
in creating the tree of the recreated merge?  One way to preserve an
originally evil merge is to notice how it was made by taking the
difference between the result of mechanical merge of original merge
parents and the original merge result, and carry it forward when
recreating the merge across new parents.  Just being curious.


^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 8/8] rebase -i: introduce --recreate-merges=no-rebase-cousins
  2018-01-18 15:36 ` [PATCH 8/8] rebase -i: introduce --recreate-merges=no-rebase-cousins Johannes Schindelin
  2018-01-18 22:00   ` Philip Oakley
@ 2018-01-20  1:09   ` Eric Sunshine
  1 sibling, 0 replies; 276+ messages in thread
From: Eric Sunshine @ 2018-01-20  1:09 UTC (permalink / raw)
  To: Johannes Schindelin; +Cc: Git List, Junio C Hamano, Jacob Keller

On Thu, Jan 18, 2018 at 10:36 AM, Johannes Schindelin
<johannes.schindelin@gmx.de> wrote:
> This one is a bit tricky to explain, so let's try with a diagram:
> [...]
> Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
> ---
> diff --git a/builtin/rebase--helper.c b/builtin/rebase--helper.c
> @@ -57,8 +59,13 @@ int cmd_rebase__helper(int argc, const char **argv, const char *prefix)
> +       if (no_rebase_cousins >= 0&& !recreate_merges)

Style: space before &&

> +               warning(_("--[no-]rebase-cousins has no effect without "
> +                         "--recreate-merges"));

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 9/8] [DO NOT APPLY, but squash?] git-rebase--interactive: clarify arguments
  2018-01-19 20:30       ` Junio C Hamano
@ 2018-01-20  9:14         ` Jacob Keller
  2018-01-29 17:02           ` Johannes Schindelin
  0 siblings, 1 reply; 276+ messages in thread
From: Jacob Keller @ 2018-01-20  9:14 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: Johannes Schindelin, Stefan Beller, Git mailing list

On Fri, Jan 19, 2018 at 12:30 PM, Junio C Hamano <gitster@pobox.com> wrote:
> Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:
>
>> Good idea! I would rather do it as an introductory patch (that only
>> converts the existing list).
>>
>> As to `merge`: it is a bit more complicated ;-)
>>
>>       m, merge <original-merge-commit> ( <label> | "<label>..." ) [<oneline>]
>>               create a merge commit using the original merge commit's
>>               message (or the oneline, if "-" is given). Use a quoted
>>               list of commits to be merged for octopus merges.
>
> Is it just the message that is being reused?
>
> Aren't the trees of the original commit and its parents participate
> in creating the tree of the recreated merge?  One way to preserve an
> originally evil merge is to notice how it was made by taking the
> difference between the result of mechanical merge of original merge
> parents and the original merge result, and carry it forward when
> recreating the merge across new parents.  Just being curious.
>

It looks like currently that only the commit is kept, with no attempt
to recreate evil merges.

Thanks,
Jake

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 2/8] sequencer: introduce the `merge` command
  2018-01-19 14:45   ` Phillip Wood
@ 2018-01-20  9:18     ` Jacob Keller
  2018-01-29 21:41       ` Johannes Schindelin
  0 siblings, 1 reply; 276+ messages in thread
From: Jacob Keller @ 2018-01-20  9:18 UTC (permalink / raw)
  To: Phillip Wood; +Cc: Johannes Schindelin, Git mailing list, Junio C Hamano, Eric Sunshine

On Fri, Jan 19, 2018 at 6:45 AM, Phillip Wood <phillip.wood@talktalk.net> wrote:
> On 18/01/18 15:35, Johannes Schindelin wrote:
>>
>> This patch is part of the effort to reimplement `--preserve-merges` with
>> a substantially improved design, a design that has been developed in the
>> Git for Windows project to maintain the dozens of Windows-specific patch
>> series on top of upstream Git.
>>
>> The previous patch implemented the `label`, `bud` and `reset` commands
>> to label commits and to reset to a labeled commits. This patch adds the
>> `merge` command, with the following syntax:
>>
>>       merge <commit> <rev> <oneline>
>
> I'm concerned that this will be confusing for users. All of the other
> rebase commands replay the changes in the commit hash immediately
> following the command name. This command instead uses the first commit
> to specify the message which is different to both 'git merge' and the
> existing rebase commands. I wonder if it would be clearer to have 'merge
> -C <commit> <rev> ...' instead so it's clear which argument specifies
> the message and which the remote head to merge. It would also allow for
> 'merge -c <commit> <rev> ...' in the future for rewording an existing
> merge message and also avoid the slightly odd 'merge - <rev> ...'. Where
> it's creating new merges I'm not sure it's a good idea to encourage
> people to only have oneline commit messages by making it harder to edit
> them, perhaps it could take another argument to mean open the editor or
> not, though as Jake said I guess it's not that common.

I actually like the idea of re-using commit message options like -C,
-c,  and -m, so we could do:

merge -C <commit> ... to take message from commit
merge -c <commit> ...  to take the message from commit and open editor to edit
merge -m "<message>" ... to take the message from the quoted test
merge ... to merge and open commit editor with default message

This also, I think, allows us to not need to put the oneline on the
end, meaning we wouldn't have to quote the parent commit arguments
since we could use option semantics?

>
> One thought that just struck me - if a merge or reset command specifies
> an invalid label is it rescheduled so that it's still in the to-do list
> when the user edits it after rebase stops?
>
> In the future it might be nice if the label, reset and merge commands
> were validated when the to-do list is parsed so that the user gets
> immediate feedback if they try to create a label that is not a valid ref
> name or that they have a typo in a name given to reset or merge rather
> than the rebase stopping later.
>

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 1/8] sequencer: introduce new commands to reset the revision
  2018-01-18 16:25   ` Jacob Keller
  2018-01-18 21:13     ` Johannes Schindelin
  2018-01-18 21:24     ` Philip Oakley
@ 2018-01-22 21:25     ` Junio C Hamano
  2018-01-29 22:00       ` Johannes Schindelin
  2 siblings, 1 reply; 276+ messages in thread
From: Junio C Hamano @ 2018-01-22 21:25 UTC (permalink / raw)
  To: Jacob Keller; +Cc: Johannes Schindelin, Git mailing list

Jacob Keller <jacob.keller@gmail.com> writes:

> The code looks good, but I'm a little wary of adding bud which
> hard-codes a specific label. I suppose it does grant a bit of
> readability to the resulting script... ? It doesn't seem that
> important compared to use using "reset onto"? At least when
> documenting this it should be made clear that the "onto" label is
> special.

I do not think we would mind "bud" too much in the end result, but
the change in 1/8 is made harder to read than necessary with it.  It
is the only thing that needs "a single-letter command name may now
not have any argument after it" change to the parser among the three
things being added here, and it also needs to be added to the list
of special commands without arguments.

It would have been easier to reason about if addition of "bud" was
in its own patch done after label and reset are added.  And if done
as a separate step, perhaps it would have been easier to realize
that it would be a more future-proof solution for handling the
"special" ness of BUD to add a new "unsigned flags" word to
todo_command_info[] structure and using a bit that says "this does
not take an arg" than to hardcode "noop and bud are the commands
without args" in the code.  That hardcode was good enough when there
was only one thing in that special case.  Now it has two.

In a similar way, the code to special case label and reset just like
exec may also want to become more table driven, perhaps using
another bit in the same new flags word to say "this does not refer
to commit".  I think that can become [v2 1/N] while addition of "bud"
can be [v2 2/N] (after all, "bud" just does the same do_reset() with
hardcoded argument, so "label/reset" must come first).





^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 2/8] sequencer: introduce the `merge` command
  2018-01-18 15:35 ` [PATCH 2/8] sequencer: introduce the `merge` command Johannes Schindelin
                     ` (2 preceding siblings ...)
  2018-01-19 14:45   ` Phillip Wood
@ 2018-01-22 22:12   ` Junio C Hamano
  2018-01-29 22:15     ` Johannes Schindelin
  3 siblings, 1 reply; 276+ messages in thread
From: Junio C Hamano @ 2018-01-22 22:12 UTC (permalink / raw)
  To: Johannes Schindelin; +Cc: git, Jacob Keller

Johannes Schindelin <johannes.schindelin@gmx.de> writes:

>  	end_of_object_name = (char *) bol + strcspn(bol, " \t\n");
> +	item->arg = end_of_object_name + strspn(end_of_object_name, " \t");
> +	item->arg_len = (int)(eol - item->arg);
> +
>  	saved = *end_of_object_name;
> +	if (item->command == TODO_MERGE && *bol == '-' &&
> +	    bol + 1 == end_of_object_name) {
> +		item->commit = NULL;
> +		return 0;
> +	}
> +
>  	*end_of_object_name = '\0';
>  	status = get_oid(bol, &commit_oid);
>  	*end_of_object_name = saved;
>  
> -	item->arg = end_of_object_name + strspn(end_of_object_name, " \t");
> -	item->arg_len = (int)(eol - item->arg);
> -

Assigning to "saved" before the added "if we are doing merge and see
'-', do this special thing" is not only unnecessary, but makes the
logic in the non-special case harder to read.  The four things
"saved = *eol; *eol = 0; do_thing_using(bol); *eol = saved;" is a
single logical unit; keep them together.

This hunk may have been the most expedient way to coax "-" into the
location where a commit object name is expected; it looks ugly, but
for the limited purpose of this series it should do.

> @@ -2069,6 +2077,132 @@ static int do_reset(const char *name, int len)
>  	return ret;
>  }
>  
> +static int do_merge(struct commit *commit, const char *arg, int arg_len,
> +		    struct replay_opts *opts)
> +{
> +	int merge_arg_len;
> +	struct strbuf ref_name = STRBUF_INIT;
> +	struct commit *head_commit, *merge_commit, *i;
> +	struct commit_list *common, *j, *reversed = NULL;
> +	struct merge_options o;
> +	int ret;
> +	static struct lock_file lock;
> +
> +	for (merge_arg_len = 0; merge_arg_len < arg_len; merge_arg_len++)
> +		if (isspace(arg[merge_arg_len]))
> +			break;

Mental note: this scans for a whitespace, and tab is accepted
instead of SP, which presumably is to allow human typed string.

> +	if (hold_locked_index(&lock, LOCK_REPORT_ON_ERROR) < 0)
> +		return -1;
> +
> +	if (commit) {
> +		const char *message = get_commit_buffer(commit, NULL);
> +		const char *body;
> +		int len;
> +
> +		if (!message) {
> +			rollback_lock_file(&lock);
> +			return error(_("could not get commit message of '%s'"),
> +				     oid_to_hex(&commit->object.oid));
> +		}
> +		write_author_script(message);
> +		find_commit_subject(message, &body);
> +		len = strlen(body);
> +		if (write_message(body, len, git_path_merge_msg(), 0) < 0) {
> +			error_errno(_("Could not write '%s'"),
> +				    git_path_merge_msg());
> +			unuse_commit_buffer(commit, message);
> +			rollback_lock_file(&lock);
> +			return -1;
> +		}
> +		unuse_commit_buffer(commit, message);
> +	} else {
> +		const char *p = arg + merge_arg_len;
> +		struct strbuf buf = STRBUF_INIT;
> +		int len;
> +
> +		strbuf_addf(&buf, "author %s", git_author_info(0));
> +		write_author_script(buf.buf);
> +		strbuf_reset(&buf);
> +
> +		p += strspn(p, " \t");

... and this matches the above mental note.  It allows consecutive
whitespaces as a separator, which is sensible behaviour.

> +		if (*p)
> +			len = strlen(p);
> +		else {
> +			strbuf_addf(&buf, "Merge branch '%.*s'",
> +				    merge_arg_len, arg);
> +			p = buf.buf;
> +			len = buf.len;
> +		}

So... "arg" received by this function can be a single non-whitespace
token, which is taken as the name of the branch being merged (in
this else clause).  Or it can also be followed by a single liner
message for the merge commit.  Presumably, this is for creating a
new merge (i.e. "commit==NULL" case), and preparing a proper log
message in the todo list is unrealistic, so this would be a
reasonable compromise.  Those users who want to write proper log
message could presumably follow such "merge" insn with a "x git
commit --amend" or something, I presume, if they really wanted to.

> +		if (write_message(p, len, git_path_merge_msg(), 0) < 0) {
> +			error_errno(_("Could not write '%s'"),
> +				    git_path_merge_msg());
> +			strbuf_release(&buf);
> +			rollback_lock_file(&lock);
> +			return -1;
> +		}
> +		strbuf_release(&buf);
> +	}

OK.  Now we have prepared the MERGE_MSG file and are ready to commit.

> +	head_commit = lookup_commit_reference_by_name("HEAD");
> +	if (!head_commit) {
> +		rollback_lock_file(&lock);
> +		return error(_("Cannot merge without a current revision"));
> +	}

Hmph, I would have expected to see this a lot earlier, before
dealing with the log message.  Leftover MERGE_MSG file after an
error will cause unexpected fallout to the end-user experience
(including what is shown by the shell prompt scripts), but if we do
this before the MERGE_MSG thing, we do not have to worry about
error codepath having to remove it.

> +	strbuf_addf(&ref_name, "refs/rewritten/%.*s", merge_arg_len, arg);
> +	merge_commit = lookup_commit_reference_by_name(ref_name.buf);
> +	if (!merge_commit) {
> +		/* fall back to non-rewritten ref or commit */
> +		strbuf_splice(&ref_name, 0, strlen("refs/rewritten/"), "", 0);
> +		merge_commit = lookup_commit_reference_by_name(ref_name.buf);
> +	}

OK, so "abc" in the example in the log message is looked up first as
a label and then we take a fallback to interpret as an object name.

Hopefully allowed names in "label" would be documented clearly in
later steps (I am guessing that "a name that can be used as a branch
name can be used as a label name and vice versa" or something like
that).

> +	if (!merge_commit) {
> +		error(_("could not resolve '%s'"), ref_name.buf);
> +		strbuf_release(&ref_name);
> +		rollback_lock_file(&lock);
> +		return -1;
> +	}
> +	write_message(oid_to_hex(&merge_commit->object.oid), GIT_SHA1_HEXSZ,
> +		      git_path_merge_head(), 0);
> +	write_message("no-ff", 5, git_path_merge_mode(), 0);

These two calls gave me a "Huh?" moment; write_message() sounds like
it is allowed to be later updated with frills suitable for *_MSG
files we place in .git/ directory (iow, it is in principle OK if
commented out instructions common to these files are added to the
output by the function), but these want exact bytes passed in the
result, for which wrapper.c::write_file() is more appropriate.

Alternatively, perhaps write_message() can be dropped and its
callers can call wrapper.c::write_file() instead?  Such a clean-up
may require teaching the append-eol thing that write_message() wants
to wrapper.c::write_file(), but it shouldn't be a rocket science.

> +	common = get_merge_bases(head_commit, merge_commit);
> +	for (j = common; j; j = j->next)
> +		commit_list_insert(j->item, &reversed);
> +	free_commit_list(common);

I know this is copy&pasted code from "builtin/merge.c", but is there
a reason to reverse the common ancestor list here?

> +	read_cache();
> +	init_merge_options(&o);
> +	o.branch1 = "HEAD";
> +	o.branch2 = ref_name.buf;
> +	o.buffer_output = 2;
> +
> +	ret = merge_recursive(&o, head_commit, merge_commit, reversed, &i);
> +	if (ret <= 0)
> +		fputs(o.obuf.buf, stdout);
> +...

Other than these minor nits, looks quite promising.  Nicely done.

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 3/8] sequencer: fast-forward merge commits, if possible
  2018-01-18 15:35 ` [PATCH 3/8] sequencer: fast-forward merge commits, if possible Johannes Schindelin
  2018-01-19 14:53   ` Phillip Wood
@ 2018-01-23 18:51   ` Junio C Hamano
  2018-01-29 22:18     ` Johannes Schindelin
  1 sibling, 1 reply; 276+ messages in thread
From: Junio C Hamano @ 2018-01-23 18:51 UTC (permalink / raw)
  To: Johannes Schindelin; +Cc: git, Jacob Keller

Johannes Schindelin <johannes.schindelin@gmx.de> writes:

> +	/*
> +	 * If HEAD is not identical to the parent of the original merge commit,
> +	 * we cannot fast-forward.
> +	 */
> +	can_fast_forward = commit && commit->parents &&
> +		!oidcmp(&commit->parents->item->object.oid,
> +			&head_commit->object.oid);
> +

I think this expression and assignment should better be done much
later.  Are you going to update commit, commit->parents, etc. that
are involved in the computation in the meantime???

>  	strbuf_addf(&ref_name, "refs/rewritten/%.*s", merge_arg_len, arg);
>  	merge_commit = lookup_commit_reference_by_name(ref_name.buf);
>  	if (!merge_commit) {
> @@ -2164,6 +2172,17 @@ static int do_merge(struct commit *commit, const char *arg, int arg_len,
>  		rollback_lock_file(&lock);
>  		return -1;
>  	}
> +
> +	if (can_fast_forward && commit->parents->next &&
> +	    !commit->parents->next->next &&
> +	    !oidcmp(&commit->parents->next->item->object.oid,
> +		    &merge_commit->object.oid)) {

... Namely, here.  Because the earlier one is computing "are we
replaying exactly the same commit on top of exactly the same
state?", which is merely one half of "can we fast-forward", and
storing it in a variable whose name is over-promising way before it
becomes necessary.  The other half of "can we fast-forward?" logic
is the remainder of the if() condition we see above.  IOW, when
fully spelled, this code can fast-forward when we are replaying a
commit on top of exactly the same first-parent and the commit being
replayed is a single parent merge.

We may even want to get rid of can_fast_forward variable.

> +		strbuf_release(&ref_name);
> +		rollback_lock_file(&lock);
> +		return fast_forward_to(&commit->object.oid,
> +				       &head_commit->object.oid, 0, opts);
> +	}
> +
>  	write_message(oid_to_hex(&merge_commit->object.oid), GIT_SHA1_HEXSZ,
>  		      git_path_merge_head(), 0);
>  	write_message("no-ff", 5, git_path_merge_mode(), 0);

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 3/8] sequencer: fast-forward merge commits, if possible
  2018-01-19 14:53   ` Phillip Wood
@ 2018-01-23 19:12     ` Junio C Hamano
  2018-01-24 10:32       ` Phillip Wood
  2018-01-29 21:47     ` Johannes Schindelin
  1 sibling, 1 reply; 276+ messages in thread
From: Junio C Hamano @ 2018-01-23 19:12 UTC (permalink / raw)
  To: Phillip Wood; +Cc: Johannes Schindelin, git, Jacob Keller

Phillip Wood <phillip.wood@talktalk.net> writes:

> On 18/01/18 15:35, Johannes Schindelin wrote:
>> 
>> Just like with regular `pick` commands, if we are trying to recreate a
>> merge commit, we now test whether the parents of said commit match HEAD
>> and the commits to be merged, and fast-forward if possible.
>> 
>> This is not only faster, but also avoids unnecessary proliferation of
>> new objects.
>
> I might have missed something but shouldn't this be checking opts->allow_ff?

Because the whole point of this mechanism is to recreate the
topology faithfully to the original, even if the original was a
redundant merge (which has a side parent that is an ancestor or a
descendant of the first parent), we should just point at the
original merge when the condition allows it, regardless of
opts->allow_ff.

I think it is a different matter if an insn to create a new merge
(i.e. "merge - <parent> <message>", not "merge <commit> <parent>")
should honor opts->allow_ff; because it is not about recreating an
existing history but is a way to create what did not exist before,
I think it is sensible if allow_ff option is honored.


^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 4/8] rebase-helper --make-script: introduce a flag to recreate merges
  2018-01-18 15:35 ` [PATCH 4/8] rebase-helper --make-script: introduce a flag to recreate merges Johannes Schindelin
  2018-01-18 21:39   ` Philip Oakley
  2018-01-19 10:34   ` Eric Sunshine
@ 2018-01-23 20:03   ` Junio C Hamano
  2018-01-29 22:37     ` Johannes Schindelin
  2 siblings, 1 reply; 276+ messages in thread
From: Junio C Hamano @ 2018-01-23 20:03 UTC (permalink / raw)
  To: Johannes Schindelin; +Cc: git, Jacob Keller

Johannes Schindelin <johannes.schindelin@gmx.de> writes:

> The sequencer just learned a new commands intended to recreate branch

s/a //;

> structure (similar in spirit to --preserve-merges, but with a
> substantially less-broken design).
> ...
> @@ -2785,6 +2787,335 @@ void append_signoff(struct strbuf *msgbuf, int ignore_footer, unsigned flag)
>  	strbuf_release(&sob);
>  }
>  
> +struct labels_entry {
> +	struct hashmap_entry entry;
> +	char label[FLEX_ARRAY];
> +};
> +
> +static int labels_cmp(const void *fndata, const struct labels_entry *a,
> +		      const struct labels_entry *b, const void *key)
> +{
> +	return key ? strcmp(a->label, key) : strcmp(a->label, b->label);
> +}

label_oid() accesses state->labels hash using strihash() as the hash
function, but the final comparison between the entries in the same
hash buckets are done with case sensitivity.  It is unclear to me if
that is what was intended, and why.

> +struct string_entry {
> +	struct oidmap_entry entry;
> +	char string[FLEX_ARRAY];
> +};
> +
> +struct label_state {
> +	struct oidmap commit2label;
> +	struct hashmap labels;
> +	struct strbuf buf;
> +};
> +
> +static const char *label_oid(struct object_id *oid, const char *label,
> +			     struct label_state *state)
> +{
> +	struct labels_entry *labels_entry;
> +	struct string_entry *string_entry;
> +	struct object_id dummy;
> +	size_t len;
> +	int i;
> +
> +	string_entry = oidmap_get(&state->commit2label, oid);
> +	if (string_entry)
> +		return string_entry->string;
> +
> +	/*
> +	 * For "uninteresting" commits, i.e. commits that are not to be
> +	 * rebased, and which can therefore not be labeled, we use a unique
> +	 * abbreviation of the commit name. This is slightly more complicated
> +	 * than calling find_unique_abbrev() because we also need to make
> +	 * sure that the abbreviation does not conflict with any other
> +	 * label.
> +	 *
> +	 * We disallow "interesting" commits to be labeled by a string that
> +	 * is a valid full-length hash, to ensure that we always can find an
> +	 * abbreviation for any uninteresting commit's names that does not
> +	 * clash with any other label.
> +	 */
> +	if (!label) {
> +		char *p;
> +
> +		strbuf_reset(&state->buf);
> +		strbuf_grow(&state->buf, GIT_SHA1_HEXSZ);
> +		label = p = state->buf.buf;
> +
> +		find_unique_abbrev_r(p, oid->hash, default_abbrev);
> +
> +		/*
> +		 * We may need to extend the abbreviated hash so that there is
> +		 * no conflicting label.
> +		 */
> +		if (hashmap_get_from_hash(&state->labels, strihash(p), p)) {
> +			size_t i = strlen(p) + 1;
> +
> +			oid_to_hex_r(p, oid);
> +			for (; i < GIT_SHA1_HEXSZ; i++) {
> +				char save = p[i];
> +				p[i] = '\0';
> +				if (!hashmap_get_from_hash(&state->labels,
> +							   strihash(p), p))
> +					break;
> +				p[i] = save;
> +			}
> +		}

If oid->hash required full 40-hex to disambiguate, then
find-unique-abbrev would give 40-hex and we'd want the same "-<num>"
suffix technique employed below to make it consistently unique.  I
wonder if organizing the function this way ...

	if (!label)
		label = oid-to-hex(oid);

	if (label already exists or full oid) {
		make it unambiguous;
	}

... allows the resulting code easier to understand and manage.

A related tangent.  Does an auto-label given to "uninteresting"
commit need to be visible to end users?  I doubted it and that is
why I said oid-to-hex in the above, but if it is given to end users,
use of find-unique-abbrev-r is perfectly fine.

> +static int make_script_with_merges(struct pretty_print_context *pp,
> +				   struct rev_info *revs, FILE *out,
> +				   unsigned flags)
> +{
> + ...
> +	int abbr = flags & TODO_LIST_ABBREVIATE_CMDS;
> +	const char *p = abbr ? "p" : "pick", *l = abbr ? "l" : "label",
> +		 *t = abbr ? "t" : "reset", *b = abbr ? "b" : "bud",
> +		 *m = abbr ? "m" : "merge";

It would be easier to understand if these short named variables are
reserved only for temporary use, not as constants.  It is not too
much to spell 

	fprintf(out, "%s onto\n", cmd_label);

than

	fprintf(out, "%s onto\n", l);

and would save readers from head-scratching, wondering where the
last assignment to variable "l" is.

> +
> +	oidmap_init(&commit2todo, 0);
> +	oidmap_init(&state.commit2label, 0);
> +	hashmap_init(&state.labels, (hashmap_cmp_fn) labels_cmp, NULL, 0);
> +	strbuf_init(&state.buf, 32);
> +
> +	if (revs->cmdline.nr && (revs->cmdline.rev[0].flags & BOTTOM)) {
> +		struct object_id *oid = &revs->cmdline.rev[0].item->oid;
> +		FLEX_ALLOC_STR(entry, string, "onto");
> +		oidcpy(&entry->entry.oid, oid);
> +		oidmap_put(&state.commit2label, entry);
> +	}
> +
> +	/*
> +	 * First phase:
> +	 * - get onelines for all commits
> +	 * - gather all branch tips (i.e. 2nd or later parents of merges)
> +	 * - label all branch tips
> +	 */

When an early part of a branch is merged and then the remaining part
of the same branch is merged again, "branch tip" and "2nd or later
parents of merges" would become different concepts.  The 2nd parent
of an early merge is not among the branch tips.

For the purpose of the "recreate the topology" algorithm, I am
imagining that you would need not just the tips but all the 2nd and
subsequent parents of merges, and my quick skimming tells me that
the following code grabs them correctly.

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 4/8] rebase-helper --make-script: introduce a flag to recreate merges
  2018-01-19 10:34   ` Eric Sunshine
@ 2018-01-23 20:13     ` Junio C Hamano
  2018-01-29 21:07       ` Johannes Schindelin
  2018-01-29 21:05     ` Johannes Schindelin
  1 sibling, 1 reply; 276+ messages in thread
From: Junio C Hamano @ 2018-01-23 20:13 UTC (permalink / raw)
  To: Eric Sunshine; +Cc: Johannes Schindelin, Git List, Jacob Keller

Eric Sunshine <sunshine@sunshineco.com> writes:

>> +               is_octopus = to_merge && to_merge->next;
>> +
>> +               if (is_octopus)
>> +                       BUG("Octopus merges not yet supported");
>
> Is this a situation which the end-user can trigger by specifying a
> merge with more than two parents? If so, shouldn't this be just a
> normal error message rather than a (developer) bug message? Or, am I
> misunderstanding?

BUG() is "we wrote code carefully so that this should not trigger;
we do not _expect_ the code to reach here".  This one is expected to
trigger, and I agree with you that it should be die(), if the series
is meant to be released to the general public in the current form
(i.e. until the limitation is lifted so that it can handle an
octopus).

If the callers are made more careful to check if there is an octopus
involved and reject the request early, then seeing an octopus in
this location in a loop will become a BUG().

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 5/8] rebase: introduce the --recreate-merges option
  2018-01-18 15:35 ` [PATCH 5/8] rebase: introduce the --recreate-merges option Johannes Schindelin
  2018-01-19 10:55   ` Eric Sunshine
@ 2018-01-23 20:22   ` Junio C Hamano
  2018-02-10 19:31     ` Johannes Schindelin
  2018-02-07  6:16   ` Sergey Organov
  2018-02-09  6:50   ` Sergey Organov
  3 siblings, 1 reply; 276+ messages in thread
From: Junio C Hamano @ 2018-01-23 20:22 UTC (permalink / raw)
  To: Johannes Schindelin; +Cc: git, Jacob Keller

Johannes Schindelin <johannes.schindelin@gmx.de> writes:

> diff --git a/Documentation/git-rebase.txt b/Documentation/git-rebase.txt
> index 8a861c1e0d6..1d061373288 100644
> --- a/Documentation/git-rebase.txt
> +++ b/Documentation/git-rebase.txt
> @@ -368,6 +368,11 @@ The commit list format can be changed by setting the configuration option
>  rebase.instructionFormat.  A customized instruction format will automatically
>  have the long commit hash prepended to the format.
>  
> +--recreate-merges::
> +	Recreate merge commits instead of flattening the history by replaying
> +	merges. Merge conflict resolutions or manual amendments to merge
> +	commits are not preserved.
> +

It is sensible to postpone tackling "evil merges" in this initial
iteration of the series, and "manual amendments ... not preserved"
is a reasonable thing to document.  But do we want to say a bit more
about conflicting merges?  "conflict resolutions ... not preserved"
sounds as if it does not stop and instead record the result with
conflict markers without even letting rerere to kick in, which
certainly is not the impression you wanted to give to the readers.

I am imagining that it will stop and give control back to the end
user just like a conflicted "pick" would, and allow "rebase
--continue" to record resolution from the working tree, and just
like conflicted "pick", it would allow rerere() to help end users
recall previous resolution.


^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 6/8] sequencer: handle autosquash and post-rewrite for merge commands
  2018-01-18 16:43   ` Jacob Keller
  2018-01-18 21:27     ` Johannes Schindelin
@ 2018-01-23 20:27     ` Junio C Hamano
  1 sibling, 0 replies; 276+ messages in thread
From: Junio C Hamano @ 2018-01-23 20:27 UTC (permalink / raw)
  To: Jacob Keller; +Cc: Johannes Schindelin, Git mailing list

Jacob Keller <jacob.keller@gmail.com> writes:

>>  static int is_per_worktree_ref(const char *refname)
>>  {
>>         return !strcmp(refname, "HEAD") ||
>> -               starts_with(refname, "refs/bisect/");
>> +               starts_with(refname, "refs/bisect/") ||
>> +               starts_with(refname, "refs/rewritten/");
>>  }
>
> Would this part make more sense to move into the commit that
> introduces writing these refs, or does it only matter once you start
> this step here?

Good spotting.  I too was wondering about multiple worktrees when I
saw the "label" thing introduced.  It probably makes sense to move
this to that step.

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 0/8] rebase -i: offer to recreate merge commits
  2018-01-18 15:35 [PATCH 0/8] rebase -i: offer to recreate merge commits Johannes Schindelin
                   ` (10 preceding siblings ...)
  2018-01-19 20:25 ` [PATCH 0/8] rebase -i: offer to recreate merge commits Junio C Hamano
@ 2018-01-23 20:29 ` Junio C Hamano
  2018-01-29 22:53   ` Johannes Schindelin
  2018-01-29 22:54 ` [PATCH v2 00/10] " Johannes Schindelin
  12 siblings, 1 reply; 276+ messages in thread
From: Junio C Hamano @ 2018-01-23 20:29 UTC (permalink / raw)
  To: Johannes Schindelin; +Cc: git, Jacob Keller

Johannes Schindelin <johannes.schindelin@gmx.de> writes:

> My original attempt was --preserve-merges, but that design was so
> limited that I did not even enable it in interactive mode.
> ...
> There are more patches in the pipeline, based on this patch series, but
> left for later in the interest of reviewable patch series: one mini
> series to use the sequencer even for `git rebase -i --root`, and another
> one to add support for octopus merges to --recreate-merges.

I left comments on a handful of them, but I do not think any of them
spotted a grave design issue to be a show stopper.  Overall, the
series was quite a pleasant read, even with those minor nits and
rooms for improvements.

Thanks.

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 3/8] sequencer: fast-forward merge commits, if possible
  2018-01-23 19:12     ` Junio C Hamano
@ 2018-01-24 10:32       ` Phillip Wood
  2018-01-24 18:51         ` Junio C Hamano
  0 siblings, 1 reply; 276+ messages in thread
From: Phillip Wood @ 2018-01-24 10:32 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: Johannes Schindelin, git, Jacob Keller

On 23/01/18 19:12, Junio C Hamano wrote:
> Phillip Wood <phillip.wood@talktalk.net> writes:
> 
>> On 18/01/18 15:35, Johannes Schindelin wrote:
>>>
>>> Just like with regular `pick` commands, if we are trying to recreate a
>>> merge commit, we now test whether the parents of said commit match HEAD
>>> and the commits to be merged, and fast-forward if possible.
>>>
>>> This is not only faster, but also avoids unnecessary proliferation of
>>> new objects.
>>
>> I might have missed something but shouldn't this be checking opts->allow_ff?
> 
> Because the whole point of this mechanism is to recreate the
> topology faithfully to the original, even if the original was a
> redundant merge (which has a side parent that is an ancestor or a
> descendant of the first parent), we should just point at the
> original merge when the condition allows it, regardless of
> opts->allow_ff.

I agree that the merge should be recreated, but I was thinking of 
something slightly different. Currently the sequencer uses 
opts->allow_ff to control whether a new commit with the same contents 
should be created even if the existing one could be reused. So I was 
querying whether we should recreate the commit when the user run 'git 
rebase --recreate-merges --no-ff' rather than just reusing it. As merges 
also have another meaning for fast-forward the terminology gets confusing.

> I think it is a different matter if an insn to create a new merge
> (i.e. "merge - <parent> <message>", not "merge <commit> <parent>")
> should honor opts->allow_ff; because it is not about recreating an
> existing history but is a way to create what did not exist before,
> I think it is sensible if allow_ff option is honored.

This is the merge sense of 'fast-forward' not the existing sequencer 
sense, without thinking about it more I'm not sure if one command line 
option for rebase is sufficient to cover both uses.

Best Wishes

Phillip


^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 3/8] sequencer: fast-forward merge commits, if possible
  2018-01-24 10:32       ` Phillip Wood
@ 2018-01-24 18:51         ` Junio C Hamano
  0 siblings, 0 replies; 276+ messages in thread
From: Junio C Hamano @ 2018-01-24 18:51 UTC (permalink / raw)
  To: Phillip Wood; +Cc: Johannes Schindelin, git, Jacob Keller

Phillip Wood <phillip.wood@talktalk.net> writes:

> I agree that the merge should be recreated, but I was thinking of
> something slightly different. Currently the sequencer uses
> opts->allow_ff to control whether a new commit with the same contents
> should be created even if the existing one could be reused.

Ahh, OK.  I misunderstood what you meant.  Yes, what you said makes
sense to me.

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 1/8] sequencer: introduce new commands to reset the revision
  2018-01-19  8:59   ` Eric Sunshine
@ 2018-01-24 22:01     ` Junio C Hamano
  2018-01-29 20:55       ` Johannes Schindelin
  2018-01-29 20:50     ` Johannes Schindelin
  1 sibling, 1 reply; 276+ messages in thread
From: Junio C Hamano @ 2018-01-24 22:01 UTC (permalink / raw)
  To: Eric Sunshine; +Cc: Johannes Schindelin, Git List, Jacob Keller

Eric Sunshine <sunshine@sunshineco.com> writes:

>> +static int do_reset(const char *name, int len)
>> +{
>> +       [...]
>> +       if (hold_locked_index(&lock, LOCK_REPORT_ON_ERROR) < 0)
>> +               return -1;
>> +
>> +       for (i = 0; i < len; i++)
>> +               if (isspace(name[i]))
>> +                       len = i;
>
> What is the purpose of this loop? I could imagine that it's trying to
> strip all whitespace from the end of 'name', however, to do that it
> would iterate backward, not forward. (Or perhaps it's trying to
> truncate at the first space, but then it would need to invert the
> condition or use 'break'.) Am I missing something obvious?

I must be missing the same thing.  Given that the callers of
do_reset(), other than the "bug" thing that passes the hard coded
"onto", uses item->arg/item->arg_len which includes everything after
the insn word on the line in the todo list, I do suspect that the
intention is to stop at the first whitespace char to avoid creating
a ref with whitespace in it, i.e. it is a bug that can be fixed with
s/len = i/break/.

The code probably should further check the resulting string with
check_ref_format() to detect strange chars and char sequences that
make the resulting refname invalid.  For example, you would not want
to allow a label with two consecutive periods in it.



^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 9/8] [DO NOT APPLY, but squash?] git-rebase--interactive: clarify arguments
  2018-01-20  9:14         ` Jacob Keller
@ 2018-01-29 17:02           ` Johannes Schindelin
  0 siblings, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-01-29 17:02 UTC (permalink / raw)
  To: Jacob Keller; +Cc: Junio C Hamano, Stefan Beller, Git mailing list

Hi,

On Sat, 20 Jan 2018, Jacob Keller wrote:

> On Fri, Jan 19, 2018 at 12:30 PM, Junio C Hamano <gitster@pobox.com> wrote:
> > Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:
> >
> >> Good idea! I would rather do it as an introductory patch (that only
> >> converts the existing list).
> >>
> >> As to `merge`: it is a bit more complicated ;-)
> >>
> >>       m, merge <original-merge-commit> ( <label> | "<label>..." ) [<oneline>]
> >>               create a merge commit using the original merge commit's
> >>               message (or the oneline, if "-" is given). Use a quoted
> >>               list of commits to be merged for octopus merges.
> >
> > Is it just the message that is being reused?
> >
> > Aren't the trees of the original commit and its parents participate
> > in creating the tree of the recreated merge?  One way to preserve an
> > originally evil merge is to notice how it was made by taking the
> > difference between the result of mechanical merge of original merge
> > parents and the original merge result, and carry it forward when
> > recreating the merge across new parents.  Just being curious.
> >
> 
> It looks like currently that only the commit is kept, with no attempt
> to recreate evil merges.

Yep. I even documented that somewhere ;-)

Ciao,
Dscho

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 1/8] sequencer: introduce new commands to reset the revision
  2018-01-18 21:24     ` Philip Oakley
  2018-01-18 21:28       ` Jacob Keller
@ 2018-01-29 20:28       ` Johannes Schindelin
  1 sibling, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-01-29 20:28 UTC (permalink / raw)
  To: Philip Oakley; +Cc: Jacob Keller, Git mailing list, Junio C Hamano

Hi Philip,

On Thu, 18 Jan 2018, Philip Oakley wrote:

> From: "Jacob Keller" <jacob.keller@gmail.com>
> > On Thu, Jan 18, 2018 at 7:35 AM, Johannes Schindelin
> > <johannes.schindelin@gmx.de> wrote:
> > > This commit implements the commands to label, and to reset to, given
> > > revisions. The syntax is:
> > >
> > >         label <name>
> > >         reset <name>
> > >
> > > As a convenience shortcut, also to improve readability of the generated
> > > todo list, a third command is introduced: bud. It simply resets to the
> > > "onto" revision, i.e. the commit onto which we currently rebase.
> > >
> >
> > The code looks good, but I'm a little wary of adding bud which
> > hard-codes a specific label. I suppose it does grant a bit of
> > readability to the resulting script... ? It doesn't seem that
> > important compared to use using "reset onto"? At least when
> > documenting this it should be made clear that the "onto" label is
> > special.
> >
> > Thanks,
> > Jake.
> 
> I'd agree.
> 
> The special 'onto' label should be fully documented, and the commit message
> should indicate which patch actually defines it (and all its corner cases and
> fall backs if --onto isn't explicitly given..)

I hoped that the example todo lists would clarify that 'onto' is just the
name of the first label:

	label onto

is *literally* how all of those todo lists start. And that is why there
are no possible concerns about any missing `--onto` argument: that
argument is irrelevant for that label.

Maybe it is just a bad name. Maybe `START` would be a better label.

What do you think?

> Likewise the choice of 'bud' should be explained with some nice
> phraseology indicating that we are growing the new flowering from the
> bud, otherwise the word is a bit too short and sudden for easy
> explanation.

I dropped the `bud` command.

Ciao,
Dscho

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 8/8] rebase -i: introduce --recreate-merges=no-rebase-cousins
  2018-01-18 22:00   ` Philip Oakley
@ 2018-01-29 20:42     ` Johannes Schindelin
  0 siblings, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-01-29 20:42 UTC (permalink / raw)
  To: Philip Oakley; +Cc: git, Junio C Hamano, Jacob Keller

Hi Philip,

On Thu, 18 Jan 2018, Philip Oakley wrote:

> From: "Johannes Schindelin" <johannes.schindelin@gmx.de>
> > This one is a bit tricky to explain, so let's try with a diagram:
> >
> >        C
> >      /   \
> > A - B - E - F
> >  \   /
> >    D
> >
> > To illustrate what this new mode is all about, let's consider what
> > happens upon `git rebase -i --recreate-merges B`, in particular to
> > the commit `D`. In the default mode, the new branch structure is:
> >
> >      --- C' --
> >      /         \
> > A - B ------ E' - F'
> >      \    /
> >        D'
> >
> > This is not really preserving the branch topology from before! The
> > reason is that the commit `D` does not have `B` as ancestor, and
> > therefore it gets rebased onto `B`.
> >
> > However, when recreating branch structure, there are legitimate use
> > cases where one might want to preserve the branch points of commits that
> > do not descend from the <upstream> commit that was passed to the rebase
> > command, e.g. when a branch from core Git's `next` was merged into Git
> > for Windows' master we will not want to rebase those commits on top of a
> > Windows-specific commit. In the example above, the desired outcome would
> > look like this:
> >
> >      --- C' --
> >      /         \
> > A - B ------ E' - F'
> >  \        /
> >   -- D' --
> 
> I'm not understanding this. I see that D properly starts from A, but
> don't see why it is now D'. Surely it's unchanged.

It is not necessarily unchanged, because this is an *interactive* rebase.
If you mark `D` for `reword`, for example, it may be changed.

I use the label D' in the mathematical sense, to indicate that D' is
derived from D. It may even be identical to D, but the point is that it is
in the todo list of the interactive rebase, so it can be changed. As
opposed to, say, A and B. Those cannot be changed in this interactive
rebase.

> Maybe it's the arc/node confusion. Maybe even spell out that the rebased
> commits from the command are B..HEAD, but that includes D, which may not
> be what folk had expected. (not even sure if the reflog comes into
> determining merge-bases here..)
> 
> I do think an exact definition is needed (e.g. via --ancestry-path or
> its equivalent?).

I don't find "ancestry path" any more intuitive a term than the
mathematically correct "uncomparable".

If you have a better way to explain this (without devolving into
mathematical terminology), please let's hear it.

Don't get me wrong, as a mathematician I am comfortable with very precise
descriptions involving plenty of Greek symbols.

But this documentation, and these commit messages do not target myself. I
know perfectly well what I am talking about here. The target audience are
software developers who may not have a background in mathematics, who do
not even want to fully understand what the heck constitutes a Directed
Acyclic Graph.

So what we need here is plain English. And I had thought that the analogy
with the family tree would be intuitive enough for even math haters to
understand easily and quickly...

Ciao,
Dscho

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 1/8] sequencer: introduce new commands to reset the revision
  2018-01-19  8:59   ` Eric Sunshine
  2018-01-24 22:01     ` Junio C Hamano
@ 2018-01-29 20:50     ` Johannes Schindelin
  2018-01-30  7:12       ` Eric Sunshine
  1 sibling, 1 reply; 276+ messages in thread
From: Johannes Schindelin @ 2018-01-29 20:50 UTC (permalink / raw)
  To: Eric Sunshine; +Cc: Git List, Junio C Hamano, Jacob Keller

Hi Eric,

On Fri, 19 Jan 2018, Eric Sunshine wrote:

> On Thu, Jan 18, 2018 at 10:35 AM, Johannes Schindelin
> <johannes.schindelin@gmx.de> wrote:
> > [...]
> > +static int do_reset(const char *name, int len)
> > +{
> > +       [...]
> > +       if (hold_locked_index(&lock, LOCK_REPORT_ON_ERROR) < 0)
> > +               return -1;
> > +
> > +       for (i = 0; i < len; i++)
> > +               if (isspace(name[i]))
> > +                       len = i;
> 
> What is the purpose of this loop? I could imagine that it's trying to
> strip all whitespace from the end of 'name', however, to do that it
> would iterate backward, not forward. (Or perhaps it's trying to
> truncate at the first space, but then it would need to invert the
> condition or use 'break'.) Am I missing something obvious?

Yes, you are missing something obvious. The idea of the `reset` command is
that it not only has a label, but also the oneline of the original commit:

	reset branch-point sequencer: prepare for cleanup

In this instance, `branch-point` is the label. And for convenience of the
person editing, it also has the oneline. This came in *extremely* handy
when editing the commit topology in Git for Windows, i.e. when introducing
topic branches or flattening them.

In the Git garden shears, I separated the two arguments via `#`:

	reset branch-point # sequencer: prepare for cleanup

I guess that is actually more readable, so I will introduce that into this
patch series, too.

Ciao,
Dscho

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 1/8] sequencer: introduce new commands to reset the revision
  2018-01-24 22:01     ` Junio C Hamano
@ 2018-01-29 20:55       ` Johannes Schindelin
  0 siblings, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-01-29 20:55 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: Eric Sunshine, Git List, Jacob Keller

Hi Junio,

On Wed, 24 Jan 2018, Junio C Hamano wrote:

> Eric Sunshine <sunshine@sunshineco.com> writes:
> 
> >> +static int do_reset(const char *name, int len)
> >> +{
> >> +       [...]
> >> +       if (hold_locked_index(&lock, LOCK_REPORT_ON_ERROR) < 0)
> >> +               return -1;
> >> +
> >> +       for (i = 0; i < len; i++)
> >> +               if (isspace(name[i]))
> >> +                       len = i;
> >
> > What is the purpose of this loop? I could imagine that it's trying to
> > strip all whitespace from the end of 'name', however, to do that it
> > would iterate backward, not forward. (Or perhaps it's trying to
> > truncate at the first space, but then it would need to invert the
> > condition or use 'break'.) Am I missing something obvious?
> 
> I must be missing the same thing.  Given that the callers of
> do_reset(), other than the "bug" thing that passes the hard coded
> "onto", uses item->arg/item->arg_len which includes everything after
> the insn word on the line in the todo list, I do suspect that the
> intention is to stop at the first whitespace char to avoid creating
> a ref with whitespace in it, i.e. it is a bug that can be fixed with
> s/len = i/break/.
> 
> The code probably should further check the resulting string with
> check_ref_format() to detect strange chars and char sequences that
> make the resulting refname invalid.  For example, you would not want
> to allow a label with two consecutive periods in it.

The code already checks that by creating a ref.

No need to go crazy and validate ref names every time we parse the todo
list (which is quite often), eh?

Ciao,
Dscho

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 4/8] rebase-helper --make-script: introduce a flag to recreate merges
  2018-01-19 10:34   ` Eric Sunshine
  2018-01-23 20:13     ` Junio C Hamano
@ 2018-01-29 21:05     ` Johannes Schindelin
  1 sibling, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-01-29 21:05 UTC (permalink / raw)
  To: Eric Sunshine; +Cc: Git List, Junio C Hamano, Jacob Keller

Hi Eric,

On Fri, 19 Jan 2018, Eric Sunshine wrote:

> On Thu, Jan 18, 2018 at 10:35 AM, Johannes Schindelin
> <johannes.schindelin@gmx.de> wrote:
> 
> > structure (similar in spirit to --preserve-merges, but with a
> > substantially less-broken design).
> > [...]
> > Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
> > ---
> > diff --git a/sequencer.c b/sequencer.c
> > @@ -2785,6 +2787,335 @@ void append_signoff(struct strbuf *msgbuf, int ignore_footer, unsigned flag)
> > +static const char *label_oid(struct object_id *oid, const char *label,
> > +                            struct label_state *state)
> > +{
> > +       [...]
> > +       } else if (((len = strlen(label)) == GIT_SHA1_RAWSZ &&
> > +                   !get_oid_hex(label, &dummy)) ||
> > +                  hashmap_get_from_hash(&state->labels,
> > +                                        strihash(label), label)) {
> > +               /*
> > +                * If the label already exists, or if the label is a valid full
> > +                * OID, we append a dash and a number to make it unique.
> > +                */
> > +               [...]
> > +               for (i = 2; ; i++) {
> 
> Why '2'? Is there some non-obvious significance to this value?

I personally found it irritating to have labels "sequencer",
"sequencer-1". It sounds *wrong* to have a "-1". Because it is the second
label referring to the term "sequencer". So if there are two labels that
both want to be named "sequencer", the first one wins, and the second one
will be called "sequencer-2".

Hence the 2.

> > +static int make_script_with_merges(struct pretty_print_context *pp,
> > +                                  struct rev_info *revs, FILE *out,
> > +                                  unsigned flags)
> > +{
> > +       [...]
> > +               is_octopus = to_merge && to_merge->next;
> > +
> > +               if (is_octopus)
> > +                       BUG("Octopus merges not yet supported");
> 
> Is this a situation which the end-user can trigger by specifying a
> merge with more than two parents? If so, shouldn't this be just a
> normal error message rather than a (developer) bug message? Or, am I
> misunderstanding?

You are misunderstanding.

This is just a place-holder here. The patches to introduce support for
octopus merges are already written. They are lined up after this here
patch series, is all.

As such, please do not occupy your mind on the specifics or even the
upper-case of the "Octopus". This line is here only as a hint for the
reviewer that this is not yet implemented. And BUG(...) was chosen because
that way, we are not even tempted to waste the time of translators.

Speaking of wasting time... let's move on to further interesting code
reviews.

Ciao,
Dscho

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 4/8] rebase-helper --make-script: introduce a flag to recreate merges
  2018-01-23 20:13     ` Junio C Hamano
@ 2018-01-29 21:07       ` Johannes Schindelin
  0 siblings, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-01-29 21:07 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: Eric Sunshine, Git List, Jacob Keller

Hi Junio,

On Tue, 23 Jan 2018, Junio C Hamano wrote:

> Eric Sunshine <sunshine@sunshineco.com> writes:
> 
> >> +               is_octopus = to_merge && to_merge->next;
> >> +
> >> +               if (is_octopus)
> >> +                       BUG("Octopus merges not yet supported");
> >
> > Is this a situation which the end-user can trigger by specifying a
> > merge with more than two parents? If so, shouldn't this be just a
> > normal error message rather than a (developer) bug message? Or, am I
> > misunderstanding?
> 
> BUG() is "we wrote code carefully so that this should not trigger;
> we do not _expect_ the code to reach here".  This one is expected to
> trigger, and I agree with you that it should be die(), if the series
> is meant to be released to the general public in the current form
> (i.e. until the limitation is lifted so that it can handle an
> octopus).
> 
> If the callers are made more careful to check if there is an octopus
> involved and reject the request early, then seeing an octopus in
> this location in a loop will become a BUG().

This has occupied both of you for way too long.

It is *not interesting*. What *is* interesting is for example the
discussion about the "cousin commits". And maybe both of you gentle
persons can spend your brain cycles splendidly by trying to come up with a
better term. Or by trying to beat out obvious or not-so-obvious bugs in
the code.

Seriously, I am not interested in a discussion about BUG() vs die() as
long as there may be real bugs hiding.

Ciao,
Dscho

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 5/8] rebase: introduce the --recreate-merges option
  2018-01-19 10:55   ` Eric Sunshine
@ 2018-01-29 21:09     ` Johannes Schindelin
  0 siblings, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-01-29 21:09 UTC (permalink / raw)
  To: Eric Sunshine; +Cc: Git List, Junio C Hamano, Jacob Keller

Hi Eric,

On Fri, 19 Jan 2018, Eric Sunshine wrote:

> On Thu, Jan 18, 2018 at 10:35 AM, Johannes Schindelin
> <johannes.schindelin@gmx.de> wrote:
> > [...]
> > With this patch, the goodness of the Git garden shears comes to `git
> > rebase -i` itself. Passing the `--recreate-merges` option will generate
> > a todo list that can be understood readily, and where it is obvious
> > how to reorder commits. New branches can be introduced by inserting
> > `label` commands and calling `merge - <label> <oneline>`. And once this
> > mode has become stable and universally accepted, we can deprecate the
> > design mistake that was `--preserve-merges`.
> >
> > Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
> > ---
> > diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh
> > @@ -900,6 +900,7 @@ fi
> >  if test t != "$preserve_merges"
> >  then
> >         git rebase--helper --make-script ${keep_empty:+--keep-empty} \
> > +               ${recreate_merges:+--recreate-merges} \
> 
> If the user specifies both --preserve-merges and --recreate-merges, it
> looks like --preserve-merges will take precedence.
> 
> Should git-rebase.sh have a mutual-exclusion check and error out if
> both are specified?

Maybe. I welcome you to contribute such a patch once recreate-merges made
it into the code base.

In other words: this would be premature optimization. We're not at that
stage yet.

Ciao,
Dscho

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 1/8] sequencer: introduce new commands to resettherevision
  2018-01-19 12:24   ` [PATCH 1/8] sequencer: introduce new commands to resettherevision Phillip Wood
  2018-01-19 18:55     ` Phillip Wood
@ 2018-01-29 21:23     ` Johannes Schindelin
  1 sibling, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-01-29 21:23 UTC (permalink / raw)
  To: phillip.wood; +Cc: git, Junio C Hamano, Jacob Keller, Philip Oakley, Eric Sunshine

Hi Phillip,

On Fri, 19 Jan 2018, Phillip Wood wrote:

> 
> On 18/01/18 15:35, Johannes Schindelin wrote:
> 
> > This idea was developed in Git for Windows' Git garden shears (that
> > are used to maintain the "thicket of branches" on top of upstream
> > Git), and this patch is part of the effort to make it available to a
> > wider audience, as well as to make the entire process more robust (by
> > implementing it in a safe and portable language rather than a Unix
> > shell script).
> > 
> > This commit implements the commands to label, and to reset to, given
> > revisions. The syntax is:
> > 
> > 	label <name>
> > 	reset <name>
> 
> If I've understood the code below correctly then reset will clobber
> untracked files, this is the opposite behaviour to what happens when
> tries to checkout <onto> at the start of a rebase - then it will fail if
> untracked files would be overwritten.

This would be completely unintentional, I will verify that untracked files
are not clobbered.

However, in practice this should not happen because the intended use case
is for revisions to be labeled *before* checking them out at a later
stage. Therefore, the files that would be clobbered would already have
been tracked in the revision when it was labeled, and I do not quite see
how those files could become untracked without playing sloppy exec games
in between.

> > Internally, the `label <name>` command creates the ref
> > `refs/rewritten/<name>`. This makes it possible to work with the labeled
> > revisions interactively, or in a scripted fashion (e.g. via the todo
> > list command `exec`).
> 
> If a user has two work trees and runs a rebase in each with the same
> label name, they'll clobber each other. I'd suggest storing them under
> refs/rewritten/<branch-name or detached HEAD SHA> instead. If the user
> tries to rebase a second worktree with the same detached HEAD as an
> existing rebase then refuse to start.

That is why a later patch marks those refs/rewritten/ refs as
worktree-local.

> > +static int do_label(const char *name, int len)
> > +{
> > +	struct ref_store *refs = get_main_ref_store();
> > +	struct ref_transaction *transaction;
> > +	struct strbuf ref_name = STRBUF_INIT, err = STRBUF_INIT;
> > +	struct strbuf msg = STRBUF_INIT;
> > +	int ret = 0;
> > +	struct object_id head_oid;
> > +
> > +	strbuf_addf(&ref_name, "refs/rewritten/%.*s", len, name);
> > +	strbuf_addf(&msg, "label '%.*s'", len, name);
> 
> The other reflog messages below have a (rebase -i) prefix

Good point. I changed it to "rebase -i (label)".

> > +	transaction = ref_store_transaction_begin(refs, &err);
> > +	if (!transaction ||
> > +	    get_oid("HEAD", &head_oid) ||
> > +	    ref_transaction_update(transaction, ref_name.buf, &head_oid, NULL,
> > +				   0, msg.buf, &err) < 0 ||
> > +	    ref_transaction_commit(transaction, &err)) {
> > +		error("%s", err.buf);
> 
> if get_oid() fails then err is empty so there wont be an message after
> the 'error: '

Yep, that would be nasty. Fixed.

> > +static int do_reset(const char *name, int len)
> > +{
> > +	struct strbuf ref_name = STRBUF_INIT;
> > +	struct object_id oid;
> > +	struct lock_file lock = LOCK_INIT;
> > +	struct tree_desc desc;
> > +	struct tree *tree;
> > +	struct unpack_trees_options opts;
> > +	int ret = 0, i;
> > +
> > +	if (hold_locked_index(&lock, LOCK_REPORT_ON_ERROR) < 0)
> > +		return -1;
> > +
> > +	for (i = 0; i < len; i++)
> > +		if (isspace(name[i]))
> > +			len = i;
> 
> If name starts with any white space then I think this effectively
> truncates name to a bunch of white space which doesn't sound right. I'm
> not sure how this is being called, but it might be better to clean up
> name when the to-do list is parsed instead.

The left-trimming of the name was already performed as part of the todo
list parsing.

And we are not really right-trimming here. We are splitting a line of the
form

	reset <label> <oneline>

In fact, after reflecting about it, I changed the code so that it would
now even read:

	reset <label> # <oneline>

So the code really is doing the intended thing here.

Ciao,
Dscho

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 1/8] sequencer: introduce new commands to resettherevision
  2018-01-19 18:59       ` Jacob Keller
@ 2018-01-29 21:25         ` Johannes Schindelin
  2018-01-29 21:29           ` Johannes Schindelin
  0 siblings, 1 reply; 276+ messages in thread
From: Johannes Schindelin @ 2018-01-29 21:25 UTC (permalink / raw)
  To: Jacob Keller; +Cc: Phillip Wood, Git mailing list, Junio C Hamano, Philip Oakley, Eric Sunshine

Hi Jake,

On Fri, 19 Jan 2018, Jacob Keller wrote:

> On Fri, Jan 19, 2018 at 10:55 AM, Phillip Wood
> <phillip.wood@talktalk.net> wrote:
> > On 19/01/18 12:24, Phillip Wood wrote:
> >>
> >> On 18/01/18 15:35, Johannes Schindelin wrote:
> >>>
> >>> Internally, the `label <name>` command creates the ref
> >>> `refs/rewritten/<name>`. This makes it possible to work with the labeled
> >>> revisions interactively, or in a scripted fashion (e.g. via the todo
> >>> list command `exec`).
> >>
> >> If a user has two work trees and runs a rebase in each with the same
> >> label name, they'll clobber each other. I'd suggest storing them under
> >> refs/rewritten/<branch-name or detached HEAD SHA> instead. If the user
> >> tries to rebase a second worktree with the same detached HEAD as an
> >> existing rebase then refuse to start.
> >>
> >
> > Ah this isn't a concern after all as patch 5 makes refs/rewritten local
> > to the worktree. Perhaps you could move that part of patch 5 here or add
> > a note to the commit message that it will become worktree local later in
> > the series
> >
> > Best Wishes
> >
> > Phillip
> 
> I'd rather it be included here as well.

But it would have been really easy to overlook in here. I really want this
to be a separate commit, also to have a chance to get this done
*differently* if somebody comes up with a splendid idea how to do that
(because hard-coding feels quite dirty).

Ciao,
Dscho

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 1/8] sequencer: introduce new commands to resettherevision
  2018-01-29 21:25         ` Johannes Schindelin
@ 2018-01-29 21:29           ` Johannes Schindelin
  0 siblings, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-01-29 21:29 UTC (permalink / raw)
  To: Jacob Keller; +Cc: Phillip Wood, Git mailing list, Junio C Hamano, Philip Oakley, Eric Sunshine

Hi,

On Mon, 29 Jan 2018, Johannes Schindelin wrote:

> On Fri, 19 Jan 2018, Jacob Keller wrote:
> 
> > On Fri, Jan 19, 2018 at 10:55 AM, Phillip Wood
> > <phillip.wood@talktalk.net> wrote:
> > > On 19/01/18 12:24, Phillip Wood wrote:
> > >>
> > >> On 18/01/18 15:35, Johannes Schindelin wrote:
> > >>>
> > >>> Internally, the `label <name>` command creates the ref
> > >>> `refs/rewritten/<name>`. This makes it possible to work with the labeled
> > >>> revisions interactively, or in a scripted fashion (e.g. via the todo
> > >>> list command `exec`).
> > >>
> > >> If a user has two work trees and runs a rebase in each with the same
> > >> label name, they'll clobber each other. I'd suggest storing them under
> > >> refs/rewritten/<branch-name or detached HEAD SHA> instead. If the user
> > >> tries to rebase a second worktree with the same detached HEAD as an
> > >> existing rebase then refuse to start.
> > >>
> > >
> > > Ah this isn't a concern after all as patch 5 makes refs/rewritten local
> > > to the worktree. Perhaps you could move that part of patch 5 here or add
> > > a note to the commit message that it will become worktree local later in
> > > the series
> > >
> > > Best Wishes
> > >
> > > Phillip
> > 
> > I'd rather it be included here as well.
> 
> But it would have been really easy to overlook in here. I really want this
> to be a separate commit, also to have a chance to get this done
> *differently* if somebody comes up with a splendid idea how to do that
> (because hard-coding feels quite dirty).

BTW there is an additional good reason why the patch to make
refs/rewritten/* worktree-local is so far away: that is the first time in
the patch series when we can test this really effectively; at that stage
we can easily just add to t3430 because all the building blocks for
`rebase -i --recreate-merges` are in place.

Ciao,
Dscho

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 2/8] sequencer: introduce the `merge` command
  2018-01-20  9:18     ` Jacob Keller
@ 2018-01-29 21:41       ` Johannes Schindelin
  2018-01-31 13:48         ` Johannes Schindelin
  0 siblings, 1 reply; 276+ messages in thread
From: Johannes Schindelin @ 2018-01-29 21:41 UTC (permalink / raw)
  To: Jacob Keller; +Cc: Phillip Wood, Git mailing list, Junio C Hamano, Eric Sunshine

Hi Jake & Phillip,

On Sat, 20 Jan 2018, Jacob Keller wrote:

> On Fri, Jan 19, 2018 at 6:45 AM, Phillip Wood <phillip.wood@talktalk.net> wrote:
> > On 18/01/18 15:35, Johannes Schindelin wrote:
> >>
> >> This patch is part of the effort to reimplement `--preserve-merges` with
> >> a substantially improved design, a design that has been developed in the
> >> Git for Windows project to maintain the dozens of Windows-specific patch
> >> series on top of upstream Git.
> >>
> >> The previous patch implemented the `label`, `bud` and `reset` commands
> >> to label commits and to reset to a labeled commits. This patch adds the
> >> `merge` command, with the following syntax:
> >>
> >>       merge <commit> <rev> <oneline>
> >
> > I'm concerned that this will be confusing for users. All of the other
> > rebase commands replay the changes in the commit hash immediately
> > following the command name. This command instead uses the first commit
> > to specify the message which is different to both 'git merge' and the
> > existing rebase commands. I wonder if it would be clearer to have 'merge
> > -C <commit> <rev> ...' instead so it's clear which argument specifies
> > the message and which the remote head to merge. It would also allow for
> > 'merge -c <commit> <rev> ...' in the future for rewording an existing
> > merge message and also avoid the slightly odd 'merge - <rev> ...'. Where
> > it's creating new merges I'm not sure it's a good idea to encourage
> > people to only have oneline commit messages by making it harder to edit
> > them, perhaps it could take another argument to mean open the editor or
> > not, though as Jake said I guess it's not that common.
> 
> I actually like the idea of re-using commit message options like -C,
> -c,  and -m, so we could do:
> 
> merge -C <commit> ... to take message from commit

That is exactly how the Git garden shears do it.

I found it not very readable. That is why I wanted to get away from it in
--recreate-merges.

> merge -c <commit> ...  to take the message from commit and open editor to edit
> merge -m "<message>" ... to take the message from the quoted test
> merge ... to merge and open commit editor with default message
> 
> This also, I think, allows us to not need to put the oneline on the
> end, meaning we wouldn't have to quote the parent commit arguments
> since we could use option semantics?

The oneline is there primarily to give you, the reader, a clue when
reading and editing the todo list.

Reusing it for the `merge -` command was only an afterthought.

> > One thought that just struck me - if a merge or reset command specifies
> > an invalid label is it rescheduled so that it's still in the to-do list
> > when the user edits it after rebase stops?

It is not rescheduled, because the command already failed, so we know it
is bad.

You have to go edit the todo list, possibly after copying the faulty
command from `git status`' output.

> > In the future it might be nice if the label, reset and merge commands
> > were validated when the to-do list is parsed so that the user gets
> > immediate feedback if they try to create a label that is not a valid
> > ref name or that they have a typo in a name given to reset or merge
> > rather than the rebase stopping later.

There are too many possible errors to make this fool-proof. What if the
ref name is valid, but there was no `label` command yet?  What if there
*has* been a `label` command but it is now stuck in the `done` file (which
we do not parse, ever)? What if the user specified two label commands with
the same label?

It sounds like an exercise in futility to try to catch these things in the
parser.

And keep in mind that the parser is used

- when shortening the commit names
- when checking the todo list for accidentally dropped picks
- when skipping unnecessary picks
- when rearranging fixup!/squash! commands
- when adding `exec` commands specified via `-x`

I am not sure that I would come out in favor of trying to catch ref name
errors during parsing time if I wanted to balance bang vs buck.

Ciao,
Dscho

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 3/8] sequencer: fast-forward merge commits, if possible
  2018-01-19 14:53   ` Phillip Wood
  2018-01-23 19:12     ` Junio C Hamano
@ 2018-01-29 21:47     ` Johannes Schindelin
  1 sibling, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-01-29 21:47 UTC (permalink / raw)
  To: phillip.wood; +Cc: git, Junio C Hamano, Jacob Keller

Hi Phillip,

On Fri, 19 Jan 2018, Phillip Wood wrote:

> On 18/01/18 15:35, Johannes Schindelin wrote:
> > 
> > Just like with regular `pick` commands, if we are trying to recreate a
> > merge commit, we now test whether the parents of said commit match HEAD
> > and the commits to be merged, and fast-forward if possible.
> > 
> > This is not only faster, but also avoids unnecessary proliferation of
> > new objects.
> 
> I might have missed something but shouldn't this be checking
> opts->allow_ff?

Good point. This is the type of review for which I was hoping.

> Another possible optimization is that if the parent branches have only
> reworded commits or some commits that have been squashed but no other
> changes then their trees will be the same as in the original merge
> commit and so could be reused without calling merge_recursive().

True. It is also a bit involved to check this condition, and I am not sure
that it is worth the effort for my use case.

So I would invite you to work on this after this patch series settles, if
you are interested.

Ciao,
Dscho

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 0/8] rebase -i: offer to recreate merge commits
  2018-01-19 20:25 ` [PATCH 0/8] rebase -i: offer to recreate merge commits Junio C Hamano
@ 2018-01-29 21:53   ` Johannes Schindelin
  0 siblings, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-01-29 21:53 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: git, Jacob Keller

Hi Junio,

On Fri, 19 Jan 2018, Junio C Hamano wrote:

> Johannes Schindelin <johannes.schindelin@gmx.de> writes:
> 
> > Think of --recreate-merges as "--preserve-merges done right". It
> > introduces new verbs for the todo list, `label`, `reset` and `merge`.
> > For a commit topology like this:
> >
> >             A - B - C
> >               \   /
> >                 D
> >
> > the generated todo list would look like this:
> >
> >             # branch D
> >             pick 0123 A
> >             label branch-point
> >             pick 1234 D
> >             label D
> >
> >             reset branch-point
> >             pick 2345 B
> >             merge 3456 D C
> 
> Yup.  I've seen this design talked about on list in the past, and
> I've always felt that this is "sequencer done right".
> 
> At the first glance, it may feel somewhat unsatisfying that "merge"
> has to say effects of which commits should be reflected in the
> result and which commot to take the log message from, i.e.
> (recreated)D is merged to form the resulting tree, and 3456=C is
> used for the log, to recreate C in the above example, while "pick"
> always uses the same commit for both, i.e. recreated B inherits both
> the changes and log message from the original B=2345 (or depending
> on the readers' point of view, "merge" is allowed to use two
> different commits, while "pick" is always limited to the same one).
> 
> But I think this distinction is probably fundamental and I am not
> opposed to it at all.  The result of "pick" has only one parent, and
> the parent is determined only by the previous actions and not by
> anything on the "pick" line in the todo list.  But the result of
> "merge" has to record all the other parents, and only the first
> parent is determined implicitly by the previous actions.  We need to
> tell the "merge" command about "3456=C" in order to recreate the
> effect of original merge commit (i.e. changes between B and C) as
> well as its log message, and we also need to tell it about label "D"
> that it is the "other parent" that need to be recorded.

Yes, this was the hard lesson of the failed preserve-merges design.

> Obviously "merge" command syntax should allow recreating an octopus,
> so whenever I said "two" in the above, I meant "N".  The original
> merge commit is needed so that the effect to replay (roughly: a
> patch going to the original merge result from its first parent) can
> be learned from the existing history, and all the other "N-1"
> parents needs to be given (and they must have been already created
> in the todo list) so that the resulting recreated merge can be
> recorded with them as parents (in addition to the first parent that
> is implicitly given as the result of all the previous steps).

I have two more patch series lined up after this one, the first one
implements --root via the sequencer, and the second one indeed extends
`merge` to handle octopus commits.

> One interesting (and probably useful) thing to notice is that if A
> were not rebased in the above sample picture, and only B were the
> one that was tweaked, then a recreated C may use the same original D
> as its side parent, and the mechanism outlined above naturally can
> support it by allowing an un-rewritten commit to be given as a side
> parent when "merge" is redoing C.

I think that you will get a kick out of reading the commit message of the
last commit, as it does talk about the problematic C: it *would* be
rebased by default.

In the next iteration I will actually switch around the default from
rebase-cousins to no-rebase-cousins for that reason.

Ciao,
Dscho

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 1/8] sequencer: introduce new commands to reset the revision
  2018-01-22 21:25     ` Junio C Hamano
@ 2018-01-29 22:00       ` Johannes Schindelin
  0 siblings, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-01-29 22:00 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: Jacob Keller, Git mailing list

Hi Junio,

On Mon, 22 Jan 2018, Junio C Hamano wrote:

> Jacob Keller <jacob.keller@gmail.com> writes:
> 
> > The code looks good, but I'm a little wary of adding bud which
> > hard-codes a specific label. I suppose it does grant a bit of
> > readability to the resulting script... ? It doesn't seem that
> > important compared to use using "reset onto"? At least when
> > documenting this it should be made clear that the "onto" label is
> > special.
> 
> I do not think we would mind "bud" too much in the end result, but
> the change in 1/8 is made harder to read than necessary with it.  It
> is the only thing that needs "a single-letter command name may now
> not have any argument after it" change to the parser among the three
> things being added here, and it also needs to be added to the list
> of special commands without arguments.
> 
> It would have been easier to reason about if addition of "bud" was
> in its own patch done after label and reset are added.  And if done
> as a separate step, perhaps it would have been easier to realize
> that it would be a more future-proof solution for handling the
> "special" ness of BUD to add a new "unsigned flags" word to
> todo_command_info[] structure and using a bit that says "this does
> not take an arg" than to hardcode "noop and bud are the commands
> without args" in the code.  That hardcode was good enough when there
> was only one thing in that special case.  Now it has two.

I dropped the `bud` command. It did come in handy when I truly recreated
branch structure from a way-too-long topic branch, but that is probably a
rare use case, and not worth spending so much air time on.

> In a similar way, the code to special case label and reset just like
> exec may also want to become more table driven, perhaps using
> another bit in the same new flags word to say "this does not refer
> to commit".  I think that can become [v2 1/N] while addition of "bud"
> can be [v2 2/N] (after all, "bud" just does the same do_reset() with
> hardcoded argument, so "label/reset" must come first).

The downside of such a table-driven approach is readability, of course. It
becomes *less* readable because all of a sudden you have to jump back and
forth between the parsing code and the table (and then you also have to
keep the table header in sight).

I have had enough problems with such table-driven approaches in the past,
even in Git's own source code. Exhibit A: t0027. I do not wish upon my
worst enemies having to investigate problems in that script, for in those
tables despair awaits ye who dare enter.

So I respectfully decline to go into that direction in the sequencer.

Ciao,
Dscho

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 2/8] sequencer: introduce the `merge` command
  2018-01-22 22:12   ` Junio C Hamano
@ 2018-01-29 22:15     ` Johannes Schindelin
  0 siblings, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-01-29 22:15 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: git, Jacob Keller

Hi Junio,

On Mon, 22 Jan 2018, Junio C Hamano wrote:

> Johannes Schindelin <johannes.schindelin@gmx.de> writes:
> 
> >  	end_of_object_name = (char *) bol + strcspn(bol, " \t\n");
> > +	item->arg = end_of_object_name + strspn(end_of_object_name, " \t");
> > +	item->arg_len = (int)(eol - item->arg);
> > +
> >  	saved = *end_of_object_name;
> > +	if (item->command == TODO_MERGE && *bol == '-' &&
> > +	    bol + 1 == end_of_object_name) {
> > +		item->commit = NULL;
> > +		return 0;
> > +	}
> > +
> >  	*end_of_object_name = '\0';
> >  	status = get_oid(bol, &commit_oid);
> >  	*end_of_object_name = saved;
> >  
> > -	item->arg = end_of_object_name + strspn(end_of_object_name, " \t");
> > -	item->arg_len = (int)(eol - item->arg);
> > -
> 
> Assigning to "saved" before the added "if we are doing merge and see
> '-', do this special thing" is not only unnecessary, but makes the
> logic in the non-special case harder to read.  The four things
> "saved = *eol; *eol = 0; do_thing_using(bol); *eol = saved;" is a
> single logical unit; keep them together.

True. This was a sloppily resolved merge conflict in one of the many
rewrites, I guess.

> > +		if (*p)
> > +			len = strlen(p);
> > +		else {
> > +			strbuf_addf(&buf, "Merge branch '%.*s'",
> > +				    merge_arg_len, arg);
> > +			p = buf.buf;
> > +			len = buf.len;
> > +		}
> 
> So... "arg" received by this function can be a single non-whitespace
> token, which is taken as the name of the branch being merged (in
> this else clause).  Or it can also be followed by a single liner
> message for the merge commit.  Presumably, this is for creating a
> new merge (i.e. "commit==NULL" case), and preparing a proper log
> message in the todo list is unrealistic, so this would be a
> reasonable compromise.  Those users who want to write proper log
> message could presumably follow such "merge" insn with a "x git
> commit --amend" or something, I presume, if they really wanted to.

Precisely.

> > +		if (write_message(p, len, git_path_merge_msg(), 0) < 0) {
> > +			error_errno(_("Could not write '%s'"),
> > +				    git_path_merge_msg());
> > +			strbuf_release(&buf);
> > +			rollback_lock_file(&lock);
> > +			return -1;
> > +		}
> > +		strbuf_release(&buf);
> > +	}
> 
> OK.  Now we have prepared the MERGE_MSG file and are ready to commit.
> 
> > +	head_commit = lookup_commit_reference_by_name("HEAD");
> > +	if (!head_commit) {
> > +		rollback_lock_file(&lock);
> > +		return error(_("Cannot merge without a current revision"));
> > +	}
> 
> Hmph, I would have expected to see this a lot earlier, before
> dealing with the log message.  Leftover MERGE_MSG file after an
> error will cause unexpected fallout to the end-user experience
> (including what is shown by the shell prompt scripts), but if we do
> this before the MERGE_MSG thing, we do not have to worry about
> error codepath having to remove it.

Fixed.

> > +	strbuf_addf(&ref_name, "refs/rewritten/%.*s", merge_arg_len, arg);
> > +	merge_commit = lookup_commit_reference_by_name(ref_name.buf);
> > +	if (!merge_commit) {
> > +		/* fall back to non-rewritten ref or commit */
> > +		strbuf_splice(&ref_name, 0, strlen("refs/rewritten/"), "", 0);
> > +		merge_commit = lookup_commit_reference_by_name(ref_name.buf);
> > +	}
> 
> OK, so "abc" in the example in the log message is looked up first as
> a label and then we take a fallback to interpret as an object name.

Yes. And auto-generated labels are guaranteed not to be full hex hashes
for that reason.

> Hopefully allowed names in "label" would be documented clearly in
> later steps (I am guessing that "a name that can be used as a branch
> name can be used as a label name and vice versa" or something like
> that).

Well, I thought that it would suffice to say that these labels are
available as refs/rewritten/<label>. It kind of goes without saying that
those need to be valid ref names, then?

> > +	if (!merge_commit) {
> > +		error(_("could not resolve '%s'"), ref_name.buf);
> > +		strbuf_release(&ref_name);
> > +		rollback_lock_file(&lock);
> > +		return -1;
> > +	}
> > +	write_message(oid_to_hex(&merge_commit->object.oid), GIT_SHA1_HEXSZ,
> > +		      git_path_merge_head(), 0);
> > +	write_message("no-ff", 5, git_path_merge_mode(), 0);
> 
> These two calls gave me a "Huh?" moment; write_message() sounds like
> it is allowed to be later updated with frills suitable for *_MSG
> files we place in .git/ directory (iow, it is in principle OK if
> commented out instructions common to these files are added to the
> output by the function), but these want exact bytes passed in the
> result, for which wrapper.c::write_file() is more appropriate.

I agree that write_message() is not a good name, but write_file() was
already taken. I do not think wrapper.c:write_file() is more appropriate,
as it has *also* misnamed: it *always completes the line*. In other words,
it is not write_file(), it is write_line_to_file(). And not even that, as
it takes a printf()-type format.

No, write_message(), even if not named appropriately, is the functionality
I want.

> Alternatively, perhaps write_message() can be dropped and its
> callers can call wrapper.c::write_file() instead?  Such a clean-up
> may require teaching the append-eol thing that write_message() wants
> to wrapper.c::write_file(), but it shouldn't be a rocket science.

write_file() does different things than write_message(). I think this
suggestion to congeal them into one is quite overzealous.

And would also lead us astray.

> > +	common = get_merge_bases(head_commit, merge_commit);
> > +	for (j = common; j; j = j->next)
> > +		commit_list_insert(j->item, &reversed);
> > +	free_commit_list(common);
> 
> I know this is copy&pasted code from "builtin/merge.c", but is there
> a reason to reverse the common ancestor list here?

Yes. You explained the reason yourself: this is `git merge`'s behavior. To
recreate it in the sequencer, the list of common ancestors has to be
reversed here, too.

Ciao,
Dscho

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 3/8] sequencer: fast-forward merge commits, if possible
  2018-01-23 18:51   ` Junio C Hamano
@ 2018-01-29 22:18     ` Johannes Schindelin
  0 siblings, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-01-29 22:18 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: git, Jacob Keller

Hi Junio,

On Tue, 23 Jan 2018, Junio C Hamano wrote:

> Johannes Schindelin <johannes.schindelin@gmx.de> writes:
> 
> > +	/*
> > +	 * If HEAD is not identical to the parent of the original merge commit,
> > +	 * we cannot fast-forward.
> > +	 */
> > +	can_fast_forward = commit && commit->parents &&
> > +		!oidcmp(&commit->parents->item->object.oid,
> > +			&head_commit->object.oid);
> > +
> 
> I think this expression and assignment should better be done much
> later.  Are you going to update commit, commit->parents, etc. that
> are involved in the computation in the meantime???

No, it is in the exact right spot.

What you are missing is that there will be some more code here, to support
octopus merges.

That's why it is exactly the right spot.

> > @@ -2164,6 +2172,17 @@ static int do_merge(struct commit *commit, const char *arg, int arg_len,
> >  		rollback_lock_file(&lock);
> >  		return -1;
> >  	}
> > +
> > +	if (can_fast_forward && commit->parents->next &&
> > +	    !commit->parents->next->next &&
> > +	    !oidcmp(&commit->parents->next->item->object.oid,
> > +		    &merge_commit->object.oid)) {
> 
> ... Namely, here.

... which the octopus merge code will never reach. That's why this is the
wrong spot for that initial can_fast_forward assignment.

> Because the earlier one is computing "are we
> replaying exactly the same commit on top of exactly the same
> state?", which is merely one half of "can we fast-forward", and
> storing it in a variable whose name is over-promising way before it
> becomes necessary.  The other half of "can we fast-forward?" logic
> is the remainder of the if() condition we see above.  IOW, when
> fully spelled, this code can fast-forward when we are replaying a
> commit on top of exactly the same first-parent and the commit being
> replayed is a single parent merge.
> 
> We may even want to get rid of can_fast_forward variable.

Absolutely not.

Ciao,
Dscho

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 4/8] rebase-helper --make-script: introduce a flag to recreate merges
  2018-01-23 20:03   ` Junio C Hamano
@ 2018-01-29 22:37     ` Johannes Schindelin
  0 siblings, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-01-29 22:37 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: git, Jacob Keller

Hi Junio,

On Tue, 23 Jan 2018, Junio C Hamano wrote:

> Johannes Schindelin <johannes.schindelin@gmx.de> writes:
> 
> > structure (similar in spirit to --preserve-merges, but with a
> > substantially less-broken design).
> > ...
> > @@ -2785,6 +2787,335 @@ void append_signoff(struct strbuf *msgbuf, int ignore_footer, unsigned flag)
> >  	strbuf_release(&sob);
> >  }
> >  
> > +struct labels_entry {
> > +	struct hashmap_entry entry;
> > +	char label[FLEX_ARRAY];
> > +};
> > +
> > +static int labels_cmp(const void *fndata, const struct labels_entry *a,
> > +		      const struct labels_entry *b, const void *key)
> > +{
> > +	return key ? strcmp(a->label, key) : strcmp(a->label, b->label);
> > +}
> 
> label_oid() accesses state->labels hash using strihash() as the hash
> function, but the final comparison between the entries in the same
> hash buckets are done with case sensitivity.  It is unclear to me if
> that is what was intended, and why.

Heh... you were almost there with your analysis. strihash() is needed for
case-insensitive comparisons, and labels are... ref names.

So the idea (which I implemented only partially) was to make the labels
case-insensitive based on `ignore_case`.

I think the best way forward will be to copy
merge-recursive.c:path_hash().

> > +struct string_entry {
> > +	struct oidmap_entry entry;
> > +	char string[FLEX_ARRAY];
> > +};
> > +
> > +struct label_state {
> > +	struct oidmap commit2label;
> > +	struct hashmap labels;
> > +	struct strbuf buf;
> > +};
> > +
> > +static const char *label_oid(struct object_id *oid, const char *label,
> > +			     struct label_state *state)
> > +{
> > +	struct labels_entry *labels_entry;
> > +	struct string_entry *string_entry;
> > +	struct object_id dummy;
> > +	size_t len;
> > +	int i;
> > +
> > +	string_entry = oidmap_get(&state->commit2label, oid);
> > +	if (string_entry)
> > +		return string_entry->string;
> > +
> > +	/*
> > +	 * For "uninteresting" commits, i.e. commits that are not to be
> > +	 * rebased, and which can therefore not be labeled, we use a unique
> > +	 * abbreviation of the commit name. This is slightly more complicated
> > +	 * than calling find_unique_abbrev() because we also need to make
> > +	 * sure that the abbreviation does not conflict with any other
> > +	 * label.
> > +	 *
> > +	 * We disallow "interesting" commits to be labeled by a string that
> > +	 * is a valid full-length hash, to ensure that we always can find an
> > +	 * abbreviation for any uninteresting commit's names that does not
> > +	 * clash with any other label.
> > +	 */
> > +	if (!label) {
> > +		char *p;
> > +
> > +		strbuf_reset(&state->buf);
> > +		strbuf_grow(&state->buf, GIT_SHA1_HEXSZ);
> > +		label = p = state->buf.buf;
> > +
> > +		find_unique_abbrev_r(p, oid->hash, default_abbrev);
> > +
> > +		/*
> > +		 * We may need to extend the abbreviated hash so that there is
> > +		 * no conflicting label.
> > +		 */
> > +		if (hashmap_get_from_hash(&state->labels, strihash(p), p)) {
> > +			size_t i = strlen(p) + 1;
> > +
> > +			oid_to_hex_r(p, oid);
> > +			for (; i < GIT_SHA1_HEXSZ; i++) {
> > +				char save = p[i];
> > +				p[i] = '\0';
> > +				if (!hashmap_get_from_hash(&state->labels,
> > +							   strihash(p), p))
> > +					break;
> > +				p[i] = save;
> > +			}
> > +		}
> 
> If oid->hash required full 40-hex to disambiguate, then
> find-unique-abbrev would give 40-hex and we'd want the same "-<num>"
> suffix technique employed below to make it consistently unique.  I
> wonder if organizing the function this way ...
> 
> 	if (!label)
> 		label = oid-to-hex(oid);
> 
> 	if (label already exists or full oid) {
> 		make it unambiguous;
> 	}

It was hard to miss, I agree. The first arm of the if() is not trying to
make a label unambiguous by adding `-<num>`, but by extending the
abbreviated 40-hex until it is unique. (And we guarantee that there is a
solution, as we do not allow valid 40-hex strings to be used as label.)

The reason: if no `label` is provided, we want a valid raw (possibly
abbreviated) commit name. If `label` is provided, we want a valid ref name
(possibly made unique by appending `-<num>`).

Different beasts. Cannot be put into the same if() arm.

> A related tangent.  Does an auto-label given to "uninteresting"
> commit need to be visible to end users?

Yes. The reason for this is the `merge`/`reset` commands when *not*
rebasing cousins. Because in that case, we have to refer to commits that
we could not possibly have `label`ed, because they were never the current
revision. Therefore we cannot assign <name>-<num> labels, but have to use
the abbreviated commit hash.

> > +static int make_script_with_merges(struct pretty_print_context *pp,
> > +				   struct rev_info *revs, FILE *out,
> > +				   unsigned flags)
> > +{
> > + ...
> > +	int abbr = flags & TODO_LIST_ABBREVIATE_CMDS;
> > +	const char *p = abbr ? "p" : "pick", *l = abbr ? "l" : "label",
> > +		 *t = abbr ? "t" : "reset", *b = abbr ? "b" : "bud",
> > +		 *m = abbr ? "m" : "merge";
> 
> It would be easier to understand if these short named variables are
> reserved only for temporary use, not as constants.  It is not too
> much to spell 
> 
> 	fprintf(out, "%s onto\n", cmd_label);
> 
> than
> 
> 	fprintf(out, "%s onto\n", l);
> 
> and would save readers from head-scratching, wondering where the
> last assignment to variable "l" is.

Sure.

> > +	oidmap_init(&commit2todo, 0);
> > +	oidmap_init(&state.commit2label, 0);
> > +	hashmap_init(&state.labels, (hashmap_cmp_fn) labels_cmp, NULL, 0);
> > +	strbuf_init(&state.buf, 32);
> > +
> > +	if (revs->cmdline.nr && (revs->cmdline.rev[0].flags & BOTTOM)) {
> > +		struct object_id *oid = &revs->cmdline.rev[0].item->oid;
> > +		FLEX_ALLOC_STR(entry, string, "onto");
> > +		oidcpy(&entry->entry.oid, oid);
> > +		oidmap_put(&state.commit2label, entry);
> > +	}
> > +
> > +	/*
> > +	 * First phase:
> > +	 * - get onelines for all commits
> > +	 * - gather all branch tips (i.e. 2nd or later parents of merges)
> > +	 * - label all branch tips
> > +	 */
> 
> When an early part of a branch is merged and then the remaining part
> of the same branch is merged again, "branch tip" and "2nd or later
> parents of merges" would become different concepts.  The 2nd parent
> of an early merge is not among the branch tips.
> 
> For the purpose of the "recreate the topology" algorithm, I am
> imagining that you would need not just the tips but all the 2nd and
> subsequent parents of merges, and my quick skimming tells me that
> the following code grabs them correctly.

Yes. I have used an earlier iteration of this patch series to create the
merging-rebase of Git for Windows v2.16.0 and that has a couple of really
good corner cases to exercise this code.

Ciao,
Dscho

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 0/8] rebase -i: offer to recreate merge commits
  2018-01-23 20:29 ` Junio C Hamano
@ 2018-01-29 22:53   ` Johannes Schindelin
  0 siblings, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-01-29 22:53 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: git, Jacob Keller

Hi Junio,

On Tue, 23 Jan 2018, Junio C Hamano wrote:

> Johannes Schindelin <johannes.schindelin@gmx.de> writes:
> 
> > My original attempt was --preserve-merges, but that design was so
> > limited that I did not even enable it in interactive mode.
> > ...
> > There are more patches in the pipeline, based on this patch series, but
> > left for later in the interest of reviewable patch series: one mini
> > series to use the sequencer even for `git rebase -i --root`, and another
> > one to add support for octopus merges to --recreate-merges.
> 
> I left comments on a handful of them, but I do not think any of them
> spotted a grave design issue to be a show stopper.  Overall, the
> series was quite a pleasant read, even with those minor nits and
> rooms for improvements.

I objected to a couple obviously problematic suggestions, and I
implemented all others (even those to which I did not respond
specifically).

Ciao,
Dscho

^ permalink raw reply	[flat|nested] 276+ messages in thread

* [PATCH v2 00/10] rebase -i: offer to recreate merge commits
  2018-01-18 15:35 [PATCH 0/8] rebase -i: offer to recreate merge commits Johannes Schindelin
                   ` (11 preceding siblings ...)
  2018-01-23 20:29 ` Junio C Hamano
@ 2018-01-29 22:54 ` " Johannes Schindelin
  2018-01-29 22:54   ` [PATCH v2 01/10] git-rebase--interactive: clarify arguments Johannes Schindelin
                     ` (12 more replies)
  12 siblings, 13 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-01-29 22:54 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Jacob Keller, Stefan Beller, Philip Oakley, Eric Sunshine, Phillip Wood

Once upon a time, I dreamt of an interactive rebase that would not
flatten branch structure, but instead recreate the commit topology
faithfully.

My original attempt was --preserve-merges, but that design was so
limited that I did not even enable it in interactive mode.

Subsequently, it *was* enabled in interactive mode, with the predictable
consequences: as the --preserve-merges design does not allow for
specifying the parents of merge commits explicitly, all the new commits'
parents are defined *implicitly* by the previous commit history, and
hence it is *not possible to even reorder commits*.

This design flaw cannot be fixed. Not without a complete re-design, at
least. This patch series offers such a re-design.

Think of --recreate-merges as "--preserve-merges done right". It
introduces new verbs for the todo list, `label`, `reset` and `merge`.
For a commit topology like this:

            A - B - C
              \   /
                D

the generated todo list would look like this:

            # branch D
            pick 0123 A
            label branch-point
            pick 1234 D
            label D

            reset branch-point
            pick 2345 B
            merge 3456 D C

There are more patches in the pipeline, based on this patch series, but
left for later in the interest of reviewable patch series: one mini
series to use the sequencer even for `git rebase -i --root`, and another
one to add support for octopus merges to --recreate-merges.

Changes since v1:

- reintroduced "sequencer: make refs generated by the `label` command
  worktree-local" (which was squashed into "sequencer: handle autosquash
  and post-rewrite for merge commands" by accident)

- got rid of the universally-hated `bud` command

- as per Stefan's suggestion, the help blurb at the end of the todo list
  now lists the syntax

- the no-rebase-cousins mode was made the default; This not only reflects
  the experience won from those years of using the Git garden shears, but
  was also deemed the better default in the discussion on the PR at
  https://github.com/git/git/pull/447

- I tried to clarify the role of the `onto` label in the commit message of
  `rebase-helper --make-script: introduce a flag to recreate merges`

- fixed punctuation at the end of error(...) messages, and incorrect
  upper-case at the start

- changed the generated todo lists to separate the label and the oneline in
  the `reset` command with a `#`, for readability

- dropped redundant paragraph in the commit message that talked about
  support for octopus merges

- avoided empty error message when HEAD could not be read during do_label()

- merge commits are fast-forwarded only unless --force-rebase was passed

- do_merge() now errors out a lot earlier when HEAD could not be parsed

- the one-letter variables to hold either abbreviated or full todo list
  instructions in make_script_recreating_merges() were renamed to clearer
  names

- The description of rebase's --recreate-merge option has been reworded;
  Hopefully it is a lot more clear now.


Johannes Schindelin (9):
  sequencer: introduce new commands to reset the revision
  sequencer: introduce the `merge` command
  sequencer: fast-forward merge commits, if possible
  rebase-helper --make-script: introduce a flag to recreate merges
  rebase: introduce the --recreate-merges option
  sequencer: make refs generated by the `label` command worktree-local
  sequencer: handle autosquash and post-rewrite for merge commands
  pull: accept --rebase=recreate to recreate the branch topology
  rebase -i: introduce --recreate-merges=[no-]rebase-cousins

Stefan Beller (1):
  git-rebase--interactive: clarify arguments

 Documentation/config.txt               |   8 +
 Documentation/git-pull.txt             |   5 +-
 Documentation/git-rebase.txt           |  14 +-
 builtin/pull.c                         |  14 +-
 builtin/rebase--helper.c               |  13 +-
 builtin/remote.c                       |   2 +
 contrib/completion/git-completion.bash |   4 +-
 git-rebase--interactive.sh             |  22 +-
 git-rebase.sh                          |  16 +
 refs.c                                 |   3 +-
 sequencer.c                            | 699 ++++++++++++++++++++++++++++++++-
 sequencer.h                            |   7 +
 t/t3430-rebase-recreate-merges.sh      | 208 ++++++++++
 13 files changed, 988 insertions(+), 27 deletions(-)
 create mode 100755 t/t3430-rebase-recreate-merges.sh


base-commit: 5be1f00a9a701532232f57958efab4be8c959a29
Published-As: https://github.com/dscho/git/releases/tag/recreate-merges-v2
Fetch-It-Via: git fetch https://github.com/dscho/git recreate-merges-v2

Interdiff vs v1:
 diff --git a/Documentation/git-rebase.txt b/Documentation/git-rebase.txt
 index ac07a5c3fc9..0e6d020d924 100644
 --- a/Documentation/git-rebase.txt
 +++ b/Documentation/git-rebase.txt
 @@ -371,12 +371,13 @@ have the long commit hash prepended to the format.
  --recreate-merges[=(rebase-cousins|no-rebase-cousins)]::
  	Recreate merge commits instead of flattening the history by replaying
  	merges. Merge conflict resolutions or manual amendments to merge
 -	commits are not preserved.
 +	commits are not recreated automatically, but have to be recreated
 +	manually.
  +
 -By default, or when `rebase-cousins` was specified, commits which do not have
 -`<upstream>` as direct ancestor are rebased onto `<upstream>` (or `<onto>`,
 -if specified). If the `rebase-cousins` mode is turned off, such commits will
 -retain their original branch point.
 +By default, or when `no-rebase-cousins` was specified, commits which do not
 +have `<upstream>` as direct ancestor keep their original branch point.
 +If the `rebase-cousins` mode is turned on, such commits are rebased onto
 +`<upstream>` (or `<onto>`, if specified).
  
  -p::
  --preserve-merges::
 diff --git a/builtin/rebase--helper.c b/builtin/rebase--helper.c
 index ef08fef4d14..cea99cb3235 100644
 --- a/builtin/rebase--helper.c
 +++ b/builtin/rebase--helper.c
 @@ -13,7 +13,7 @@ int cmd_rebase__helper(int argc, const char **argv, const char *prefix)
  {
  	struct replay_opts opts = REPLAY_OPTS_INIT;
  	unsigned flags = 0, keep_empty = 0, recreate_merges = 0;
 -	int abbreviate_commands = 0, no_rebase_cousins = -1;
 +	int abbreviate_commands = 0, rebase_cousins = -1;
  	enum {
  		CONTINUE = 1, ABORT, MAKE_SCRIPT, SHORTEN_OIDS, EXPAND_OIDS,
  		CHECK_TODO_LIST, SKIP_UNNECESSARY_PICKS, REARRANGE_SQUASH,
 @@ -23,7 +23,7 @@ int cmd_rebase__helper(int argc, const char **argv, const char *prefix)
  		OPT_BOOL(0, "ff", &opts.allow_ff, N_("allow fast-forward")),
  		OPT_BOOL(0, "keep-empty", &keep_empty, N_("keep empty commits")),
  		OPT_BOOL(0, "recreate-merges", &recreate_merges, N_("recreate merge commits")),
 -		OPT_BOOL(0, "no-rebase-cousins", &no_rebase_cousins,
 +		OPT_BOOL(0, "rebase-cousins", &rebase_cousins,
  			 N_("keep original branch points of cousins")),
  		OPT_CMDMODE(0, "continue", &command, N_("continue rebase"),
  				CONTINUE),
 @@ -59,10 +59,10 @@ int cmd_rebase__helper(int argc, const char **argv, const char *prefix)
  	flags |= keep_empty ? TODO_LIST_KEEP_EMPTY : 0;
  	flags |= abbreviate_commands ? TODO_LIST_ABBREVIATE_CMDS : 0;
  	flags |= recreate_merges ? TODO_LIST_RECREATE_MERGES : 0;
 -	flags |= no_rebase_cousins > 0 ? TODO_LIST_NO_REBASE_COUSINS : 0;
 +	flags |= rebase_cousins > 0 ? TODO_LIST_REBASE_COUSINS : 0;
  	flags |= command == SHORTEN_OIDS ? TODO_LIST_SHORTEN_IDS : 0;
  
 -	if (no_rebase_cousins >= 0&& !recreate_merges)
 +	if (rebase_cousins >= 0 && !recreate_merges)
  		warning(_("--[no-]rebase-cousins has no effect without "
  			  "--recreate-merges"));
  
 diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh
 index 23184c77e88..5e21e4cf269 100644
 --- a/git-rebase--interactive.sh
 +++ b/git-rebase--interactive.sh
 @@ -155,17 +155,19 @@ reschedule_last_action () {
  append_todo_help () {
  	gettext "
  Commands:
 -p, pick = use commit
 -r, reword = use commit, but edit the commit message
 -e, edit = use commit, but stop for amending
 -s, squash = use commit, but meld into previous commit
 -f, fixup = like \"squash\", but discard this commit's log message
 -x, exec = run command (the rest of the line) using shell
 -d, drop = remove commit
 -l, label = label current HEAD with a name
 -t, reset = reset HEAD to a label
 -b, bud = reset HEAD to the revision labeled 'onto'
 -m, merge = create a merge commit using a given commit's message
 +p, pick <commit> = use commit
 +r, reword <commit> = use commit, but edit the commit message
 +e, edit <commit> = use commit, but stop for amending
 +s, squash <commit> = use commit, but meld into previous commit
 +f, fixup <commit> = like \"squash\", but discard this commit's log message
 +x, exec <commit> = run command (the rest of the line) using shell
 +d, drop <commit> = remove commit
 +l, label <label> = label current HEAD with a name
 +t, reset <label> = reset HEAD to a label
 +m, merge <original-merge-commit> ( <label> | \"<label>...\" ) [<oneline>]
 +.       create a merge commit using the original merge commit's
 +.       message (or the oneline, if "-" is given). Use a quoted
 +.       list of commits to be merged for octopus merges.
  
  These lines can be re-ordered; they are executed from top to bottom.
  " | git stripspace --comment-lines >>"$todo"
 @@ -901,7 +903,7 @@ if test t != "$preserve_merges"
  then
  	git rebase--helper --make-script ${keep_empty:+--keep-empty} \
  		${recreate_merges:+--recreate-merges} \
 -		${no_rebase_cousins:+--no-rebase-cousins} \
 +		${rebase_cousins:+--rebase-cousins} \
  		$revisions ${restrict_revision+^$restrict_revision} >"$todo" ||
  	die "$(gettext "Could not generate todo list")"
  else
 diff --git a/git-rebase.sh b/git-rebase.sh
 index 3403b1416a8..58d778a2da0 100755
 --- a/git-rebase.sh
 +++ b/git-rebase.sh
 @@ -88,7 +88,7 @@ state_dir=
  # One of {'', continue, skip, abort}, as parsed from command line
  action=
  recreate_merges=
 -no_rebase_cousins=
 +rebase_cousins=
  preserve_merges=
  autosquash=
  keep_empty=
 @@ -272,8 +272,8 @@ do
  	--recreate-merges=*)
  		recreate_merges=t
  		case "${1#*=}" in
 -		rebase-cousins) no_rebase_cousins=;;
 -		no-rebase-cousins) no_rebase_cousins=t;;
 +		rebase-cousins) rebase_cousins=t;;
 +		no-rebase-cousins) rebase_cousins=;;
  		*) die "Unknown mode: $1";;
  		esac
  		test -z "$interactive_rebase" && interactive_rebase=implied
 diff --git a/sequencer.c b/sequencer.c
 index 2b4e6b12321..cd2f2ae5d53 100644
 --- a/sequencer.c
 +++ b/sequencer.c
 @@ -780,7 +780,6 @@ enum todo_command {
  	TODO_EXEC,
  	TODO_LABEL,
  	TODO_RESET,
 -	TODO_BUD,
  	TODO_MERGE,
  	/* commands that do nothing but are counted for reporting progress */
  	TODO_NOOP,
 @@ -802,7 +801,6 @@ static struct {
  	{ 'x', "exec" },
  	{ 'l', "label" },
  	{ 't', "reset" },
 -	{ 'b', "bud" },
  	{ 'm', "merge" },
  	{ 0,   "noop" },
  	{ 'd', "drop" },
 @@ -1285,7 +1283,7 @@ static int parse_insn_line(struct todo_item *item, const char *bol, char *eol)
  	padding = strspn(bol, " \t");
  	bol += padding;
  
 -	if (item->command == TODO_NOOP || item->command == TODO_BUD) {
 +	if (item->command == TODO_NOOP) {
  		if (bol != eol)
  			return error(_("%s does not accept arguments: '%s'"),
  				     command_to_string(item->command), bol);
 @@ -1311,13 +1309,13 @@ static int parse_insn_line(struct todo_item *item, const char *bol, char *eol)
  	item->arg = end_of_object_name + strspn(end_of_object_name, " \t");
  	item->arg_len = (int)(eol - item->arg);
  
 -	saved = *end_of_object_name;
  	if (item->command == TODO_MERGE && *bol == '-' &&
  	    bol + 1 == end_of_object_name) {
  		item->commit = NULL;
  		return 0;
  	}
  
 +	saved = *end_of_object_name;
  	*end_of_object_name = '\0';
  	status = get_oid(bol, &commit_oid);
  	*end_of_object_name = saved;
 @@ -1969,7 +1967,7 @@ static int safe_append(const char *filename, const char *fmt, ...)
  	}
  	if (commit_lock_file(&lock) < 0) {
  		rollback_lock_file(&lock);
 -		return error(_("failed to finalize '%s'."), filename);
 +		return error(_("failed to finalize '%s'"), filename);
  	}
  
  	return 0;
 @@ -1985,14 +1983,18 @@ static int do_label(const char *name, int len)
  	struct object_id head_oid;
  
  	strbuf_addf(&ref_name, "refs/rewritten/%.*s", len, name);
 -	strbuf_addf(&msg, "label '%.*s'", len, name);
 +	strbuf_addf(&msg, "rebase -i (label) '%.*s'", len, name);
  
  	transaction = ref_store_transaction_begin(refs, &err);
 -	if (!transaction ||
 -	    get_oid("HEAD", &head_oid) ||
 -	    ref_transaction_update(transaction, ref_name.buf, &head_oid, NULL,
 -				   0, msg.buf, &err) < 0 ||
 -	    ref_transaction_commit(transaction, &err)) {
 +	if (!transaction) {
 +		error("%s", err.buf);
 +		ret = -1;
 +	} else if (get_oid("HEAD", &head_oid)) {
 +		error(_("could not read HEAD"));
 +		ret = -1;
 +	} else if (ref_transaction_update(transaction, ref_name.buf, &head_oid,
 +					  NULL, 0, msg.buf, &err) < 0 ||
 +		   ref_transaction_commit(transaction, &err)) {
  		error("%s", err.buf);
  		ret = -1;
  	}
 @@ -2021,6 +2023,7 @@ static int do_reset(const char *name, int len)
  	if (hold_locked_index(&lock, LOCK_REPORT_ON_ERROR) < 0)
  		return -1;
  
 +	/* Determine the length of the label */
  	for (i = 0; i < len; i++)
  		if (isspace(name[i]))
  			len = i;
 @@ -2045,7 +2048,7 @@ static int do_reset(const char *name, int len)
  
  	read_cache_unmerged();
  	if (!fill_tree_descriptor(&desc, &oid)) {
 -		error(_("Failed to find tree of %s."), oid_to_hex(&oid));
 +		error(_("failed to find tree of %s"), oid_to_hex(&oid));
  		rollback_lock_file(&lock);
  		free((void *)desc.buffer);
  		strbuf_release(&ref_name);
 @@ -2097,6 +2100,12 @@ static int do_merge(struct commit *commit, const char *arg, int arg_len,
  	if (hold_locked_index(&lock, LOCK_REPORT_ON_ERROR) < 0)
  		return -1;
  
 +	head_commit = lookup_commit_reference_by_name("HEAD");
 +	if (!head_commit) {
 +		rollback_lock_file(&lock);
 +		return error(_("cannot merge without a current revision"));
 +	}
 +
  	if (commit) {
  		const char *message = get_commit_buffer(commit, NULL);
  		const char *body;
 @@ -2111,7 +2120,7 @@ static int do_merge(struct commit *commit, const char *arg, int arg_len,
  		find_commit_subject(message, &body);
  		len = strlen(body);
  		if (write_message(body, len, git_path_merge_msg(), 0) < 0) {
 -			error_errno(_("Could not write '%s'"),
 +			error_errno(_("could not write '%s'"),
  				    git_path_merge_msg());
  			unuse_commit_buffer(commit, message);
  			rollback_lock_file(&lock);
 @@ -2138,7 +2147,7 @@ static int do_merge(struct commit *commit, const char *arg, int arg_len,
  		}
  
  		if (write_message(p, len, git_path_merge_msg(), 0) < 0) {
 -			error_errno(_("Could not write '%s'"),
 +			error_errno(_("could not write '%s'"),
  				    git_path_merge_msg());
  			strbuf_release(&buf);
  			rollback_lock_file(&lock);
 @@ -2147,17 +2156,11 @@ static int do_merge(struct commit *commit, const char *arg, int arg_len,
  		strbuf_release(&buf);
  	}
  
 -	head_commit = lookup_commit_reference_by_name("HEAD");
 -	if (!head_commit) {
 -		rollback_lock_file(&lock);
 -		return error(_("Cannot merge without a current revision"));
 -	}
 -
  	/*
  	 * If HEAD is not identical to the parent of the original merge commit,
  	 * we cannot fast-forward.
  	 */
 -	can_fast_forward = commit && commit->parents &&
 +	can_fast_forward = opts->allow_ff && commit && commit->parents &&
  		!oidcmp(&commit->parents->item->object.oid,
  			&head_commit->object.oid);
  
 @@ -2411,8 +2414,6 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts)
  			res = do_label(item->arg, item->arg_len);
  		else if (item->command == TODO_RESET)
  			res = do_reset(item->arg, item->arg_len);
 -		else if (item->command == TODO_BUD)
 -			res = do_reset("onto", 4);
  		else if (item->command == TODO_MERGE) {
  			res = do_merge(item->commit,
  				       item->arg, item->arg_len, opts);
 @@ -2905,7 +2906,7 @@ static int make_script_with_merges(struct pretty_print_context *pp,
  				   unsigned flags)
  {
  	int keep_empty = flags & TODO_LIST_KEEP_EMPTY;
 -	int no_rebase_cousins = flags & TODO_LIST_NO_REBASE_COUSINS;
 +	int rebase_cousins = flags & TODO_LIST_REBASE_COUSINS;
  	struct strbuf buf = STRBUF_INIT, oneline = STRBUF_INIT;
  	struct strbuf label = STRBUF_INIT;
  	struct commit_list *commits = NULL, **tail = &commits, *iter;
 @@ -2918,9 +2919,10 @@ static int make_script_with_merges(struct pretty_print_context *pp,
  	struct label_state state = { OIDMAP_INIT, { NULL }, STRBUF_INIT };
  
  	int abbr = flags & TODO_LIST_ABBREVIATE_CMDS;
 -	const char *p = abbr ? "p" : "pick", *l = abbr ? "l" : "label",
 -		 *t = abbr ? "t" : "reset", *b = abbr ? "b" : "bud",
 -		 *m = abbr ? "m" : "merge";
 +	const char *cmd_pick = abbr ? "p" : "pick",
 +		*cmd_label = abbr ? "l" : "label",
 +		*cmd_reset = abbr ? "t" : "reset",
 +		*cmd_merge = abbr ? "m" : "merge";
  
  	oidmap_init(&commit2todo, 0);
  	oidmap_init(&state.commit2label, 0);
 @@ -2961,7 +2963,7 @@ static int make_script_with_merges(struct pretty_print_context *pp,
  			strbuf_reset(&buf);
  			if (!keep_empty && is_original_commit_empty(commit))
  				strbuf_addf(&buf, "%c ", comment_line_char);
 -			strbuf_addf(&buf, "%s %s %s", p,
 +			strbuf_addf(&buf, "%s %s %s", cmd_pick,
  				    oid_to_hex(&commit->object.oid),
  				    oneline.buf);
  
 @@ -2995,7 +2997,8 @@ static int make_script_with_merges(struct pretty_print_context *pp,
  				*(char *)p1 = '-';
  
  		strbuf_reset(&buf);
 -		strbuf_addf(&buf, "%s %s", m, oid_to_hex(&commit->object.oid));
 +		strbuf_addf(&buf, "%s %s",
 +			    cmd_merge, oid_to_hex(&commit->object.oid));
  
  		/* label the tip of merged branch */
  		oid = &to_merge->item->object.oid;
 @@ -3046,7 +3049,7 @@ static int make_script_with_merges(struct pretty_print_context *pp,
  	 * gathering commits not yet shown, reversing the list on the fly,
  	 * then outputting that list (labeling revisions as needed).
  	 */
 -	fprintf(out, "%s onto\n", l);
 +	fprintf(out, "%s onto\n", cmd_label);
  	for (iter = tips; iter; iter = iter->next) {
  		struct commit_list *list = NULL, *iter2;
  
 @@ -3071,7 +3074,7 @@ static int make_script_with_merges(struct pretty_print_context *pp,
  		}
  
  		if (!commit)
 -			fprintf(out, "%s\n", b);
 +			fprintf(out, "%s onto\n", cmd_reset);
  		else {
  			const char *to = NULL;
  
 @@ -3079,17 +3082,17 @@ static int make_script_with_merges(struct pretty_print_context *pp,
  					   &commit->object.oid);
  			if (entry)
  				to = entry->string;
 -			else if (no_rebase_cousins)
 +			else if (!rebase_cousins)
  				to = label_oid(&commit->object.oid, NULL,
  					       &state);
  
 -			if (!to || !strcmp("onto", to))
 -				fprintf(out, "%s\n", b);
 +			if (!to || !strcmp(to, "onto"))
 +				fprintf(out, "%s onto\n", cmd_reset);
  			else {
  				strbuf_reset(&oneline);
  				pretty_print_commit(pp, commit, &oneline);
 -				fprintf(out, "%s %s %s\n",
 -					t, to, oneline.buf);
 +				fprintf(out, "%s %s # %s\n",
 +					cmd_reset, to, oneline.buf);
  			}
  		}
  
 @@ -3101,7 +3104,8 @@ static int make_script_with_merges(struct pretty_print_context *pp,
  				fprintf(out, "%s\n", entry->string);
  			entry = oidmap_get(&state.commit2label, oid);
  			if (entry)
 -				fprintf(out, "%s %s\n", l, entry->string);
 +				fprintf(out, "%s %s\n",
 +					cmd_label, entry->string);
  			oidset_insert(&shown, oid);
  		}
  
 diff --git a/sequencer.h b/sequencer.h
 index 9530dba3cba..deebc6e3258 100644
 --- a/sequencer.h
 +++ b/sequencer.h
 @@ -51,12 +51,10 @@ int sequencer_remove_state(struct replay_opts *opts);
  #define TODO_LIST_RECREATE_MERGES (1U << 3)
  /*
   * When recreating merges, commits that do have the base commit as ancestor
 - * ("cousins") are rebased onto the new base by default. If those commits
 - * should keep their original branch point, this flag needs to be passed.
 - *
 - * This flag only makes sense when <base> and <onto> are different.
 + * ("cousins") are *not* rebased onto the new base by default. If those
 + * commits should be rebased onto the new base, this flag needs to be passed.
   */
 -#define TODO_LIST_NO_REBASE_COUSINS (1U << 4)
 +#define TODO_LIST_REBASE_COUSINS (1U << 4)
  int sequencer_make_script(FILE *out, int argc, const char **argv,
  			  unsigned flags);
  
 diff --git a/t/t3430-rebase-recreate-merges.sh b/t/t3430-rebase-recreate-merges.sh
 index 22930e470a4..ab51b584ff9 100755
 --- a/t/t3430-rebase-recreate-merges.sh
 +++ b/t/t3430-rebase-recreate-merges.sh
 @@ -54,11 +54,11 @@ pick D
  label onebranch
  
  # second
 -bud
 +reset onto
  pick B
  label second
  
 -bud
 +reset onto
  merge H second
  merge - onebranch Merge the topic branch 'onebranch'
  EOF
 @@ -93,18 +93,18 @@ test_expect_success 'generate correct todo list' '
  	cat >expect <<-\EOF &&
  	label onto
  
 -	bud
 +	reset onto
  	pick d9df450 B
  	label E
  
 -	bud
 +	reset onto
  	pick 5dee784 C
  	label branch-point
  	pick ca2c861 F
  	pick 088b00a G
  	label H
  
 -	reset branch-point C
 +	reset branch-point # C
  	pick 12bd07b D
  	merge 2051b56 E E
  	merge 233d48a H H
 @@ -143,7 +143,7 @@ test_expect_success 'with a branch tip that was cherry-picked already' '
  	EOF
  '
  
 -test_expect_success 'rebase cousins unless told not to' '
 +test_expect_success 'do not rebase cousins unless asked for' '
  	write_script copy-editor.sh <<-\EOF &&
  	cp "$1" "$(git rev-parse --git-path ORIGINAL-TODO)"
  	EOF
 @@ -152,10 +152,10 @@ test_expect_success 'rebase cousins unless told not to' '
  	git checkout -b cousins master &&
  	before="$(git rev-parse --verify HEAD)" &&
  	test_tick &&
 -	git rebase -i --recreate-merges=no-rebase-cousins HEAD^ &&
 +	git rebase -i --recreate-merges HEAD^ &&
  	test_cmp_rev HEAD $before &&
  	test_tick &&
 -	git rebase -i --recreate-merges HEAD^ &&
 +	git rebase -i --recreate-merges=rebase-cousins HEAD^ &&
  	test_cmp_graph HEAD^.. <<-\EOF
  	*   Merge the topic branch '\''onebranch'\''
  	|\
-- 
2.16.1.windows.1


^ permalink raw reply	[flat|nested] 276+ messages in thread

* [PATCH v2 01/10] git-rebase--interactive: clarify arguments
  2018-01-29 22:54 ` [PATCH v2 00/10] " Johannes Schindelin
@ 2018-01-29 22:54   ` Johannes Schindelin
  2018-01-29 22:54   ` [PATCH v2 02/10] sequencer: introduce new commands to reset the revision Johannes Schindelin
                     ` (11 subsequent siblings)
  12 siblings, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-01-29 22:54 UTC (permalink / raw)
  To: git; +Cc: Stefan Beller, Junio C Hamano, Jacob Keller, Stefan Beller, Philip Oakley, Eric Sunshine, Phillip Wood

From: Stefan Beller <stefanbeller@gmail.com>

Up to now each command took a commit as its first argument and ignored
the rest of the line (usually the subject of the commit)

Now that we are about to introduce commands that take different
arguments, clarify each command by giving the argument list.

Signed-off-by: Stefan Beller <sbeller@google.com>
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 git-rebase--interactive.sh | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh
index d47bd29593a..fcedece1860 100644
--- a/git-rebase--interactive.sh
+++ b/git-rebase--interactive.sh
@@ -155,13 +155,13 @@ reschedule_last_action () {
 append_todo_help () {
 	gettext "
 Commands:
-p, pick = use commit
-r, reword = use commit, but edit the commit message
-e, edit = use commit, but stop for amending
-s, squash = use commit, but meld into previous commit
-f, fixup = like \"squash\", but discard this commit's log message
-x, exec = run command (the rest of the line) using shell
-d, drop = remove commit
+p, pick <commit> = use commit
+r, reword <commit> = use commit, but edit the commit message
+e, edit <commit> = use commit, but stop for amending
+s, squash <commit> = use commit, but meld into previous commit
+f, fixup <commit> = like \"squash\", but discard this commit's log message
+x, exec <commit> = run command (the rest of the line) using shell
+d, drop <commit> = remove commit
 
 These lines can be re-ordered; they are executed from top to bottom.
 " | git stripspace --comment-lines >>"$todo"
-- 
2.16.1.windows.1



^ permalink raw reply	[flat|nested] 276+ messages in thread

* [PATCH v2 02/10] sequencer: introduce new commands to reset the revision
  2018-01-29 22:54 ` [PATCH v2 00/10] " Johannes Schindelin
  2018-01-29 22:54   ` [PATCH v2 01/10] git-rebase--interactive: clarify arguments Johannes Schindelin
@ 2018-01-29 22:54   ` Johannes Schindelin
  2018-01-30  8:06     ` Eric Sunshine
  2018-01-30 20:17     ` Stefan Beller
  2018-01-29 22:54   ` [PATCH v2 03/10] sequencer: introduce the `merge` command Johannes Schindelin
                     ` (10 subsequent siblings)
  12 siblings, 2 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-01-29 22:54 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Jacob Keller, Stefan Beller, Philip Oakley, Eric Sunshine, Phillip Wood

In the upcoming commits, we will teach the sequencer to recreate merges.
This will be done in a very different way from the unfortunate design of
`git rebase --preserve-merges` (which does not allow for reordering
commits, or changing the branch topology).

The main idea is to introduce new todo list commands, to support
labeling the current revision with a given name, resetting the current
revision to a previous state, merging labeled revisions.

This idea was developed in Git for Windows' Git garden shears (that are
used to maintain the "thicket of branches" on top of upstream Git), and
this patch is part of the effort to make it available to a wider
audience, as well as to make the entire process more robust (by
implementing it in a safe and portable language rather than a Unix shell
script).

This commit implements the commands to label, and to reset to, given
revisions. The syntax is:

	label <name>
	reset <name>

Internally, the `label <name>` command creates the ref
`refs/rewritten/<name>`. This makes it possible to work with the labeled
revisions interactively, or in a scripted fashion (e.g. via the todo
list command `exec`).

Later in this patch series, we will mark the `refs/rewritten/` refs as
worktree-local, to allow for interactive rebases to be run in parallel in
worktrees linked to the same repository.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 git-rebase--interactive.sh |   2 +
 sequencer.c                | 180 ++++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 179 insertions(+), 3 deletions(-)

diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh
index fcedece1860..7e5281e74aa 100644
--- a/git-rebase--interactive.sh
+++ b/git-rebase--interactive.sh
@@ -162,6 +162,8 @@ s, squash <commit> = use commit, but meld into previous commit
 f, fixup <commit> = like \"squash\", but discard this commit's log message
 x, exec <commit> = run command (the rest of the line) using shell
 d, drop <commit> = remove commit
+l, label <label> = label current HEAD with a name
+t, reset <label> = reset HEAD to a label
 
 These lines can be re-ordered; they are executed from top to bottom.
 " | git stripspace --comment-lines >>"$todo"
diff --git a/sequencer.c b/sequencer.c
index 4d3f60594cb..92ca8d2adee 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -21,6 +21,8 @@
 #include "log-tree.h"
 #include "wt-status.h"
 #include "hashmap.h"
+#include "unpack-trees.h"
+#include "worktree.h"
 
 #define GIT_REFLOG_ACTION "GIT_REFLOG_ACTION"
 
@@ -116,6 +118,13 @@ static GIT_PATH_FUNC(rebase_path_stopped_sha, "rebase-merge/stopped-sha")
 static GIT_PATH_FUNC(rebase_path_rewritten_list, "rebase-merge/rewritten-list")
 static GIT_PATH_FUNC(rebase_path_rewritten_pending,
 	"rebase-merge/rewritten-pending")
+
+/*
+ * The path of the file listing refs that need to be deleted after the rebase
+ * finishes. This is used by the `merge` command.
+ */
+static GIT_PATH_FUNC(rebase_path_refs_to_delete, "rebase-merge/refs-to-delete")
+
 /*
  * The following files are written by git-rebase just after parsing the
  * command-line (and are only consumed, not modified, by the sequencer).
@@ -767,6 +776,8 @@ enum todo_command {
 	TODO_SQUASH,
 	/* commands that do something else than handling a single commit */
 	TODO_EXEC,
+	TODO_LABEL,
+	TODO_RESET,
 	/* commands that do nothing but are counted for reporting progress */
 	TODO_NOOP,
 	TODO_DROP,
@@ -785,6 +796,8 @@ static struct {
 	{ 'f', "fixup" },
 	{ 's', "squash" },
 	{ 'x', "exec" },
+	{ 'l', "label" },
+	{ 't', "reset" },
 	{ 0,   "noop" },
 	{ 'd', "drop" },
 	{ 0,   NULL }
@@ -1253,7 +1266,8 @@ static int parse_insn_line(struct todo_item *item, const char *bol, char *eol)
 		if (skip_prefix(bol, todo_command_info[i].str, &bol)) {
 			item->command = i;
 			break;
-		} else if (bol[1] == ' ' && *bol == todo_command_info[i].c) {
+		} else if ((bol + 1 == eol || bol[1] == ' ') &&
+			   *bol == todo_command_info[i].c) {
 			bol++;
 			item->command = i;
 			break;
@@ -1279,7 +1293,8 @@ static int parse_insn_line(struct todo_item *item, const char *bol, char *eol)
 		return error(_("missing arguments for %s"),
 			     command_to_string(item->command));
 
-	if (item->command == TODO_EXEC) {
+	if (item->command == TODO_EXEC || item->command == TODO_LABEL ||
+	    item->command == TODO_RESET) {
 		item->commit = NULL;
 		item->arg = bol;
 		item->arg_len = (int)(eol - bol);
@@ -1919,6 +1934,144 @@ static int do_exec(const char *command_line)
 	return status;
 }
 
+static int safe_append(const char *filename, const char *fmt, ...)
+{
+	va_list ap;
+	struct lock_file lock = LOCK_INIT;
+	int fd = hold_lock_file_for_update(&lock, filename, 0);
+	struct strbuf buf = STRBUF_INIT;
+
+	if (fd < 0)
+		return error_errno(_("could not lock '%s'"), filename);
+
+	if (strbuf_read_file(&buf, filename, 0) < 0 && errno != ENOENT)
+		return error_errno(_("could not read '%s'"), filename);
+	strbuf_complete(&buf, '\n');
+	va_start(ap, fmt);
+	strbuf_vaddf(&buf, fmt, ap);
+	va_end(ap);
+
+	if (write_in_full(fd, buf.buf, buf.len) < 0) {
+		rollback_lock_file(&lock);
+		return error_errno(_("could not write to '%s'"), filename);
+	}
+	if (commit_lock_file(&lock) < 0) {
+		rollback_lock_file(&lock);
+		return error(_("failed to finalize '%s'"), filename);
+	}
+
+	return 0;
+}
+
+static int do_label(const char *name, int len)
+{
+	struct ref_store *refs = get_main_ref_store();
+	struct ref_transaction *transaction;
+	struct strbuf ref_name = STRBUF_INIT, err = STRBUF_INIT;
+	struct strbuf msg = STRBUF_INIT;
+	int ret = 0;
+	struct object_id head_oid;
+
+	strbuf_addf(&ref_name, "refs/rewritten/%.*s", len, name);
+	strbuf_addf(&msg, "rebase -i (label) '%.*s'", len, name);
+
+	transaction = ref_store_transaction_begin(refs, &err);
+	if (!transaction) {
+		error("%s", err.buf);
+		ret = -1;
+	} else if (get_oid("HEAD", &head_oid)) {
+		error(_("could not read HEAD"));
+		ret = -1;
+	} else if (ref_transaction_update(transaction, ref_name.buf, &head_oid,
+					  NULL, 0, msg.buf, &err) < 0 ||
+		   ref_transaction_commit(transaction, &err)) {
+		error("%s", err.buf);
+		ret = -1;
+	}
+	ref_transaction_free(transaction);
+	strbuf_release(&err);
+	strbuf_release(&msg);
+
+	if (!ret)
+		ret = safe_append(rebase_path_refs_to_delete(),
+				  "%s\n", ref_name.buf);
+	strbuf_release(&ref_name);
+
+	return ret;
+}
+
+static int do_reset(const char *name, int len)
+{
+	struct strbuf ref_name = STRBUF_INIT;
+	struct object_id oid;
+	struct lock_file lock = LOCK_INIT;
+	struct tree_desc desc;
+	struct tree *tree;
+	struct unpack_trees_options opts;
+	int ret = 0, i;
+
+	if (hold_locked_index(&lock, LOCK_REPORT_ON_ERROR) < 0)
+		return -1;
+
+	/* Determine the length of the label */
+	for (i = 0; i < len; i++)
+		if (isspace(name[i]))
+			len = i;
+
+	strbuf_addf(&ref_name, "refs/rewritten/%.*s", len, name);
+	if (get_oid(ref_name.buf, &oid) &&
+	    get_oid(ref_name.buf + strlen("refs/rewritten/"), &oid)) {
+		error(_("could not read '%s'"), ref_name.buf);
+		rollback_lock_file(&lock);
+		strbuf_release(&ref_name);
+		return -1;
+	}
+
+	memset(&opts, 0, sizeof(opts));
+	opts.head_idx = 1;
+	opts.src_index = &the_index;
+	opts.dst_index = &the_index;
+	opts.fn = oneway_merge;
+	opts.merge = 1;
+	opts.update = 1;
+	opts.reset = 1;
+
+	read_cache_unmerged();
+	if (!fill_tree_descriptor(&desc, &oid)) {
+		error(_("failed to find tree of %s"), oid_to_hex(&oid));
+		rollback_lock_file(&lock);
+		free((void *)desc.buffer);
+		strbuf_release(&ref_name);
+		return -1;
+	}
+
+	if (unpack_trees(1, &desc, &opts)) {
+		rollback_lock_file(&lock);
+		free((void *)desc.buffer);
+		strbuf_release(&ref_name);
+		return -1;
+	}
+
+	tree = parse_tree_indirect(&oid);
+	prime_cache_tree(&the_index, tree);
+
+	if (write_locked_index(&the_index, &lock, COMMIT_LOCK) < 0)
+		ret = error(_("could not write index"));
+	free((void *)desc.buffer);
+
+	if (!ret) {
+		struct strbuf msg = STRBUF_INIT;
+
+		strbuf_addf(&msg, "(rebase -i) reset '%.*s'", len, name);
+		ret = update_ref(msg.buf, "HEAD", &oid, NULL, 0,
+				 UPDATE_REFS_MSG_ON_ERR);
+		strbuf_release(&msg);
+	}
+
+	strbuf_release(&ref_name);
+	return ret;
+}
+
 static int is_final_fixup(struct todo_list *todo_list)
 {
 	int i = todo_list->current;
@@ -2102,7 +2255,11 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts)
 				/* `current` will be incremented below */
 				todo_list->current = -1;
 			}
-		} else if (!is_noop(item->command))
+		} else if (item->command == TODO_LABEL)
+			res = do_label(item->arg, item->arg_len);
+		else if (item->command == TODO_RESET)
+			res = do_reset(item->arg, item->arg_len);
+		else if (!is_noop(item->command))
 			return error(_("unknown command %d"), item->command);
 
 		todo_list->current++;
@@ -2207,6 +2364,23 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts)
 		}
 		apply_autostash(opts);
 
+		strbuf_reset(&buf);
+		if (strbuf_read_file(&buf, rebase_path_refs_to_delete(), 0)
+		    > 0) {
+			char *p = buf.buf;
+			while (*p) {
+				char *eol = strchr(p, '\n');
+				if (eol)
+					*eol = '\0';
+				if (delete_ref("(rebase -i) cleanup",
+					       p, NULL, 0) < 0)
+					warning(_("could not delete '%s'"), p);
+				if (!eol)
+					break;
+				p = eol + 1;
+			}
+		}
+
 		fprintf(stderr, "Successfully rebased and updated %s.\n",
 			head_ref.buf);
 
-- 
2.16.1.windows.1



^ permalink raw reply	[flat|nested] 276+ messages in thread

* [PATCH v2 03/10] sequencer: introduce the `merge` command
  2018-01-29 22:54 ` [PATCH v2 00/10] " Johannes Schindelin
  2018-01-29 22:54   ` [PATCH v2 01/10] git-rebase--interactive: clarify arguments Johannes Schindelin
  2018-01-29 22:54   ` [PATCH v2 02/10] sequencer: introduce new commands to reset the revision Johannes Schindelin
@ 2018-01-29 22:54   ` Johannes Schindelin
  2018-01-29 22:54   ` [PATCH v2 04/10] sequencer: fast-forward merge commits, if possible Johannes Schindelin
                     ` (9 subsequent siblings)
  12 siblings, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-01-29 22:54 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Jacob Keller, Stefan Beller, Philip Oakley, Eric Sunshine, Phillip Wood

This patch is part of the effort to reimplement `--preserve-merges` with
a substantially improved design, a design that has been developed in the
Git for Windows project to maintain the dozens of Windows-specific patch
series on top of upstream Git.

The previous patch implemented the `label` and `reset` commands to label
commits and to reset to a labeled commits. This patch adds the `merge`
command, with the following syntax:

	merge <commit> <rev> <oneline>

The <commit> parameter in this instance is the *original* merge commit,
whose author and message will be used for the to-be-created merge
commit.

The <rev> parameter refers to the (possibly rewritten) revision to
merge. Let's see an example of a todo list:

	label onto

	# Branch abc
	reset onto
	pick deadbeef Hello, world!
	label abc

	reset onto
	pick cafecafe And now for something completely different
	merge baaabaaa abc Merge the branch 'abc' into master

To support creating *new* merges, i.e. without copying the commit
message from an existing commit, use the special value `-` as <commit>
parameter (in which case the text after the <rev> parameter is used as
commit message):

	merge - abc This will be the actual commit message of the merge

This comes in handy when splitting a branch into two or more branches.

Note: this patch only adds support for recursive merges, to keep things
simple. Support for octopus merges will be added later in a separate
patch series, support for merges using strategies other than the
recursive merge is left for the future.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 git-rebase--interactive.sh |   4 ++
 sequencer.c                | 146 +++++++++++++++++++++++++++++++++++++++++++--
 2 files changed, 146 insertions(+), 4 deletions(-)

diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh
index 7e5281e74aa..d6fd30f6c09 100644
--- a/git-rebase--interactive.sh
+++ b/git-rebase--interactive.sh
@@ -164,6 +164,10 @@ x, exec <commit> = run command (the rest of the line) using shell
 d, drop <commit> = remove commit
 l, label <label> = label current HEAD with a name
 t, reset <label> = reset HEAD to a label
+m, merge <original-merge-commit> ( <label> | \"<label>...\" ) [<oneline>]
+.       create a merge commit using the original merge commit's
+.       message (or the oneline, if "-" is given). Use a quoted
+.       list of commits to be merged for octopus merges.
 
 These lines can be re-ordered; they are executed from top to bottom.
 " | git stripspace --comment-lines >>"$todo"
diff --git a/sequencer.c b/sequencer.c
index 92ca8d2adee..dfc9f9e13cd 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -778,6 +778,7 @@ enum todo_command {
 	TODO_EXEC,
 	TODO_LABEL,
 	TODO_RESET,
+	TODO_MERGE,
 	/* commands that do nothing but are counted for reporting progress */
 	TODO_NOOP,
 	TODO_DROP,
@@ -798,6 +799,7 @@ static struct {
 	{ 'x', "exec" },
 	{ 'l', "label" },
 	{ 't', "reset" },
+	{ 'm', "merge" },
 	{ 0,   "noop" },
 	{ 'd', "drop" },
 	{ 0,   NULL }
@@ -1302,14 +1304,20 @@ static int parse_insn_line(struct todo_item *item, const char *bol, char *eol)
 	}
 
 	end_of_object_name = (char *) bol + strcspn(bol, " \t\n");
+	item->arg = end_of_object_name + strspn(end_of_object_name, " \t");
+	item->arg_len = (int)(eol - item->arg);
+
+	if (item->command == TODO_MERGE && *bol == '-' &&
+	    bol + 1 == end_of_object_name) {
+		item->commit = NULL;
+		return 0;
+	}
+
 	saved = *end_of_object_name;
 	*end_of_object_name = '\0';
 	status = get_oid(bol, &commit_oid);
 	*end_of_object_name = saved;
 
-	item->arg = end_of_object_name + strspn(end_of_object_name, " \t");
-	item->arg_len = (int)(eol - item->arg);
-
 	if (status < 0)
 		return -1;
 
@@ -2072,6 +2080,132 @@ static int do_reset(const char *name, int len)
 	return ret;
 }
 
+static int do_merge(struct commit *commit, const char *arg, int arg_len,
+		    struct replay_opts *opts)
+{
+	int merge_arg_len;
+	struct strbuf ref_name = STRBUF_INIT;
+	struct commit *head_commit, *merge_commit, *i;
+	struct commit_list *common, *j, *reversed = NULL;
+	struct merge_options o;
+	int ret;
+	static struct lock_file lock;
+
+	for (merge_arg_len = 0; merge_arg_len < arg_len; merge_arg_len++)
+		if (isspace(arg[merge_arg_len]))
+			break;
+
+	if (hold_locked_index(&lock, LOCK_REPORT_ON_ERROR) < 0)
+		return -1;
+
+	head_commit = lookup_commit_reference_by_name("HEAD");
+	if (!head_commit) {
+		rollback_lock_file(&lock);
+		return error(_("cannot merge without a current revision"));
+	}
+
+	if (commit) {
+		const char *message = get_commit_buffer(commit, NULL);
+		const char *body;
+		int len;
+
+		if (!message) {
+			rollback_lock_file(&lock);
+			return error(_("could not get commit message of '%s'"),
+				     oid_to_hex(&commit->object.oid));
+		}
+		write_author_script(message);
+		find_commit_subject(message, &body);
+		len = strlen(body);
+		if (write_message(body, len, git_path_merge_msg(), 0) < 0) {
+			error_errno(_("could not write '%s'"),
+				    git_path_merge_msg());
+			unuse_commit_buffer(commit, message);
+			rollback_lock_file(&lock);
+			return -1;
+		}
+		unuse_commit_buffer(commit, message);
+	} else {
+		const char *p = arg + merge_arg_len;
+		struct strbuf buf = STRBUF_INIT;
+		int len;
+
+		strbuf_addf(&buf, "author %s", git_author_info(0));
+		write_author_script(buf.buf);
+		strbuf_reset(&buf);
+
+		p += strspn(p, " \t");
+		if (*p)
+			len = strlen(p);
+		else {
+			strbuf_addf(&buf, "Merge branch '%.*s'",
+				    merge_arg_len, arg);
+			p = buf.buf;
+			len = buf.len;
+		}
+
+		if (write_message(p, len, git_path_merge_msg(), 0) < 0) {
+			error_errno(_("could not write '%s'"),
+				    git_path_merge_msg());
+			strbuf_release(&buf);
+			rollback_lock_file(&lock);
+			return -1;
+		}
+		strbuf_release(&buf);
+	}
+
+	strbuf_addf(&ref_name, "refs/rewritten/%.*s", merge_arg_len, arg);
+	merge_commit = lookup_commit_reference_by_name(ref_name.buf);
+	if (!merge_commit) {
+		/* fall back to non-rewritten ref or commit */
+		strbuf_splice(&ref_name, 0, strlen("refs/rewritten/"), "", 0);
+		merge_commit = lookup_commit_reference_by_name(ref_name.buf);
+	}
+	if (!merge_commit) {
+		error(_("could not resolve '%s'"), ref_name.buf);
+		strbuf_release(&ref_name);
+		rollback_lock_file(&lock);
+		return -1;
+	}
+	write_message(oid_to_hex(&merge_commit->object.oid), GIT_SHA1_HEXSZ,
+		      git_path_merge_head(), 0);
+	write_message("no-ff", 5, git_path_merge_mode(), 0);
+
+	common = get_merge_bases(head_commit, merge_commit);
+	for (j = common; j; j = j->next)
+		commit_list_insert(j->item, &reversed);
+	free_commit_list(common);
+
+	read_cache();
+	init_merge_options(&o);
+	o.branch1 = "HEAD";
+	o.branch2 = ref_name.buf;
+	o.buffer_output = 2;
+
+	ret = merge_recursive(&o, head_commit, merge_commit, reversed, &i);
+	if (ret <= 0)
+		fputs(o.obuf.buf, stdout);
+	strbuf_release(&o.obuf);
+	if (ret < 0) {
+		strbuf_release(&ref_name);
+		rollback_lock_file(&lock);
+		return error(_("conflicts while merging '%.*s'"),
+			     merge_arg_len, arg);
+	}
+
+	if (active_cache_changed &&
+	    write_locked_index(&the_index, &lock, COMMIT_LOCK)) {
+		strbuf_release(&ref_name);
+		return error(_("merge: Unable to write new index file"));
+	}
+	rollback_lock_file(&lock);
+
+	ret = run_git_commit(git_path_merge_msg(), opts, 0);
+	strbuf_release(&ref_name);
+
+	return ret;
+}
+
 static int is_final_fixup(struct todo_list *todo_list)
 {
 	int i = todo_list->current;
@@ -2259,6 +2393,9 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts)
 			res = do_label(item->arg, item->arg_len);
 		else if (item->command == TODO_RESET)
 			res = do_reset(item->arg, item->arg_len);
+		else if (item->command == TODO_MERGE)
+			res = do_merge(item->commit,
+				       item->arg, item->arg_len, opts);
 		else if (!is_noop(item->command))
 			return error(_("unknown command %d"), item->command);
 
@@ -2758,7 +2895,8 @@ int transform_todos(unsigned flags)
 					  oid_to_hex(&item->commit->object.oid);
 
 			strbuf_addf(&buf, " %s", oid);
-		}
+		} else if (item->command == TODO_MERGE)
+			strbuf_addstr(&buf, " -");
 		/* add all the rest */
 		if (!item->arg_len)
 			strbuf_addch(&buf, '\n');
-- 
2.16.1.windows.1



^ permalink raw reply	[flat|nested] 276+ messages in thread

* [PATCH v2 04/10] sequencer: fast-forward merge commits, if possible
  2018-01-29 22:54 ` [PATCH v2 00/10] " Johannes Schindelin
                     ` (2 preceding siblings ...)
  2018-01-29 22:54   ` [PATCH v2 03/10] sequencer: introduce the `merge` command Johannes Schindelin
@ 2018-01-29 22:54   ` Johannes Schindelin
  2018-01-29 22:54   ` [PATCH v2 05/10] rebase-helper --make-script: introduce a flag to recreate merges Johannes Schindelin
                     ` (8 subsequent siblings)
  12 siblings, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-01-29 22:54 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Jacob Keller, Stefan Beller, Philip Oakley, Eric Sunshine, Phillip Wood

Just like with regular `pick` commands, if we are trying to recreate a
merge commit, we now test whether the parents of said commit match HEAD
and the commits to be merged, and fast-forward if possible.

This is not only faster, but also avoids unnecessary proliferation of
new objects.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 sequencer.c | 21 ++++++++++++++++++++-
 1 file changed, 20 insertions(+), 1 deletion(-)

diff --git a/sequencer.c b/sequencer.c
index dfc9f9e13cd..df61e7883b3 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -2088,7 +2088,7 @@ static int do_merge(struct commit *commit, const char *arg, int arg_len,
 	struct commit *head_commit, *merge_commit, *i;
 	struct commit_list *common, *j, *reversed = NULL;
 	struct merge_options o;
-	int ret;
+	int can_fast_forward, ret;
 	static struct lock_file lock;
 
 	for (merge_arg_len = 0; merge_arg_len < arg_len; merge_arg_len++)
@@ -2154,6 +2154,14 @@ static int do_merge(struct commit *commit, const char *arg, int arg_len,
 		strbuf_release(&buf);
 	}
 
+	/*
+	 * If HEAD is not identical to the parent of the original merge commit,
+	 * we cannot fast-forward.
+	 */
+	can_fast_forward = opts->allow_ff && commit && commit->parents &&
+		!oidcmp(&commit->parents->item->object.oid,
+			&head_commit->object.oid);
+
 	strbuf_addf(&ref_name, "refs/rewritten/%.*s", merge_arg_len, arg);
 	merge_commit = lookup_commit_reference_by_name(ref_name.buf);
 	if (!merge_commit) {
@@ -2167,6 +2175,17 @@ static int do_merge(struct commit *commit, const char *arg, int arg_len,
 		rollback_lock_file(&lock);
 		return -1;
 	}
+
+	if (can_fast_forward && commit->parents->next &&
+	    !commit->parents->next->next &&
+	    !oidcmp(&commit->parents->next->item->object.oid,
+		    &merge_commit->object.oid)) {
+		strbuf_release(&ref_name);
+		rollback_lock_file(&lock);
+		return fast_forward_to(&commit->object.oid,
+				       &head_commit->object.oid, 0, opts);
+	}
+
 	write_message(oid_to_hex(&merge_commit->object.oid), GIT_SHA1_HEXSZ,
 		      git_path_merge_head(), 0);
 	write_message("no-ff", 5, git_path_merge_mode(), 0);
-- 
2.16.1.windows.1



^ permalink raw reply	[flat|nested] 276+ messages in thread

* [PATCH v2 05/10] rebase-helper --make-script: introduce a flag to recreate merges
  2018-01-29 22:54 ` [PATCH v2 00/10] " Johannes Schindelin
                     ` (3 preceding siblings ...)
  2018-01-29 22:54   ` [PATCH v2 04/10] sequencer: fast-forward merge commits, if possible Johannes Schindelin
@ 2018-01-29 22:54   ` Johannes Schindelin
  2018-01-29 22:54   ` [PATCH v2 06/10] rebase: introduce the --recreate-merges option Johannes Schindelin
                     ` (7 subsequent siblings)
  12 siblings, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-01-29 22:54 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Jacob Keller, Stefan Beller, Philip Oakley, Eric Sunshine, Phillip Wood

The sequencer just learned new commands intended to recreate branch
structure (similar in spirit to --preserve-merges, but with a
substantially less-broken design).

Let's allow the rebase--helper to generate todo lists making use of
these commands, triggered by the new --recreate-merges option. For a
commit topology like this (where the HEAD points to C):

	- A - B - C
	    \   /
	      D

the generated todo list would look like this:

	# branch D
	pick 0123 A
	label branch-point
	pick 1234 D
	label D

	reset branch-point
	pick 2345 B
	merge 3456 D C

To keep things simple, we first only implement support for merge commits
with exactly two parents, leaving support for octopus merges to a later
patch in this patch series.

As a special, hard-coded label, all merge-recreating todo lists start with
the command `label onto` so that we can later always refer to the revision
onto which everything is rebased.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 builtin/rebase--helper.c |   4 +-
 sequencer.c              | 346 ++++++++++++++++++++++++++++++++++++++++++++++-
 sequencer.h              |   1 +
 3 files changed, 348 insertions(+), 3 deletions(-)

diff --git a/builtin/rebase--helper.c b/builtin/rebase--helper.c
index 7daee544b7b..a34ab5c0655 100644
--- a/builtin/rebase--helper.c
+++ b/builtin/rebase--helper.c
@@ -12,7 +12,7 @@ static const char * const builtin_rebase_helper_usage[] = {
 int cmd_rebase__helper(int argc, const char **argv, const char *prefix)
 {
 	struct replay_opts opts = REPLAY_OPTS_INIT;
-	unsigned flags = 0, keep_empty = 0;
+	unsigned flags = 0, keep_empty = 0, recreate_merges = 0;
 	int abbreviate_commands = 0;
 	enum {
 		CONTINUE = 1, ABORT, MAKE_SCRIPT, SHORTEN_OIDS, EXPAND_OIDS,
@@ -22,6 +22,7 @@ int cmd_rebase__helper(int argc, const char **argv, const char *prefix)
 	struct option options[] = {
 		OPT_BOOL(0, "ff", &opts.allow_ff, N_("allow fast-forward")),
 		OPT_BOOL(0, "keep-empty", &keep_empty, N_("keep empty commits")),
+		OPT_BOOL(0, "recreate-merges", &recreate_merges, N_("recreate merge commits")),
 		OPT_CMDMODE(0, "continue", &command, N_("continue rebase"),
 				CONTINUE),
 		OPT_CMDMODE(0, "abort", &command, N_("abort rebase"),
@@ -55,6 +56,7 @@ int cmd_rebase__helper(int argc, const char **argv, const char *prefix)
 
 	flags |= keep_empty ? TODO_LIST_KEEP_EMPTY : 0;
 	flags |= abbreviate_commands ? TODO_LIST_ABBREVIATE_CMDS : 0;
+	flags |= recreate_merges ? TODO_LIST_RECREATE_MERGES : 0;
 	flags |= command == SHORTEN_OIDS ? TODO_LIST_SHORTEN_IDS : 0;
 
 	if (command == CONTINUE && argc == 1)
diff --git a/sequencer.c b/sequencer.c
index df61e7883b3..d5af315a440 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -23,6 +23,8 @@
 #include "hashmap.h"
 #include "unpack-trees.h"
 #include "worktree.h"
+#include "oidmap.h"
+#include "oidset.h"
 
 #define GIT_REFLOG_ACTION "GIT_REFLOG_ACTION"
 
@@ -2786,6 +2788,338 @@ void append_signoff(struct strbuf *msgbuf, int ignore_footer, unsigned flag)
 	strbuf_release(&sob);
 }
 
+struct labels_entry {
+	struct hashmap_entry entry;
+	char label[FLEX_ARRAY];
+};
+
+static int labels_cmp(const void *fndata, const struct labels_entry *a,
+		      const struct labels_entry *b, const void *key)
+{
+	return key ? strcmp(a->label, key) : strcmp(a->label, b->label);
+}
+
+struct string_entry {
+	struct oidmap_entry entry;
+	char string[FLEX_ARRAY];
+};
+
+struct label_state {
+	struct oidmap commit2label;
+	struct hashmap labels;
+	struct strbuf buf;
+};
+
+static const char *label_oid(struct object_id *oid, const char *label,
+			     struct label_state *state)
+{
+	struct labels_entry *labels_entry;
+	struct string_entry *string_entry;
+	struct object_id dummy;
+	size_t len;
+	int i;
+
+	string_entry = oidmap_get(&state->commit2label, oid);
+	if (string_entry)
+		return string_entry->string;
+
+	/*
+	 * For "uninteresting" commits, i.e. commits that are not to be
+	 * rebased, and which can therefore not be labeled, we use a unique
+	 * abbreviation of the commit name. This is slightly more complicated
+	 * than calling find_unique_abbrev() because we also need to make
+	 * sure that the abbreviation does not conflict with any other
+	 * label.
+	 *
+	 * We disallow "interesting" commits to be labeled by a string that
+	 * is a valid full-length hash, to ensure that we always can find an
+	 * abbreviation for any uninteresting commit's names that does not
+	 * clash with any other label.
+	 */
+	if (!label) {
+		char *p;
+
+		strbuf_reset(&state->buf);
+		strbuf_grow(&state->buf, GIT_SHA1_HEXSZ);
+		label = p = state->buf.buf;
+
+		find_unique_abbrev_r(p, oid->hash, default_abbrev);
+
+		/*
+		 * We may need to extend the abbreviated hash so that there is
+		 * no conflicting label.
+		 */
+		if (hashmap_get_from_hash(&state->labels, strihash(p), p)) {
+			size_t i = strlen(p) + 1;
+
+			oid_to_hex_r(p, oid);
+			for (; i < GIT_SHA1_HEXSZ; i++) {
+				char save = p[i];
+				p[i] = '\0';
+				if (!hashmap_get_from_hash(&state->labels,
+							   strihash(p), p))
+					break;
+				p[i] = save;
+			}
+		}
+	} else if (((len = strlen(label)) == GIT_SHA1_RAWSZ &&
+		    !get_oid_hex(label, &dummy)) ||
+		   hashmap_get_from_hash(&state->labels,
+					 strihash(label), label)) {
+		/*
+		 * If the label already exists, or if the label is a valid full
+		 * OID, we append a dash and a number to make it unique.
+		 */
+		struct strbuf *buf = &state->buf;
+
+		strbuf_reset(buf);
+		strbuf_add(buf, label, len);
+
+		for (i = 2; ; i++) {
+			strbuf_setlen(buf, len);
+			strbuf_addf(buf, "-%d", i);
+			if (!hashmap_get_from_hash(&state->labels,
+						   strihash(buf->buf),
+						   buf->buf))
+				break;
+		}
+
+		label = buf->buf;
+	}
+
+	FLEX_ALLOC_STR(labels_entry, label, label);
+	hashmap_entry_init(labels_entry, strihash(label));
+	hashmap_add(&state->labels, labels_entry);
+
+	FLEX_ALLOC_STR(string_entry, string, label);
+	oidcpy(&string_entry->entry.oid, oid);
+	oidmap_put(&state->commit2label, string_entry);
+
+	return string_entry->string;
+}
+
+static int make_script_with_merges(struct pretty_print_context *pp,
+				   struct rev_info *revs, FILE *out,
+				   unsigned flags)
+{
+	int keep_empty = flags & TODO_LIST_KEEP_EMPTY;
+	struct strbuf buf = STRBUF_INIT, oneline = STRBUF_INIT;
+	struct strbuf label = STRBUF_INIT;
+	struct commit_list *commits = NULL, **tail = &commits, *iter;
+	struct commit_list *tips = NULL, **tips_tail = &tips;
+	struct commit *commit;
+	struct oidmap commit2todo = OIDMAP_INIT;
+	struct string_entry *entry;
+	struct oidset interesting = OIDSET_INIT, child_seen = OIDSET_INIT,
+		shown = OIDSET_INIT;
+	struct label_state state = { OIDMAP_INIT, { NULL }, STRBUF_INIT };
+
+	int abbr = flags & TODO_LIST_ABBREVIATE_CMDS;
+	const char *cmd_pick = abbr ? "p" : "pick",
+		*cmd_label = abbr ? "l" : "label",
+		*cmd_reset = abbr ? "t" : "reset",
+		*cmd_merge = abbr ? "m" : "merge";
+
+	oidmap_init(&commit2todo, 0);
+	oidmap_init(&state.commit2label, 0);
+	hashmap_init(&state.labels, (hashmap_cmp_fn) labels_cmp, NULL, 0);
+	strbuf_init(&state.buf, 32);
+
+	if (revs->cmdline.nr && (revs->cmdline.rev[0].flags & BOTTOM)) {
+		struct object_id *oid = &revs->cmdline.rev[0].item->oid;
+		FLEX_ALLOC_STR(entry, string, "onto");
+		oidcpy(&entry->entry.oid, oid);
+		oidmap_put(&state.commit2label, entry);
+	}
+
+	/*
+	 * First phase:
+	 * - get onelines for all commits
+	 * - gather all branch tips (i.e. 2nd or later parents of merges)
+	 * - label all branch tips
+	 */
+	while ((commit = get_revision(revs))) {
+		struct commit_list *to_merge;
+		int is_octopus;
+		const char *p1, *p2;
+		struct object_id *oid;
+
+		tail = &commit_list_insert(commit, tail)->next;
+		oidset_insert(&interesting, &commit->object.oid);
+
+		if ((commit->object.flags & PATCHSAME))
+			continue;
+
+		strbuf_reset(&oneline);
+		pretty_print_commit(pp, commit, &oneline);
+
+		to_merge = commit->parents ? commit->parents->next : NULL;
+		if (!to_merge) {
+			/* non-merge commit: easy case */
+			strbuf_reset(&buf);
+			if (!keep_empty && is_original_commit_empty(commit))
+				strbuf_addf(&buf, "%c ", comment_line_char);
+			strbuf_addf(&buf, "%s %s %s", cmd_pick,
+				    oid_to_hex(&commit->object.oid),
+				    oneline.buf);
+
+			FLEX_ALLOC_STR(entry, string, buf.buf);
+			oidcpy(&entry->entry.oid, &commit->object.oid);
+			oidmap_put(&commit2todo, entry);
+
+			continue;
+		}
+
+		is_octopus = to_merge && to_merge->next;
+
+		if (is_octopus)
+			BUG("Octopus merges not yet supported");
+
+		/* Create a label */
+		strbuf_reset(&label);
+		if (skip_prefix(oneline.buf, "Merge ", &p1) &&
+		    (p1 = strchr(p1, '\'')) &&
+		    (p2 = strchr(++p1, '\'')))
+			strbuf_add(&label, p1, p2 - p1);
+		else if (skip_prefix(oneline.buf, "Merge pull request ",
+				     &p1) &&
+			 (p1 = strstr(p1, " from ")))
+			strbuf_addstr(&label, p1 + strlen(" from "));
+		else
+			strbuf_addbuf(&label, &oneline);
+
+		for (p1 = label.buf; *p1; p1++)
+			if (isspace(*p1))
+				*(char *)p1 = '-';
+
+		strbuf_reset(&buf);
+		strbuf_addf(&buf, "%s %s",
+			    cmd_merge, oid_to_hex(&commit->object.oid));
+
+		/* label the tip of merged branch */
+		oid = &to_merge->item->object.oid;
+		strbuf_addch(&buf, ' ');
+
+		if (!oidset_contains(&interesting, oid))
+			strbuf_addstr(&buf, label_oid(oid, NULL, &state));
+		else {
+			tips_tail = &commit_list_insert(to_merge->item,
+							tips_tail)->next;
+
+			strbuf_addstr(&buf, label_oid(oid, label.buf, &state));
+		}
+		strbuf_addf(&buf, " %s", oneline.buf);
+
+		FLEX_ALLOC_STR(entry, string, buf.buf);
+		oidcpy(&entry->entry.oid, &commit->object.oid);
+		oidmap_put(&commit2todo, entry);
+	}
+
+	/*
+	 * Second phase:
+	 * - label branch points
+	 * - add HEAD to the branch tips
+	 */
+	for (iter = commits; iter; iter = iter->next) {
+		struct commit_list *parent = iter->item->parents;
+		for (; parent; parent = parent->next) {
+			struct object_id *oid = &parent->item->object.oid;
+			if (!oidset_contains(&interesting, oid))
+				continue;
+			if (!oidset_contains(&child_seen, oid))
+				oidset_insert(&child_seen, oid);
+			else
+				label_oid(oid, "branch-point", &state);
+		}
+
+		/* Add HEAD as implict "tip of branch" */
+		if (!iter->next)
+			tips_tail = &commit_list_insert(iter->item,
+							tips_tail)->next;
+	}
+
+	/*
+	 * Third phase: output the todo list. This is a bit tricky, as we
+	 * want to avoid jumping back and forth between revisions. To
+	 * accomplish that goal, we walk backwards from the branch tips,
+	 * gathering commits not yet shown, reversing the list on the fly,
+	 * then outputting that list (labeling revisions as needed).
+	 */
+	fprintf(out, "%s onto\n", cmd_label);
+	for (iter = tips; iter; iter = iter->next) {
+		struct commit_list *list = NULL, *iter2;
+
+		commit = iter->item;
+		if (oidset_contains(&shown, &commit->object.oid))
+			continue;
+		entry = oidmap_get(&state.commit2label, &commit->object.oid);
+
+		if (entry)
+			fprintf(out, "\n# Branch %s\n", entry->string);
+		else
+			fprintf(out, "\n");
+
+		while (oidset_contains(&interesting, &commit->object.oid) &&
+		       !oidset_contains(&shown, &commit->object.oid)) {
+			commit_list_insert(commit, &list);
+			if (!commit->parents) {
+				commit = NULL;
+				break;
+			}
+			commit = commit->parents->item;
+		}
+
+		if (!commit)
+			fprintf(out, "%s onto\n", cmd_reset);
+		else {
+			const char *to = NULL;
+
+			entry = oidmap_get(&state.commit2label,
+					   &commit->object.oid);
+			if (entry)
+				to = entry->string;
+
+			if (!to || !strcmp(to, "onto"))
+				fprintf(out, "%s onto\n", cmd_reset);
+			else {
+				strbuf_reset(&oneline);
+				pretty_print_commit(pp, commit, &oneline);
+				fprintf(out, "%s %s # %s\n",
+					cmd_reset, to, oneline.buf);
+			}
+		}
+
+		for (iter2 = list; iter2; iter2 = iter2->next) {
+			struct object_id *oid = &iter2->item->object.oid;
+			entry = oidmap_get(&commit2todo, oid);
+			/* only show if not already upstream */
+			if (entry)
+				fprintf(out, "%s\n", entry->string);
+			entry = oidmap_get(&state.commit2label, oid);
+			if (entry)
+				fprintf(out, "%s %s\n",
+					cmd_label, entry->string);
+			oidset_insert(&shown, oid);
+		}
+
+		free_commit_list(list);
+	}
+
+	free_commit_list(commits);
+	free_commit_list(tips);
+
+	strbuf_release(&label);
+	strbuf_release(&oneline);
+	strbuf_release(&buf);
+
+	oidmap_free(&commit2todo, 1);
+	oidmap_free(&state.commit2label, 1);
+	hashmap_free(&state.labels, 1);
+	strbuf_release(&state.buf);
+
+	return 0;
+}
+
 int sequencer_make_script(FILE *out, int argc, const char **argv,
 			  unsigned flags)
 {
@@ -2796,11 +3130,16 @@ int sequencer_make_script(FILE *out, int argc, const char **argv,
 	struct commit *commit;
 	int keep_empty = flags & TODO_LIST_KEEP_EMPTY;
 	const char *insn = flags & TODO_LIST_ABBREVIATE_CMDS ? "p" : "pick";
+	int recreate_merges = flags & TODO_LIST_RECREATE_MERGES;
 
 	init_revisions(&revs, NULL);
 	revs.verbose_header = 1;
-	revs.max_parents = 1;
-	revs.cherry_pick = 1;
+	if (recreate_merges)
+		revs.cherry_mark = 1;
+	else {
+		revs.max_parents = 1;
+		revs.cherry_pick = 1;
+	}
 	revs.limited = 1;
 	revs.reverse = 1;
 	revs.right_only = 1;
@@ -2824,6 +3163,9 @@ int sequencer_make_script(FILE *out, int argc, const char **argv,
 	if (prepare_revision_walk(&revs) < 0)
 		return error(_("make_script: error preparing revisions"));
 
+	if (recreate_merges)
+		return make_script_with_merges(&pp, &revs, out, flags);
+
 	while ((commit = get_revision(&revs))) {
 		strbuf_reset(&buf);
 		if (!keep_empty && is_original_commit_empty(commit))
diff --git a/sequencer.h b/sequencer.h
index 81f6d7d393f..11d1ac925ef 100644
--- a/sequencer.h
+++ b/sequencer.h
@@ -48,6 +48,7 @@ int sequencer_remove_state(struct replay_opts *opts);
 #define TODO_LIST_KEEP_EMPTY (1U << 0)
 #define TODO_LIST_SHORTEN_IDS (1U << 1)
 #define TODO_LIST_ABBREVIATE_CMDS (1U << 2)
+#define TODO_LIST_RECREATE_MERGES (1U << 3)
 int sequencer_make_script(FILE *out, int argc, const char **argv,
 			  unsigned flags);
 
-- 
2.16.1.windows.1



^ permalink raw reply	[flat|nested] 276+ messages in thread

* [PATCH v2 06/10] rebase: introduce the --recreate-merges option
  2018-01-29 22:54 ` [PATCH v2 00/10] " Johannes Schindelin
                     ` (4 preceding siblings ...)
  2018-01-29 22:54   ` [PATCH v2 05/10] rebase-helper --make-script: introduce a flag to recreate merges Johannes Schindelin
@ 2018-01-29 22:54   ` Johannes Schindelin
  2018-01-29 22:54   ` [PATCH v2 07/10] sequencer: make refs generated by the `label` command worktree-local Johannes Schindelin
                     ` (6 subsequent siblings)
  12 siblings, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-01-29 22:54 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Jacob Keller, Stefan Beller, Philip Oakley, Eric Sunshine, Phillip Wood

Once upon a time, this here developer thought: wouldn't it be nice if,
say, Git for Windows' patches on top of core Git could be represented as
a thicket of branches, and be rebased on top of core Git in order to
maintain a cherry-pick'able set of patch series?

The original attempt at an answer was: git rebase --preserve-merges.

However, that experiment was never intended as an interactive option,
and it only piggy-backed on git rebase --interactive because that
command's implementation looked already very, very familiar: it was
designed by the same person who designed --preserve-merges: yours truly.

Some time later, some other developer (I am looking at you, Andreas!
;-)) decided that it would be a good idea to allow --preserve-merges to
be combined with --interactive (with caveats!) and the Git maintainer
(well, the interim Git maintainer during Junio's absence, that is)
agreed, and that is when the glamor of the --preserve-merges design
started to fall apart rather quickly and unglamorously.

The reason? In --preserve-merges mode, the parents of a merge commit (or
for that matter, of *any* commit) were not stated explicitly, but were
*implied* by the commit name passed to the `pick` command.

This made it impossible, for example, to reorder commits. Not to mention
to flatten the branch topology or, deity forbid, to split topic branches
into two.

Alas, these shortcomings also prevented that mode (whose original
purpose was to serve Git for Windows' needs, with the additional hope
that it may be useful to others, too) from serving Git for Windows'
needs.

Five years later, when it became really untenable to have one unwieldy,
big hodge-podge patch series of partly related, partly unrelated patches
in Git for Windows that was rebased onto core Git's tags from time to
time (earning the undeserved wrath of the developer of the ill-fated
git-remote-hg series that first obsoleted Git for Windows' competing
approach, only to be abandoned without maintainer later) was really
untenable, the "Git garden shears" were born [*1*/*2*]: a script,
piggy-backing on top of the interactive rebase, that would first
determine the branch topology of the patches to be rebased, create a
pseudo todo list for further editing, transform the result into a real
todo list (making heavy use of the `exec` command to "implement" the
missing todo list commands) and finally recreate the patch series on
top of the new base commit.

That was in 2013. And it took about three weeks to come up with the
design and implement it as an out-of-tree script. Needless to say, the
implementation needed quite a few years to stabilize, all the while the
design itself proved itself sound.

With this patch, the goodness of the Git garden shears comes to `git
rebase -i` itself. Passing the `--recreate-merges` option will generate
a todo list that can be understood readily, and where it is obvious
how to reorder commits. New branches can be introduced by inserting
`label` commands and calling `merge - <label> <oneline>`. And once this
mode has become stable and universally accepted, we can deprecate the
design mistake that was `--preserve-merges`.

Link *1*:
https://github.com/msysgit/msysgit/blob/master/share/msysGit/shears.sh
Link *2*:
https://github.com/git-for-windows/build-extra/blob/master/shears.sh

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 Documentation/git-rebase.txt           |   9 +-
 contrib/completion/git-completion.bash |   2 +-
 git-rebase--interactive.sh             |   1 +
 git-rebase.sh                          |   6 ++
 t/t3430-rebase-recreate-merges.sh      | 146 +++++++++++++++++++++++++++++++++
 5 files changed, 162 insertions(+), 2 deletions(-)
 create mode 100755 t/t3430-rebase-recreate-merges.sh

diff --git a/Documentation/git-rebase.txt b/Documentation/git-rebase.txt
index 8a861c1e0d6..e9da7e26329 100644
--- a/Documentation/git-rebase.txt
+++ b/Documentation/git-rebase.txt
@@ -368,6 +368,12 @@ The commit list format can be changed by setting the configuration option
 rebase.instructionFormat.  A customized instruction format will automatically
 have the long commit hash prepended to the format.
 
+--recreate-merges::
+	Recreate merge commits instead of flattening the history by replaying
+	merges. Merge conflict resolutions or manual amendments to merge
+	commits are not recreated automatically, but have to be recreated
+	manually.
+
 -p::
 --preserve-merges::
 	Recreate merge commits instead of flattening the history by replaying
@@ -770,7 +776,8 @@ BUGS
 The todo list presented by `--preserve-merges --interactive` does not
 represent the topology of the revision graph.  Editing commits and
 rewording their commit messages should work fine, but attempts to
-reorder commits tend to produce counterintuitive results.
+reorder commits tend to produce counterintuitive results. Use
+--recreate-merges for a more faithful representation.
 
 For example, an attempt to rearrange
 ------------
diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash
index 3683c772c55..6893c3adabc 100644
--- a/contrib/completion/git-completion.bash
+++ b/contrib/completion/git-completion.bash
@@ -2008,7 +2008,7 @@ _git_rebase ()
 	--*)
 		__gitcomp "
 			--onto --merge --strategy --interactive
-			--preserve-merges --stat --no-stat
+			--recreate-merges --preserve-merges --stat --no-stat
 			--committer-date-is-author-date --ignore-date
 			--ignore-whitespace --whitespace=
 			--autosquash --no-autosquash
diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh
index d6fd30f6c09..97b7954f7d3 100644
--- a/git-rebase--interactive.sh
+++ b/git-rebase--interactive.sh
@@ -902,6 +902,7 @@ fi
 if test t != "$preserve_merges"
 then
 	git rebase--helper --make-script ${keep_empty:+--keep-empty} \
+		${recreate_merges:+--recreate-merges} \
 		$revisions ${restrict_revision+^$restrict_revision} >"$todo" ||
 	die "$(gettext "Could not generate todo list")"
 else
diff --git a/git-rebase.sh b/git-rebase.sh
index fd72a35c65b..d69bc7d0e0d 100755
--- a/git-rebase.sh
+++ b/git-rebase.sh
@@ -17,6 +17,7 @@ q,quiet!           be quiet. implies --no-stat
 autostash          automatically stash/stash pop before and after
 fork-point         use 'merge-base --fork-point' to refine upstream
 onto=!             rebase onto given branch instead of upstream
+recreate-merges!   try to recreate merges instead of skipping them
 p,preserve-merges! try to recreate merges instead of ignoring them
 s,strategy=!       use the given merge strategy
 no-ff!             cherry-pick all commits, even if unchanged
@@ -86,6 +87,7 @@ type=
 state_dir=
 # One of {'', continue, skip, abort}, as parsed from command line
 action=
+recreate_merges=
 preserve_merges=
 autosquash=
 keep_empty=
@@ -262,6 +264,10 @@ do
 	--keep-empty)
 		keep_empty=yes
 		;;
+	--recreate-merges)
+		recreate_merges=t
+		test -z "$interactive_rebase" && interactive_rebase=implied
+		;;
 	--preserve-merges)
 		preserve_merges=t
 		test -z "$interactive_rebase" && interactive_rebase=implied
diff --git a/t/t3430-rebase-recreate-merges.sh b/t/t3430-rebase-recreate-merges.sh
new file mode 100755
index 00000000000..b5ea4130bb5
--- /dev/null
+++ b/t/t3430-rebase-recreate-merges.sh
@@ -0,0 +1,146 @@
+#!/bin/sh
+#
+# Copyright (c) 2017 Johannes E. Schindelin
+#
+
+test_description='git rebase -i --recreate-merges
+
+This test runs git rebase "interactively", retaining the branch structure by
+recreating merge commits.
+
+Initial setup:
+
+    -- B --                   (first)
+   /       \
+ A - C - D - E - H            (master)
+       \       /
+         F - G                (second)
+'
+. ./test-lib.sh
+. "$TEST_DIRECTORY"/lib-rebase.sh
+
+test_expect_success 'setup' '
+	write_script replace-editor.sh <<-\EOF &&
+	mv "$1" "$(git rev-parse --git-path ORIGINAL-TODO)"
+	cp script-from-scratch "$1"
+	EOF
+
+	test_commit A &&
+	git checkout -b first &&
+	test_commit B &&
+	git checkout master &&
+	test_commit C &&
+	test_commit D &&
+	git merge --no-commit B &&
+	test_tick &&
+	git commit -m E &&
+	git tag -m E E &&
+	git checkout -b second C &&
+	test_commit F &&
+	test_commit G &&
+	git checkout master &&
+	git merge --no-commit G &&
+	test_tick &&
+	git commit -m H &&
+	git tag -m H H
+'
+
+cat >script-from-scratch <<\EOF
+label onto
+
+# onebranch
+pick G
+pick D
+label onebranch
+
+# second
+reset onto
+pick B
+label second
+
+reset onto
+merge H second
+merge - onebranch Merge the topic branch 'onebranch'
+EOF
+
+test_cmp_graph () {
+	cat >expect &&
+	git log --graph --boundary --format=%s "$@" >output &&
+	sed "s/ *$//" <output >output.trimmed &&
+	test_cmp expect output.trimmed
+}
+
+test_expect_success 'create completely different structure' '
+	test_config sequence.editor \""$PWD"/replace-editor.sh\" &&
+	test_tick &&
+	git rebase -i --recreate-merges A &&
+	test_cmp_graph <<-\EOF
+	*   Merge the topic branch '\''onebranch'\''
+	|\
+	| * D
+	| * G
+	* |   H
+	|\ \
+	| |/
+	|/|
+	| * B
+	|/
+	* A
+	EOF
+'
+
+test_expect_success 'generate correct todo list' '
+	cat >expect <<-\EOF &&
+	label onto
+
+	reset onto
+	pick d9df450 B
+	label E
+
+	reset onto
+	pick 5dee784 C
+	label branch-point
+	pick ca2c861 F
+	pick 088b00a G
+	label H
+
+	reset branch-point # C
+	pick 12bd07b D
+	merge 2051b56 E E
+	merge 233d48a H H
+
+	EOF
+
+	grep -v "^#" <.git/ORIGINAL-TODO >output &&
+	test_cmp expect output
+'
+
+test_expect_success 'with a branch tip that was cherry-picked already' '
+	git checkout -b already-upstream master &&
+	base="$(git rev-parse --verify HEAD)" &&
+
+	test_commit A1 &&
+	test_commit A2 &&
+	git reset --hard $base &&
+	test_commit B1 &&
+	test_tick &&
+	git merge -m "Merge branch A" A2 &&
+
+	git checkout -b upstream-with-a2 $base &&
+	test_tick &&
+	git cherry-pick A2 &&
+
+	git checkout already-upstream &&
+	test_tick &&
+	git rebase -i --recreate-merges upstream-with-a2 &&
+	test_cmp_graph upstream-with-a2.. <<-\EOF
+	*   Merge branch A
+	|\
+	| * A1
+	* | B1
+	|/
+	o A2
+	EOF
+'
+
+test_done
-- 
2.16.1.windows.1



^ permalink raw reply	[flat|nested] 276+ messages in thread

* [PATCH v2 07/10] sequencer: make refs generated by the `label` command worktree-local
  2018-01-29 22:54 ` [PATCH v2 00/10] " Johannes Schindelin
                     ` (5 preceding siblings ...)
  2018-01-29 22:54   ` [PATCH v2 06/10] rebase: introduce the --recreate-merges option Johannes Schindelin
@ 2018-01-29 22:54   ` Johannes Schindelin
  2018-01-29 22:55   ` [PATCH v2 08/10] sequencer: handle autosquash and post-rewrite for merge commands Johannes Schindelin
                     ` (5 subsequent siblings)
  12 siblings, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-01-29 22:54 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Jacob Keller, Stefan Beller, Philip Oakley, Eric Sunshine, Phillip Wood

This allows for rebases to be run in parallel in separate worktrees
(think: interrupted in the middle of one rebase, being asked to perform
a different rebase, adding a separate worktree just for that job).

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 refs.c                            |  3 ++-
 t/t3430-rebase-recreate-merges.sh | 14 ++++++++++++++
 2 files changed, 16 insertions(+), 1 deletion(-)

diff --git a/refs.c b/refs.c
index 20ba82b4343..e8b84c189ff 100644
--- a/refs.c
+++ b/refs.c
@@ -600,7 +600,8 @@ int dwim_log(const char *str, int len, struct object_id *oid, char **log)
 static int is_per_worktree_ref(const char *refname)
 {
 	return !strcmp(refname, "HEAD") ||
-		starts_with(refname, "refs/bisect/");
+		starts_with(refname, "refs/bisect/") ||
+		starts_with(refname, "refs/rewritten/");
 }
 
 static int is_pseudoref_syntax(const char *refname)
diff --git a/t/t3430-rebase-recreate-merges.sh b/t/t3430-rebase-recreate-merges.sh
index b5ea4130bb5..5295bb03dc0 100755
--- a/t/t3430-rebase-recreate-merges.sh
+++ b/t/t3430-rebase-recreate-merges.sh
@@ -143,4 +143,18 @@ test_expect_success 'with a branch tip that was cherry-picked already' '
 	EOF
 '
 
+test_expect_success 'refs/rewritten/* is worktree-local' '
+	git worktree add wt &&
+	cat >wt/script-from-scratch <<-\EOF &&
+	label xyz
+	exec GIT_DIR=../.git git rev-parse --verify refs/rewritten/xyz >a || :
+	exec git rev-parse --verify refs/rewritten/xyz >b
+	EOF
+
+	test_config -C wt sequence.editor \""$PWD"/replace-editor.sh\" &&
+	git -C wt rebase -i HEAD &&
+	test_must_be_empty wt/a &&
+	test_cmp_rev HEAD "$(cat wt/b)"
+'
+
 test_done
-- 
2.16.1.windows.1



^ permalink raw reply	[flat|nested] 276+ messages in thread

* [PATCH v2 08/10] sequencer: handle autosquash and post-rewrite for merge commands
  2018-01-29 22:54 ` [PATCH v2 00/10] " Johannes Schindelin
                     ` (6 preceding siblings ...)
  2018-01-29 22:54   ` [PATCH v2 07/10] sequencer: make refs generated by the `label` command worktree-local Johannes Schindelin
@ 2018-01-29 22:55   ` Johannes Schindelin
  2018-01-29 22:55   ` [PATCH v2 09/10] pull: accept --rebase=recreate to recreate the branch topology Johannes Schindelin
                     ` (4 subsequent siblings)
  12 siblings, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-01-29 22:55 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Jacob Keller, Stefan Beller, Philip Oakley, Eric Sunshine, Phillip Wood

In the previous patches, we implemented the basic functionality of the
`git rebase -i --recreate-merges` command, in particular the `merge`
command to create merge commits in the sequencer.

The interactive rebase is a lot more these days, though, than a simple
cherry-pick in a loop. For example, it calls the post-rewrite hook (if
any) after rebasing with a mapping of the old->new commits. And the
interactive rebase also supports the autosquash mode, where commits
whose oneline is of the form `fixup! <oneline>` or `squash! <oneline>`
are rearranged to amend commits whose oneline they match.

This patch implements the post-rewrite and autosquash handling for the
`merge` command we just introduced. The other commands that were added
recently (`label` and `reset`) do not create new commits, therefore
post-rewrite & autosquash do not need to handle them.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 sequencer.c                       | 10 +++++++---
 t/t3430-rebase-recreate-merges.sh | 25 +++++++++++++++++++++++++
 2 files changed, 32 insertions(+), 3 deletions(-)

diff --git a/sequencer.c b/sequencer.c
index d5af315a440..4cc73775394 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -2414,10 +2414,13 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts)
 			res = do_label(item->arg, item->arg_len);
 		else if (item->command == TODO_RESET)
 			res = do_reset(item->arg, item->arg_len);
-		else if (item->command == TODO_MERGE)
+		else if (item->command == TODO_MERGE) {
 			res = do_merge(item->commit,
 				       item->arg, item->arg_len, opts);
-		else if (!is_noop(item->command))
+			if (item->commit)
+				record_in_rewritten(&item->commit->object.oid,
+						    peek_command(todo_list, 1));
+		} else if (!is_noop(item->command))
 			return error(_("unknown command %d"), item->command);
 
 		todo_list->current++;
@@ -3560,7 +3563,8 @@ int rearrange_squash(void)
 		struct subject2item_entry *entry;
 
 		next[i] = tail[i] = -1;
-		if (item->command >= TODO_EXEC) {
+		if (item->command >= TODO_EXEC &&
+		    (item->command != TODO_MERGE || !item->commit)) {
 			subjects[i] = NULL;
 			continue;
 		}
diff --git a/t/t3430-rebase-recreate-merges.sh b/t/t3430-rebase-recreate-merges.sh
index 5295bb03dc0..2eeda0c512b 100755
--- a/t/t3430-rebase-recreate-merges.sh
+++ b/t/t3430-rebase-recreate-merges.sh
@@ -157,4 +157,29 @@ test_expect_success 'refs/rewritten/* is worktree-local' '
 	test_cmp_rev HEAD "$(cat wt/b)"
 '
 
+test_expect_success 'post-rewrite hook and fixups work for merges' '
+	git checkout -b post-rewrite &&
+	test_commit same1 &&
+	git reset --hard HEAD^ &&
+	test_commit same2 &&
+	git merge -m "to fix up" same1 &&
+	echo same old same old >same2.t &&
+	test_tick &&
+	git commit --fixup HEAD same2.t &&
+	fixup="$(git rev-parse HEAD)" &&
+
+	mkdir -p .git/hooks &&
+	test_when_finished "rm .git/hooks/post-rewrite" &&
+	echo "cat >actual" | write_script .git/hooks/post-rewrite &&
+
+	test_tick &&
+	git rebase -i --autosquash --recreate-merges HEAD^^^ &&
+	printf "%s %s\n%s %s\n%s %s\n%s %s\n" >expect $(git rev-parse \
+		$fixup^^2 HEAD^2 \
+		$fixup^^ HEAD^ \
+		$fixup^ HEAD \
+		$fixup HEAD) &&
+	test_cmp expect actual
+'
+
 test_done
-- 
2.16.1.windows.1



^ permalink raw reply	[flat|nested] 276+ messages in thread

* [PATCH v2 09/10] pull: accept --rebase=recreate to recreate the branch topology
  2018-01-29 22:54 ` [PATCH v2 00/10] " Johannes Schindelin
                     ` (7 preceding siblings ...)
  2018-01-29 22:55   ` [PATCH v2 08/10] sequencer: handle autosquash and post-rewrite for merge commands Johannes Schindelin
@ 2018-01-29 22:55   ` Johannes Schindelin
  2018-01-29 22:55   ` [PATCH v2 10/10] rebase -i: introduce --recreate-merges=[no-]rebase-cousins Johannes Schindelin
                     ` (3 subsequent siblings)
  12 siblings, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-01-29 22:55 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Jacob Keller, Stefan Beller, Philip Oakley, Eric Sunshine, Phillip Wood

Similar to the `preserve` mode simply passing the `--preserve-merges`
option to the `rebase` command, the `recreate` mode simply passes the
`--recreate-merges` option.

This will allow users to conveniently rebase non-trivial commit
topologies when pulling new commits, without flattening them.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 Documentation/config.txt               |  8 ++++++++
 Documentation/git-pull.txt             |  5 ++++-
 builtin/pull.c                         | 14 ++++++++++----
 builtin/remote.c                       |  2 ++
 contrib/completion/git-completion.bash |  2 +-
 5 files changed, 25 insertions(+), 6 deletions(-)

diff --git a/Documentation/config.txt b/Documentation/config.txt
index 0e25b2c92b3..da41ab246dc 100644
--- a/Documentation/config.txt
+++ b/Documentation/config.txt
@@ -1058,6 +1058,10 @@ branch.<name>.rebase::
 	"git pull" is run. See "pull.rebase" for doing this in a non
 	branch-specific manner.
 +
+When recreate, also pass `--recreate-merges` along to 'git rebase'
+so that locally committed merge commits will not be flattened
+by running 'git pull'.
++
 When preserve, also pass `--preserve-merges` along to 'git rebase'
 so that locally committed merge commits will not be flattened
 by running 'git pull'.
@@ -2607,6 +2611,10 @@ pull.rebase::
 	pull" is run. See "branch.<name>.rebase" for setting this on a
 	per-branch basis.
 +
+When recreate, also pass `--recreate-merges` along to 'git rebase'
+so that locally committed merge commits will not be flattened
+by running 'git pull'.
++
 When preserve, also pass `--preserve-merges` along to 'git rebase'
 so that locally committed merge commits will not be flattened
 by running 'git pull'.
diff --git a/Documentation/git-pull.txt b/Documentation/git-pull.txt
index ce05b7a5b13..b4f9f057ea9 100644
--- a/Documentation/git-pull.txt
+++ b/Documentation/git-pull.txt
@@ -101,13 +101,16 @@ Options related to merging
 include::merge-options.txt[]
 
 -r::
---rebase[=false|true|preserve|interactive]::
+--rebase[=false|true|recreate|preserve|interactive]::
 	When true, rebase the current branch on top of the upstream
 	branch after fetching. If there is a remote-tracking branch
 	corresponding to the upstream branch and the upstream branch
 	was rebased since last fetched, the rebase uses that information
 	to avoid rebasing non-local changes.
 +
+When set to recreate, rebase with the `--recreate-merges` option passed
+to `git rebase` so that locally created merge commits will not be flattened.
++
 When set to preserve, rebase with the `--preserve-merges` option passed
 to `git rebase` so that locally created merge commits will not be flattened.
 +
diff --git a/builtin/pull.c b/builtin/pull.c
index 511dbbe0f6e..e33c84e0345 100644
--- a/builtin/pull.c
+++ b/builtin/pull.c
@@ -27,14 +27,16 @@ enum rebase_type {
 	REBASE_FALSE = 0,
 	REBASE_TRUE,
 	REBASE_PRESERVE,
+	REBASE_RECREATE,
 	REBASE_INTERACTIVE
 };
 
 /**
  * Parses the value of --rebase. If value is a false value, returns
  * REBASE_FALSE. If value is a true value, returns REBASE_TRUE. If value is
- * "preserve", returns REBASE_PRESERVE. If value is a invalid value, dies with
- * a fatal error if fatal is true, otherwise returns REBASE_INVALID.
+ * "recreate", returns REBASE_RECREATE. If value is "preserve", returns
+ * REBASE_PRESERVE. If value is a invalid value, dies with a fatal error if
+ * fatal is true, otherwise returns REBASE_INVALID.
  */
 static enum rebase_type parse_config_rebase(const char *key, const char *value,
 		int fatal)
@@ -47,6 +49,8 @@ static enum rebase_type parse_config_rebase(const char *key, const char *value,
 		return REBASE_TRUE;
 	else if (!strcmp(value, "preserve"))
 		return REBASE_PRESERVE;
+	else if (!strcmp(value, "recreate"))
+		return REBASE_RECREATE;
 	else if (!strcmp(value, "interactive"))
 		return REBASE_INTERACTIVE;
 
@@ -130,7 +134,7 @@ static struct option pull_options[] = {
 	/* Options passed to git-merge or git-rebase */
 	OPT_GROUP(N_("Options related to merging")),
 	{ OPTION_CALLBACK, 'r', "rebase", &opt_rebase,
-	  "false|true|preserve|interactive",
+	  "false|true|recreate|preserve|interactive",
 	  N_("incorporate changes by rebasing rather than merging"),
 	  PARSE_OPT_OPTARG, parse_opt_rebase },
 	OPT_PASSTHRU('n', NULL, &opt_diffstat, NULL,
@@ -798,7 +802,9 @@ static int run_rebase(const struct object_id *curr_head,
 	argv_push_verbosity(&args);
 
 	/* Options passed to git-rebase */
-	if (opt_rebase == REBASE_PRESERVE)
+	if (opt_rebase == REBASE_RECREATE)
+		argv_array_push(&args, "--recreate-merges");
+	else if (opt_rebase == REBASE_PRESERVE)
 		argv_array_push(&args, "--preserve-merges");
 	else if (opt_rebase == REBASE_INTERACTIVE)
 		argv_array_push(&args, "--interactive");
diff --git a/builtin/remote.c b/builtin/remote.c
index d95bf904c3b..b7d0f7ce596 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -306,6 +306,8 @@ static int config_read_branches(const char *key, const char *value, void *cb)
 				info->rebase = v;
 			else if (!strcmp(value, "preserve"))
 				info->rebase = NORMAL_REBASE;
+			else if (!strcmp(value, "recreate"))
+				info->rebase = NORMAL_REBASE;
 			else if (!strcmp(value, "interactive"))
 				info->rebase = INTERACTIVE_REBASE;
 		}
diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash
index 6893c3adabc..6f98c96fee9 100644
--- a/contrib/completion/git-completion.bash
+++ b/contrib/completion/git-completion.bash
@@ -2182,7 +2182,7 @@ _git_config ()
 		return
 		;;
 	branch.*.rebase)
-		__gitcomp "false true preserve interactive"
+		__gitcomp "false true recreate preserve interactive"
 		return
 		;;
 	remote.pushdefault)
-- 
2.16.1.windows.1



^ permalink raw reply	[flat|nested] 276+ messages in thread

* [PATCH v2 10/10] rebase -i: introduce --recreate-merges=[no-]rebase-cousins
  2018-01-29 22:54 ` [PATCH v2 00/10] " Johannes Schindelin
                     ` (8 preceding siblings ...)
  2018-01-29 22:55   ` [PATCH v2 09/10] pull: accept --rebase=recreate to recreate the branch topology Johannes Schindelin
@ 2018-01-29 22:55   ` Johannes Schindelin
  2018-01-30 18:47   ` [PATCH v2 00/10] rebase -i: offer to recreate merge commits Stefan Beller
                     ` (2 subsequent siblings)
  12 siblings, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-01-29 22:55 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Jacob Keller, Stefan Beller, Philip Oakley, Eric Sunshine, Phillip Wood

This one is a bit tricky to explain, so let's try with a diagram:

        C
      /   \
A - B - E - F
  \   /
    D

To illustrate what this new mode is all about, let's consider what
happens upon `git rebase -i --recreate-merges B`, in particular to
the commit `D`. So far, the new branch structure would be:

       --- C' --
      /         \
A - B ------ E' - F'
      \    /
        D'

This is not really preserving the branch topology from before! The
reason is that the commit `D` does not have `B` as ancestor, and
therefore it gets rebased onto `B`.

This is unintuitive behavior. Even worse, when recreating branch
structure, most use cases would appear to want cousins *not* to be
rebased onto the new base commit. For example, Git for Windows (the
heaviest user of the Git garden shears, which served as the blueprint
for --recreate-merges) frequently merges branches from `next` early, and
these branches certainly do *not* want to be rebased. In the example
above, the desired outcome would look like this:

       --- C' --
      /         \
A - B ------ E' - F'
  \        /
   -- D' --

Let's introduce the term "cousins" for such commits ("D" in the
example), and let's not rebase them by default, introducing the new
"rebase-cousins" mode for use cases where they should be rebased.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 Documentation/git-rebase.txt      |  7 ++++++-
 builtin/rebase--helper.c          |  9 ++++++++-
 git-rebase--interactive.sh        |  1 +
 git-rebase.sh                     | 12 +++++++++++-
 sequencer.c                       |  4 ++++
 sequencer.h                       |  6 ++++++
 t/t3430-rebase-recreate-merges.sh | 23 +++++++++++++++++++++++
 7 files changed, 59 insertions(+), 3 deletions(-)

diff --git a/Documentation/git-rebase.txt b/Documentation/git-rebase.txt
index e9da7e26329..0e6d020d924 100644
--- a/Documentation/git-rebase.txt
+++ b/Documentation/git-rebase.txt
@@ -368,11 +368,16 @@ The commit list format can be changed by setting the configuration option
 rebase.instructionFormat.  A customized instruction format will automatically
 have the long commit hash prepended to the format.
 
---recreate-merges::
+--recreate-merges[=(rebase-cousins|no-rebase-cousins)]::
 	Recreate merge commits instead of flattening the history by replaying
 	merges. Merge conflict resolutions or manual amendments to merge
 	commits are not recreated automatically, but have to be recreated
 	manually.
++
+By default, or when `no-rebase-cousins` was specified, commits which do not
+have `<upstream>` as direct ancestor keep their original branch point.
+If the `rebase-cousins` mode is turned on, such commits are rebased onto
+`<upstream>` (or `<onto>`, if specified).
 
 -p::
 --preserve-merges::
diff --git a/builtin/rebase--helper.c b/builtin/rebase--helper.c
index a34ab5c0655..cea99cb3235 100644
--- a/builtin/rebase--helper.c
+++ b/builtin/rebase--helper.c
@@ -13,7 +13,7 @@ int cmd_rebase__helper(int argc, const char **argv, const char *prefix)
 {
 	struct replay_opts opts = REPLAY_OPTS_INIT;
 	unsigned flags = 0, keep_empty = 0, recreate_merges = 0;
-	int abbreviate_commands = 0;
+	int abbreviate_commands = 0, rebase_cousins = -1;
 	enum {
 		CONTINUE = 1, ABORT, MAKE_SCRIPT, SHORTEN_OIDS, EXPAND_OIDS,
 		CHECK_TODO_LIST, SKIP_UNNECESSARY_PICKS, REARRANGE_SQUASH,
@@ -23,6 +23,8 @@ int cmd_rebase__helper(int argc, const char **argv, const char *prefix)
 		OPT_BOOL(0, "ff", &opts.allow_ff, N_("allow fast-forward")),
 		OPT_BOOL(0, "keep-empty", &keep_empty, N_("keep empty commits")),
 		OPT_BOOL(0, "recreate-merges", &recreate_merges, N_("recreate merge commits")),
+		OPT_BOOL(0, "rebase-cousins", &rebase_cousins,
+			 N_("keep original branch points of cousins")),
 		OPT_CMDMODE(0, "continue", &command, N_("continue rebase"),
 				CONTINUE),
 		OPT_CMDMODE(0, "abort", &command, N_("abort rebase"),
@@ -57,8 +59,13 @@ int cmd_rebase__helper(int argc, const char **argv, const char *prefix)
 	flags |= keep_empty ? TODO_LIST_KEEP_EMPTY : 0;
 	flags |= abbreviate_commands ? TODO_LIST_ABBREVIATE_CMDS : 0;
 	flags |= recreate_merges ? TODO_LIST_RECREATE_MERGES : 0;
+	flags |= rebase_cousins > 0 ? TODO_LIST_REBASE_COUSINS : 0;
 	flags |= command == SHORTEN_OIDS ? TODO_LIST_SHORTEN_IDS : 0;
 
+	if (rebase_cousins >= 0 && !recreate_merges)
+		warning(_("--[no-]rebase-cousins has no effect without "
+			  "--recreate-merges"));
+
 	if (command == CONTINUE && argc == 1)
 		return !!sequencer_continue(&opts);
 	if (command == ABORT && argc == 1)
diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh
index 97b7954f7d3..5e21e4cf269 100644
--- a/git-rebase--interactive.sh
+++ b/git-rebase--interactive.sh
@@ -903,6 +903,7 @@ if test t != "$preserve_merges"
 then
 	git rebase--helper --make-script ${keep_empty:+--keep-empty} \
 		${recreate_merges:+--recreate-merges} \
+		${rebase_cousins:+--rebase-cousins} \
 		$revisions ${restrict_revision+^$restrict_revision} >"$todo" ||
 	die "$(gettext "Could not generate todo list")"
 else
diff --git a/git-rebase.sh b/git-rebase.sh
index d69bc7d0e0d..58d778a2da0 100755
--- a/git-rebase.sh
+++ b/git-rebase.sh
@@ -17,7 +17,7 @@ q,quiet!           be quiet. implies --no-stat
 autostash          automatically stash/stash pop before and after
 fork-point         use 'merge-base --fork-point' to refine upstream
 onto=!             rebase onto given branch instead of upstream
-recreate-merges!   try to recreate merges instead of skipping them
+recreate-merges?   try to recreate merges instead of skipping them
 p,preserve-merges! try to recreate merges instead of ignoring them
 s,strategy=!       use the given merge strategy
 no-ff!             cherry-pick all commits, even if unchanged
@@ -88,6 +88,7 @@ state_dir=
 # One of {'', continue, skip, abort}, as parsed from command line
 action=
 recreate_merges=
+rebase_cousins=
 preserve_merges=
 autosquash=
 keep_empty=
@@ -268,6 +269,15 @@ do
 		recreate_merges=t
 		test -z "$interactive_rebase" && interactive_rebase=implied
 		;;
+	--recreate-merges=*)
+		recreate_merges=t
+		case "${1#*=}" in
+		rebase-cousins) rebase_cousins=t;;
+		no-rebase-cousins) rebase_cousins=;;
+		*) die "Unknown mode: $1";;
+		esac
+		test -z "$interactive_rebase" && interactive_rebase=implied
+		;;
 	--preserve-merges)
 		preserve_merges=t
 		test -z "$interactive_rebase" && interactive_rebase=implied
diff --git a/sequencer.c b/sequencer.c
index 4cc73775394..cd2f2ae5d53 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -2906,6 +2906,7 @@ static int make_script_with_merges(struct pretty_print_context *pp,
 				   unsigned flags)
 {
 	int keep_empty = flags & TODO_LIST_KEEP_EMPTY;
+	int rebase_cousins = flags & TODO_LIST_REBASE_COUSINS;
 	struct strbuf buf = STRBUF_INIT, oneline = STRBUF_INIT;
 	struct strbuf label = STRBUF_INIT;
 	struct commit_list *commits = NULL, **tail = &commits, *iter;
@@ -3081,6 +3082,9 @@ static int make_script_with_merges(struct pretty_print_context *pp,
 					   &commit->object.oid);
 			if (entry)
 				to = entry->string;
+			else if (!rebase_cousins)
+				to = label_oid(&commit->object.oid, NULL,
+					       &state);
 
 			if (!to || !strcmp(to, "onto"))
 				fprintf(out, "%s onto\n", cmd_reset);
diff --git a/sequencer.h b/sequencer.h
index 11d1ac925ef..deebc6e3258 100644
--- a/sequencer.h
+++ b/sequencer.h
@@ -49,6 +49,12 @@ int sequencer_remove_state(struct replay_opts *opts);
 #define TODO_LIST_SHORTEN_IDS (1U << 1)
 #define TODO_LIST_ABBREVIATE_CMDS (1U << 2)
 #define TODO_LIST_RECREATE_MERGES (1U << 3)
+/*
+ * When recreating merges, commits that do have the base commit as ancestor
+ * ("cousins") are *not* rebased onto the new base by default. If those
+ * commits should be rebased onto the new base, this flag needs to be passed.
+ */
+#define TODO_LIST_REBASE_COUSINS (1U << 4)
 int sequencer_make_script(FILE *out, int argc, const char **argv,
 			  unsigned flags);
 
diff --git a/t/t3430-rebase-recreate-merges.sh b/t/t3430-rebase-recreate-merges.sh
index 2eeda0c512b..ab51b584ff9 100755
--- a/t/t3430-rebase-recreate-merges.sh
+++ b/t/t3430-rebase-recreate-merges.sh
@@ -143,6 +143,29 @@ test_expect_success 'with a branch tip that was cherry-picked already' '
 	EOF
 '
 
+test_expect_success 'do not rebase cousins unless asked for' '
+	write_script copy-editor.sh <<-\EOF &&
+	cp "$1" "$(git rev-parse --git-path ORIGINAL-TODO)"
+	EOF
+
+	test_config sequence.editor \""$PWD"/copy-editor.sh\" &&
+	git checkout -b cousins master &&
+	before="$(git rev-parse --verify HEAD)" &&
+	test_tick &&
+	git rebase -i --recreate-merges HEAD^ &&
+	test_cmp_rev HEAD $before &&
+	test_tick &&
+	git rebase -i --recreate-merges=rebase-cousins HEAD^ &&
+	test_cmp_graph HEAD^.. <<-\EOF
+	*   Merge the topic branch '\''onebranch'\''
+	|\
+	| * D
+	| * G
+	|/
+	o H
+	EOF
+'
+
 test_expect_success 'refs/rewritten/* is worktree-local' '
 	git worktree add wt &&
 	cat >wt/script-from-scratch <<-\EOF &&
-- 
2.16.1.windows.1

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 1/8] sequencer: introduce new commands to reset the revision
  2018-01-29 20:50     ` Johannes Schindelin
@ 2018-01-30  7:12       ` Eric Sunshine
  0 siblings, 0 replies; 276+ messages in thread
From: Eric Sunshine @ 2018-01-30  7:12 UTC (permalink / raw)
  To: Johannes Schindelin; +Cc: Git List, Junio C Hamano, Jacob Keller

On Mon, Jan 29, 2018 at 3:50 PM, Johannes Schindelin
<Johannes.Schindelin@gmx.de> wrote:
> On Fri, 19 Jan 2018, Eric Sunshine wrote:
>> On Thu, Jan 18, 2018 at 10:35 AM, Johannes Schindelin
>> <johannes.schindelin@gmx.de> wrote:
>> > +static int do_reset(const char *name, int len)
>> > +{
>> > +       for (i = 0; i < len; i++)
>> > +               if (isspace(name[i]))
>> > +                       len = i;
>>
>> What is the purpose of this loop? I could imagine that it's trying to
>> strip all whitespace from the end of 'name', however, to do that it
>> would iterate backward, not forward. (Or perhaps it's trying to
>> truncate at the first space, but then it would need to invert the
>> condition or use 'break'.) Am I missing something obvious?
>
> Yes, you are missing something obvious. The idea of the `reset` command is
> that it not only has a label, but also the oneline of the original commit:
>
>         reset branch-point sequencer: prepare for cleanup
>
> In this instance, `branch-point` is the label. And for convenience of the
> person editing, it also has the oneline.

No, that's not what I was missing. What I was missing was that
assigning 'i' to 'len' also causes the loop to terminate. It's
embarrassing how long I had to stare at this loop to see that, and I
suspect that's what fooled a couple other reviewers, as well, since
idiomatic loops don't normally muck with the termination condition in
quite that fashion (and is why I suggested that a 'break' might be
missing).

Had the loop been a bit more idiomatic:

    for (i = 0; i < len; i++)
        if (isspace(name[i]))
            break;
    len = i;

then the question would never have arisen. Anyhow, it's a minor point
in the greater scheme of the patch series.

> In the Git garden shears, I separated the two arguments via `#`:
>
>         reset branch-point # sequencer: prepare for cleanup
>
> I guess that is actually more readable, so I will introduce that into this
> patch series, too.

Given my termination-condition blindness, the extra "#" would not have
helped me understand the loop any better.

Having now played with the feature a tiny bit, I don't have a strong
opinion about the "#" other than to note that it seems inconsistent
with other commands which don't use "#" as a separator.

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH v2 02/10] sequencer: introduce new commands to reset the revision
  2018-01-29 22:54   ` [PATCH v2 02/10] sequencer: introduce new commands to reset the revision Johannes Schindelin
@ 2018-01-30  8:06     ` Eric Sunshine
  2018-02-10 20:58       ` Johannes Schindelin
  2018-01-30 20:17     ` Stefan Beller
  1 sibling, 1 reply; 276+ messages in thread
From: Eric Sunshine @ 2018-01-30  8:06 UTC (permalink / raw)
  To: Johannes Schindelin; +Cc: Git List, Junio C Hamano, Jacob Keller, Stefan Beller, Philip Oakley, Phillip Wood

On Mon, Jan 29, 2018 at 5:54 PM, Johannes Schindelin
<johannes.schindelin@gmx.de> wrote:
> [...]
> This commit implements the commands to label, and to reset to, given
> revisions. The syntax is:
>
>         label <name>
>         reset <name>
> [...]
>
> Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
> ---
> diff --git a/sequencer.c b/sequencer.c
> @@ -1253,7 +1266,8 @@ static int parse_insn_line(struct todo_item *item, const char *bol, char *eol)
>                 if (skip_prefix(bol, todo_command_info[i].str, &bol)) {
>                         item->command = i;
>                         break;
> -               } else if (bol[1] == ' ' && *bol == todo_command_info[i].c) {
> +               } else if ((bol + 1 == eol || bol[1] == ' ') &&
> +                          *bol == todo_command_info[i].c) {

This adds support for commands which have no arguments, however, now
that the "bud" command has been retired, this can go away too, right?

>                         bol++;
>                         item->command = i;
>                         break;
> @@ -1919,6 +1934,144 @@ static int do_exec(const char *command_line)
> +static int safe_append(const char *filename, const char *fmt, ...)
> +{
> +       va_list ap;
> +       struct lock_file lock = LOCK_INIT;
> +       int fd = hold_lock_file_for_update(&lock, filename, 0);
> +       struct strbuf buf = STRBUF_INIT;
> +
> +       if (fd < 0)
> +               return error_errno(_("could not lock '%s'"), filename);

Minor: unable_to_lock_message() can provide a more detailed
explanation of the failure.

> +
> +       if (strbuf_read_file(&buf, filename, 0) < 0 && errno != ENOENT)
> +               return error_errno(_("could not read '%s'"), filename);
> +       strbuf_complete(&buf, '\n');
> +       va_start(ap, fmt);
> +       strbuf_vaddf(&buf, fmt, ap);
> +       va_end(ap);

Would it make sense to also

    strbuf_complete(&buf, '\n')

here, as well, to be a bit more robust against lazy callers?

> +
> +       if (write_in_full(fd, buf.buf, buf.len) < 0) {
> +               rollback_lock_file(&lock);
> +               return error_errno(_("could not write to '%s'"), filename);

Reading lockfile.h & tempfile.c, I see that rollback_lock_file()
clobbers write_in_full()'s errno before error_errno() is called.

> +       }
> +       if (commit_lock_file(&lock) < 0) {
> +               rollback_lock_file(&lock);
> +               return error(_("failed to finalize '%s'"), filename);
> +       }
> +
> +       return 0;
> +}
> +
> +static int do_reset(const char *name, int len)
> +{
> +       [...]
> +       strbuf_addf(&ref_name, "refs/rewritten/%.*s", len, name);
> +       if (get_oid(ref_name.buf, &oid) &&
> +           get_oid(ref_name.buf + strlen("refs/rewritten/"), &oid)) {
> +               error(_("could not read '%s'"), ref_name.buf);

Checking my understanding: The two get_oid() calls allow the argument
to 'reset' to be a label created with the 'label' command or any other
way to name an object, right? If so, then I wonder if the error
invocation should instead be:

    error(_("could not read '%.*s'"), len, name);

> +               rollback_lock_file(&lock);
> +               strbuf_release(&ref_name);
> +               return -1;
> +       }

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH v2 00/10] rebase -i: offer to recreate merge commits
  2018-01-29 22:54 ` [PATCH v2 00/10] " Johannes Schindelin
                     ` (9 preceding siblings ...)
  2018-01-29 22:55   ` [PATCH v2 10/10] rebase -i: introduce --recreate-merges=[no-]rebase-cousins Johannes Schindelin
@ 2018-01-30 18:47   ` Stefan Beller
  2018-01-31 13:08     ` Johannes Schindelin
  2018-01-30 21:36   ` Junio C Hamano
  2018-02-11  0:09   ` [PATCH v3 00/12] " Johannes Schindelin
  12 siblings, 1 reply; 276+ messages in thread
From: Stefan Beller @ 2018-01-30 18:47 UTC (permalink / raw)
  To: Johannes Schindelin; +Cc: git, Junio C Hamano, Jacob Keller, Philip Oakley, Eric Sunshine, Phillip Wood

On Mon, Jan 29, 2018 at 2:54 PM, Johannes Schindelin
<johannes.schindelin@gmx.de> wrote:
> Once upon a time, I dreamt of an interactive rebase that would not
> flatten branch structure, but instead recreate the commit topology
> faithfully.
>
> My original attempt was --preserve-merges, but that design was so
> limited that I did not even enable it in interactive mode.
>
> Subsequently, it *was* enabled in interactive mode, with the predictable
> consequences: as the --preserve-merges design does not allow for
> specifying the parents of merge commits explicitly, all the new commits'
> parents are defined *implicitly* by the previous commit history, and
> hence it is *not possible to even reorder commits*.
>
> This design flaw cannot be fixed. Not without a complete re-design, at
> least. This patch series offers such a re-design.
>
> Think of --recreate-merges as "--preserve-merges done right". It
> introduces new verbs for the todo list, `label`, `reset` and `merge`.
> For a commit topology like this:
>
>             A - B - C
>               \   /
>                 D
>
> the generated todo list would look like this:
>
>             # branch D
>             pick 0123 A
>             label branch-point
>             pick 1234 D
>             label D
>
>             reset branch-point
>             pick 2345 B
>             merge 3456 D C
>
> There are more patches in the pipeline, based on this patch series, but
> left for later in the interest of reviewable patch series: one mini
> series to use the sequencer even for `git rebase -i --root`, and another
> one to add support for octopus merges to --recreate-merges.
>
> Changes since v1:
>
> - reintroduced "sequencer: make refs generated by the `label` command
>   worktree-local" (which was squashed into "sequencer: handle autosquash
>   and post-rewrite for merge commands" by accident)
>
> - got rid of the universally-hated `bud` command

Sorry if you got the impression for that. Maybe I was imprecise.
I had no strong opinion one way or another, I merely pointed out the
collision in abbreviation letters with the potential new 'break', IIRC.

'bud' was a special case for resetting to a specific revision
(and labeling it?)

Maybe we can have default labels, such that there is no need to reset
to the first revision manually, but can just use these defaults in the merge.
(I haven't thought about this in the big picture, just food for thought)


>
> - as per Stefan's suggestion, the help blurb at the end of the todo list
>   now lists the syntax
>
> - the no-rebase-cousins mode was made the default; This not only reflects
>   the experience won from those years of using the Git garden shears, but
>   was also deemed the better default in the discussion on the PR at
>   https://github.com/git/git/pull/447
>
> - I tried to clarify the role of the `onto` label in the commit message of
>   `rebase-helper --make-script: introduce a flag to recreate merges`
>
> - fixed punctuation at the end of error(...) messages, and incorrect
>   upper-case at the start
>
> - changed the generated todo lists to separate the label and the oneline in
>   the `reset` command with a `#`, for readability
>
> - dropped redundant paragraph in the commit message that talked about
>   support for octopus merges
>
> - avoided empty error message when HEAD could not be read during do_label()
>
> - merge commits are fast-forwarded only unless --force-rebase was passed
>
> - do_merge() now errors out a lot earlier when HEAD could not be parsed
>
> - the one-letter variables to hold either abbreviated or full todo list
>   instructions in make_script_recreating_merges() were renamed to clearer
>   names
>
> - The description of rebase's --recreate-merge option has been reworded;
>   Hopefully it is a lot more clear now.
>
>
> Johannes Schindelin (9):
>   sequencer: introduce new commands to reset the revision
>   sequencer: introduce the `merge` command
>   sequencer: fast-forward merge commits, if possible
>   rebase-helper --make-script: introduce a flag to recreate merges
>   rebase: introduce the --recreate-merges option
>   sequencer: make refs generated by the `label` command worktree-local
>   sequencer: handle autosquash and post-rewrite for merge commands
>   pull: accept --rebase=recreate to recreate the branch topology
>   rebase -i: introduce --recreate-merges=[no-]rebase-cousins
>
> Stefan Beller (1):
>   git-rebase--interactive: clarify arguments

No need to honor me with authorship, as I just wrote
that patch in a quick hurry to express the idea.
But this is fine, too.

The interdiff looks good to me, I'll review the patches now.

Thanks,
Stefan

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH v2 02/10] sequencer: introduce new commands to reset the revision
  2018-01-29 22:54   ` [PATCH v2 02/10] sequencer: introduce new commands to reset the revision Johannes Schindelin
  2018-01-30  8:06     ` Eric Sunshine
@ 2018-01-30 20:17     ` Stefan Beller
  2018-01-31 13:21       ` Johannes Schindelin
  1 sibling, 1 reply; 276+ messages in thread
From: Stefan Beller @ 2018-01-30 20:17 UTC (permalink / raw)
  To: Johannes Schindelin; +Cc: git, Junio C Hamano, Jacob Keller, Philip Oakley, Eric Sunshine, Phillip Wood

On Mon, Jan 29, 2018 at 2:54 PM, Johannes Schindelin
<johannes.schindelin@gmx.de> wrote:
> In the upcoming commits, we will teach the sequencer to recreate merges.
> This will be done in a very different way from the unfortunate design of
> `git rebase --preserve-merges` (which does not allow for reordering
> commits, or changing the branch topology).
>
> The main idea is to introduce new todo list commands, to support
> labeling the current revision with a given name, resetting the current
> revision to a previous state, merging labeled revisions.
>
> This idea was developed in Git for Windows' Git garden shears (that are
> used to maintain the "thicket of branches" on top of upstream Git), and
> this patch is part of the effort to make it available to a wider
> audience, as well as to make the entire process more robust (by
> implementing it in a safe and portable language rather than a Unix shell
> script).
>
> This commit implements the commands to label, and to reset to, given
> revisions. The syntax is:
>
>         label <name>
>         reset <name>
>
> Internally, the `label <name>` command creates the ref
> `refs/rewritten/<name>`. This makes it possible to work with the labeled
> revisions interactively, or in a scripted fashion (e.g. via the todo
> list command `exec`).
>
> Later in this patch series, we will mark the `refs/rewritten/` refs as
> worktree-local, to allow for interactive rebases to be run in parallel in
> worktrees linked to the same repository.
>
> Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
> ---
>  git-rebase--interactive.sh |   2 +
>  sequencer.c                | 180 ++++++++++++++++++++++++++++++++++++++++++++-
>  2 files changed, 179 insertions(+), 3 deletions(-)
>
> diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh
> index fcedece1860..7e5281e74aa 100644
> --- a/git-rebase--interactive.sh
> +++ b/git-rebase--interactive.sh
> @@ -162,6 +162,8 @@ s, squash <commit> = use commit, but meld into previous commit
>  f, fixup <commit> = like \"squash\", but discard this commit's log message
>  x, exec <commit> = run command (the rest of the line) using shell
>  d, drop <commit> = remove commit
> +l, label <label> = label current HEAD with a name
> +t, reset <label> = reset HEAD to a label
>
>  These lines can be re-ordered; they are executed from top to bottom.
>  " | git stripspace --comment-lines >>"$todo"
> diff --git a/sequencer.c b/sequencer.c
> index 4d3f60594cb..92ca8d2adee 100644
> --- a/sequencer.c
> +++ b/sequencer.c
> @@ -21,6 +21,8 @@
>  #include "log-tree.h"
>  #include "wt-status.h"
>  #include "hashmap.h"
> +#include "unpack-trees.h"
> +#include "worktree.h"
>
>  #define GIT_REFLOG_ACTION "GIT_REFLOG_ACTION"
>
> @@ -116,6 +118,13 @@ static GIT_PATH_FUNC(rebase_path_stopped_sha, "rebase-merge/stopped-sha")
>  static GIT_PATH_FUNC(rebase_path_rewritten_list, "rebase-merge/rewritten-list")
>  static GIT_PATH_FUNC(rebase_path_rewritten_pending,
>         "rebase-merge/rewritten-pending")
> +
> +/*
> + * The path of the file listing refs that need to be deleted after the rebase
> + * finishes. This is used by the `merge` command.
> + */

So this file contains (label -> commit), which is appended in do_label,
it uses refs to store the commits in refs/rewritten.
We do not have to worry about the contents of that file getting too long,
or label re-use, because the directory containing all these helper files will
be deleted upon successful rebase in `sequencer_remove_state()`.



> +static GIT_PATH_FUNC(rebase_path_refs_to_delete, "rebase-merge/refs-to-delete")
> +
>  /*
>   * The following files are written by git-rebase just after parsing the
>   * command-line (and are only consumed, not modified, by the sequencer).
> @@ -767,6 +776,8 @@ enum todo_command {
>         TODO_SQUASH,
>         /* commands that do something else than handling a single commit */
>         TODO_EXEC,
> +       TODO_LABEL,
> +       TODO_RESET,
>         /* commands that do nothing but are counted for reporting progress */
>         TODO_NOOP,
>         TODO_DROP,
> @@ -785,6 +796,8 @@ static struct {
>         { 'f', "fixup" },
>         { 's', "squash" },
>         { 'x', "exec" },
> +       { 'l', "label" },
> +       { 't', "reset" },
>         { 0,   "noop" },
>         { 'd', "drop" },
>         { 0,   NULL }
> @@ -1253,7 +1266,8 @@ static int parse_insn_line(struct todo_item *item, const char *bol, char *eol)
>                 if (skip_prefix(bol, todo_command_info[i].str, &bol)) {
>                         item->command = i;
>                         break;
> -               } else if (bol[1] == ' ' && *bol == todo_command_info[i].c) {
> +               } else if ((bol + 1 == eol || bol[1] == ' ') &&
> +                          *bol == todo_command_info[i].c) {
>                         bol++;
>                         item->command = i;
>                         break;
> @@ -1279,7 +1293,8 @@ static int parse_insn_line(struct todo_item *item, const char *bol, char *eol)
>                 return error(_("missing arguments for %s"),
>                              command_to_string(item->command));
>
> -       if (item->command == TODO_EXEC) {
> +       if (item->command == TODO_EXEC || item->command == TODO_LABEL ||
> +           item->command == TODO_RESET) {
>                 item->commit = NULL;
>                 item->arg = bol;
>                 item->arg_len = (int)(eol - bol);
> @@ -1919,6 +1934,144 @@ static int do_exec(const char *command_line)
>         return status;
>  }
>
> +static int safe_append(const char *filename, const char *fmt, ...)
> +{
> +       va_list ap;
> +       struct lock_file lock = LOCK_INIT;
> +       int fd = hold_lock_file_for_update(&lock, filename, 0);
> +       struct strbuf buf = STRBUF_INIT;
> +
> +       if (fd < 0)
> +               return error_errno(_("could not lock '%s'"), filename);
> +
> +       if (strbuf_read_file(&buf, filename, 0) < 0 && errno != ENOENT)
> +               return error_errno(_("could not read '%s'"), filename);
> +       strbuf_complete(&buf, '\n');
> +       va_start(ap, fmt);
> +       strbuf_vaddf(&buf, fmt, ap);
> +       va_end(ap);
> +
> +       if (write_in_full(fd, buf.buf, buf.len) < 0) {
> +               rollback_lock_file(&lock);
> +               return error_errno(_("could not write to '%s'"), filename);
> +       }
> +       if (commit_lock_file(&lock) < 0) {
> +               rollback_lock_file(&lock);
> +               return error(_("failed to finalize '%s'"), filename);
> +       }
> +
> +       return 0;
> +}
> +
> +static int do_label(const char *name, int len)
> +{
> +       struct ref_store *refs = get_main_ref_store();
> +       struct ref_transaction *transaction;
> +       struct strbuf ref_name = STRBUF_INIT, err = STRBUF_INIT;
> +       struct strbuf msg = STRBUF_INIT;
> +       int ret = 0;
> +       struct object_id head_oid;
> +
> +       strbuf_addf(&ref_name, "refs/rewritten/%.*s", len, name);
> +       strbuf_addf(&msg, "rebase -i (label) '%.*s'", len, name);
> +
> +       transaction = ref_store_transaction_begin(refs, &err);
> +       if (!transaction) {
> +               error("%s", err.buf);
> +               ret = -1;
> +       } else if (get_oid("HEAD", &head_oid)) {
> +               error(_("could not read HEAD"));
> +               ret = -1;
> +       } else if (ref_transaction_update(transaction, ref_name.buf, &head_oid,
> +                                         NULL, 0, msg.buf, &err) < 0 ||
> +                  ref_transaction_commit(transaction, &err)) {
> +               error("%s", err.buf);
> +               ret = -1;
> +       }
> +       ref_transaction_free(transaction);
> +       strbuf_release(&err);
> +       strbuf_release(&msg);
> +
> +       if (!ret)
> +               ret = safe_append(rebase_path_refs_to_delete(),
> +                                 "%s\n", ref_name.buf);
> +       strbuf_release(&ref_name);
> +
> +       return ret;
> +}
> +
> +static int do_reset(const char *name, int len)
> +{
> +       struct strbuf ref_name = STRBUF_INIT;
> +       struct object_id oid;
> +       struct lock_file lock = LOCK_INIT;
> +       struct tree_desc desc;
> +       struct tree *tree;
> +       struct unpack_trees_options opts;
> +       int ret = 0, i;
> +
> +       if (hold_locked_index(&lock, LOCK_REPORT_ON_ERROR) < 0)
> +               return -1;
> +
> +       /* Determine the length of the label */
> +       for (i = 0; i < len; i++)
> +               if (isspace(name[i]))
> +                       len = i;
> +
> +       strbuf_addf(&ref_name, "refs/rewritten/%.*s", len, name);
> +       if (get_oid(ref_name.buf, &oid) &&
> +           get_oid(ref_name.buf + strlen("refs/rewritten/"), &oid)) {
> +               error(_("could not read '%s'"), ref_name.buf);
> +               rollback_lock_file(&lock);
> +               strbuf_release(&ref_name);
> +               return -1;
> +       }
> +
> +       memset(&opts, 0, sizeof(opts));
> +       opts.head_idx = 1;
> +       opts.src_index = &the_index;
> +       opts.dst_index = &the_index;
> +       opts.fn = oneway_merge;
> +       opts.merge = 1;
> +       opts.update = 1;
> +       opts.reset = 1;
> +
> +       read_cache_unmerged();

In read-tree.c merge.c and pull.c we guard this conditionally
and use die_resolve_conflict to bail out. In am.c we do not.

I think we'd want to guard it here, too?

Constructing an instruction sheet that produces a merge
conflict just before the reset is a bit artificial, but still:

    label onto
    pick abc
    exec false # run "git merge out-of-rebase-merge"
        # manually to produce a conflict
    reset onto # we want to stop here telling the user to fix it.

> +       if (!fill_tree_descriptor(&desc, &oid)) {
> +               error(_("failed to find tree of %s"), oid_to_hex(&oid));
> +               rollback_lock_file(&lock);
> +               free((void *)desc.buffer);
> +               strbuf_release(&ref_name);
> +               return -1;
> +       }
> +
> +       if (unpack_trees(1, &desc, &opts)) {
> +               rollback_lock_file(&lock);
> +               free((void *)desc.buffer);
> +               strbuf_release(&ref_name);
> +               return -1;
> +       }
> +
> +       tree = parse_tree_indirect(&oid);
> +       prime_cache_tree(&the_index, tree);
> +
> +       if (write_locked_index(&the_index, &lock, COMMIT_LOCK) < 0)
> +               ret = error(_("could not write index"));
> +       free((void *)desc.buffer);

For most newer structs we have a {release, clear, free}_X,
but for tree_desc's this seems to be the convention to avoid memleaks.

Thanks,
Stefan

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH v2 00/10] rebase -i: offer to recreate merge commits
  2018-01-29 22:54 ` [PATCH v2 00/10] " Johannes Schindelin
                     ` (10 preceding siblings ...)
  2018-01-30 18:47   ` [PATCH v2 00/10] rebase -i: offer to recreate merge commits Stefan Beller
@ 2018-01-30 21:36   ` Junio C Hamano
  2018-01-31 13:29     ` Johannes Schindelin
  2018-02-11  0:09   ` [PATCH v3 00/12] " Johannes Schindelin
  12 siblings, 1 reply; 276+ messages in thread
From: Junio C Hamano @ 2018-01-30 21:36 UTC (permalink / raw)
  To: Johannes Schindelin; +Cc: git, Jacob Keller, Stefan Beller, Philip Oakley, Eric Sunshine, Phillip Wood

Johannes Schindelin <johannes.schindelin@gmx.de> writes:

> Changes since v1:
>
> - reintroduced "sequencer: make refs generated by the `label` command
>   worktree-local" (which was squashed into "sequencer: handle autosquash
>   and post-rewrite for merge commands" by accident)

Good.

> - got rid of the universally-hated `bud` command

Universally is a bit too strong a word, unless you want to hint that
you are specifically ignoring my input ;-).

> - the no-rebase-cousins mode was made the default

Although I lack first-hand experience with this implementation, this
design decision matches my instinct.

May comment on individual patches separately, later.

Thanks.

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH v2 00/10] rebase -i: offer to recreate merge commits
  2018-01-30 18:47   ` [PATCH v2 00/10] rebase -i: offer to recreate merge commits Stefan Beller
@ 2018-01-31 13:08     ` Johannes Schindelin
  0 siblings, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-01-31 13:08 UTC (permalink / raw)
  To: Stefan Beller; +Cc: git, Junio C Hamano, Jacob Keller, Philip Oakley, Eric Sunshine, Phillip Wood

Hi Stefan,

On Tue, 30 Jan 2018, Stefan Beller wrote:

> > - got rid of the universally-hated `bud` command
> 
> Sorry if you got the impression for that. Maybe I was imprecise.

You were not the most vocal voice. Anyway, `bud` is gone now.

> > Stefan Beller (1):
> >   git-rebase--interactive: clarify arguments
> 
> No need to honor me with authorship, as I just wrote
> that patch in a quick hurry to express the idea.

You wrote it.

> The interdiff looks good to me, I'll review the patches now.

Thanks,
Dscho

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH v2 02/10] sequencer: introduce new commands to reset the revision
  2018-01-30 20:17     ` Stefan Beller
@ 2018-01-31 13:21       ` Johannes Schindelin
  2018-01-31 18:02         ` [PATCH v2 02/10] sequencer: introduce new commands to reset therevision Phillip Wood
  0 siblings, 1 reply; 276+ messages in thread
From: Johannes Schindelin @ 2018-01-31 13:21 UTC (permalink / raw)
  To: Stefan Beller; +Cc: git, Junio C Hamano, Jacob Keller, Philip Oakley, Eric Sunshine, Phillip Wood

Hi Stefan,

On Tue, 30 Jan 2018, Stefan Beller wrote:

> On Mon, Jan 29, 2018 at 2:54 PM, Johannes Schindelin
> <johannes.schindelin@gmx.de> wrote:
> > @@ -116,6 +118,13 @@ static GIT_PATH_FUNC(rebase_path_stopped_sha, "rebase-merge/stopped-sha")
> >  static GIT_PATH_FUNC(rebase_path_rewritten_list, "rebase-merge/rewritten-list")
> >  static GIT_PATH_FUNC(rebase_path_rewritten_pending,
> >         "rebase-merge/rewritten-pending")
> > +
> > +/*
> > + * The path of the file listing refs that need to be deleted after the rebase
> > + * finishes. This is used by the `merge` command.
> > + */

Whoops. The comment "This is used by the `merge` command`" is completely
wrong. Will fix.

> So this file contains (label -> commit),

Only `label`. No `commit`.

> which is appended in do_label, it uses refs to store the commits in
> refs/rewritten.  We do not have to worry about the contents of that file
> getting too long, or label re-use, because the directory containing all
> these helper files will be deleted upon successful rebase in
> `sequencer_remove_state()`.

Yes.

> > +static int do_reset(const char *name, int len)
> > +{
> > +       struct strbuf ref_name = STRBUF_INIT;
> > +       struct object_id oid;
> > +       struct lock_file lock = LOCK_INIT;
> > +       struct tree_desc desc;
> > +       struct tree *tree;
> > +       struct unpack_trees_options opts;
> > +       int ret = 0, i;
> > +
> > +       if (hold_locked_index(&lock, LOCK_REPORT_ON_ERROR) < 0)
> > +               return -1;
> > +
> > +       /* Determine the length of the label */
> > +       for (i = 0; i < len; i++)
> > +               if (isspace(name[i]))
> > +                       len = i;
> > +
> > +       strbuf_addf(&ref_name, "refs/rewritten/%.*s", len, name);
> > +       if (get_oid(ref_name.buf, &oid) &&
> > +           get_oid(ref_name.buf + strlen("refs/rewritten/"), &oid)) {
> > +               error(_("could not read '%s'"), ref_name.buf);
> > +               rollback_lock_file(&lock);
> > +               strbuf_release(&ref_name);
> > +               return -1;
> > +       }
> > +
> > +       memset(&opts, 0, sizeof(opts));
> > +       opts.head_idx = 1;
> > +       opts.src_index = &the_index;
> > +       opts.dst_index = &the_index;
> > +       opts.fn = oneway_merge;
> > +       opts.merge = 1;
> > +       opts.update = 1;
> > +       opts.reset = 1;
> > +
> > +       read_cache_unmerged();
> 
> In read-tree.c merge.c and pull.c we guard this conditionally
> and use die_resolve_conflict to bail out. In am.c we do not.
> 
> I think we'd want to guard it here, too?

Yes.

> Constructing an instruction sheet that produces a merge
> conflict just before the reset is a bit artificial, but still:
> 
>     label onto
>     pick abc
>     exec false # run "git merge out-of-rebase-merge"
>         # manually to produce a conflict

This would fail already, as `exec` tests for a clean index after the
operation ran.

>     reset onto # we want to stop here telling the user to fix it.

But you are absolutely right that we still need to fix it.

> > +       if (!fill_tree_descriptor(&desc, &oid)) {
> > +               error(_("failed to find tree of %s"), oid_to_hex(&oid));
> > +               rollback_lock_file(&lock);
> > +               free((void *)desc.buffer);
> > +               strbuf_release(&ref_name);
> > +               return -1;
> > +       }
> > +
> > +       if (unpack_trees(1, &desc, &opts)) {
> > +               rollback_lock_file(&lock);
> > +               free((void *)desc.buffer);
> > +               strbuf_release(&ref_name);
> > +               return -1;
> > +       }
> > +
> > +       tree = parse_tree_indirect(&oid);
> > +       prime_cache_tree(&the_index, tree);
> > +
> > +       if (write_locked_index(&the_index, &lock, COMMIT_LOCK) < 0)
> > +               ret = error(_("could not write index"));
> > +       free((void *)desc.buffer);
> 
> For most newer structs we have a {release, clear, free}_X,
> but for tree_desc's this seems to be the convention to avoid memleaks.

Yep, this code is just copy-edited from elsewhere. It seemed to be
different enough from the (very generic) use case in builtin/reset.c that
I did not think refactoring this into a convenience function in
unpack-trees.[ch] would make sense.

Ciao,
Dscho

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH v2 00/10] rebase -i: offer to recreate merge commits
  2018-01-30 21:36   ` Junio C Hamano
@ 2018-01-31 13:29     ` Johannes Schindelin
  2018-02-01  6:37       ` Jacob Keller
  0 siblings, 1 reply; 276+ messages in thread
From: Johannes Schindelin @ 2018-01-31 13:29 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: git, Jacob Keller, Stefan Beller, Philip Oakley, Eric Sunshine, Phillip Wood

Hi Junio,

On Tue, 30 Jan 2018, Junio C Hamano wrote:

> Johannes Schindelin <johannes.schindelin@gmx.de> writes:
> 
> > Changes since v1:
> >
> > - reintroduced "sequencer: make refs generated by the `label` command
> >   worktree-local" (which was squashed into "sequencer: handle autosquash
> >   and post-rewrite for merge commands" by accident)
> 
> Good.
> 
> > - got rid of the universally-hated `bud` command
> 
> Universally is a bit too strong a word, unless you want to hint that
> you are specifically ignoring my input ;-).

In the interest of comic effect, I exaggerated a little.

> > - the no-rebase-cousins mode was made the default
> 
> Although I lack first-hand experience with this implementation, this
> design decision matches my instinct.

Excellent.

> May comment on individual patches separately, later.

I think I may want to introduce a bigger change, still. I forgot who
exactly came up with the suggestion to use `merge -C <original-commit>
<to-merge>` (I think it was Jake), and I reacted too forcefully in
rejecting it.

This design had been my original design in the Git garden shears, and I
did not like it because it felt clunky and it also broke the style of
<command> <commit>.

But the longer I think about this, the more I come to the conclusion that
I was wrong, and that the -C way is the way that leaves the door open to
the pretty elegant `-c <commit>` (imitating `git commit`'s option to
borrow the commit message from elsewhere but still allowing to edit it).

And it also leaves open the door to just write `merge <to-merge>` and have
the sequencer come up with a default merge message that the user can then
edit.

I'll probably refrain from implementing support for -m because I do not
want to implement the dequoting.

Ciao,
Dscho

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 2/8] sequencer: introduce the `merge` command
  2018-01-29 21:41       ` Johannes Schindelin
@ 2018-01-31 13:48         ` Johannes Schindelin
  2018-01-31 17:58           ` Phillip Wood
  2018-02-01  6:40           ` Jacob Keller
  0 siblings, 2 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-01-31 13:48 UTC (permalink / raw)
  To: Jacob Keller; +Cc: Phillip Wood, Git mailing list, Junio C Hamano, Eric Sunshine

Hi Jake & Phillip,

On Mon, 29 Jan 2018, Johannes Schindelin wrote:

> On Sat, 20 Jan 2018, Jacob Keller wrote:
> 
> > On Fri, Jan 19, 2018 at 6:45 AM, Phillip Wood <phillip.wood@talktalk.net> wrote:
> > > On 18/01/18 15:35, Johannes Schindelin wrote:
> > >>
> > >> This patch adds the `merge` command, with the following syntax:
> > >>
> > >>       merge <commit> <rev> <oneline>
> > >
> > > I'm concerned that this will be confusing for users. All of the other
> > > rebase commands replay the changes in the commit hash immediately
> > > following the command name. This command instead uses the first
> > > commit to specify the message which is different to both 'git merge'
> > > and the existing rebase commands. I wonder if it would be clearer to
> > > have 'merge -C <commit> <rev> ...' instead so it's clear which
> > > argument specifies the message and which the remote head to merge.
> > > It would also allow for 'merge -c <commit> <rev> ...' in the future
> > > for rewording an existing merge message and also avoid the slightly
> > > odd 'merge - <rev> ...'. Where it's creating new merges I'm not sure
> > > it's a good idea to encourage people to only have oneline commit
> > > messages by making it harder to edit them, perhaps it could take
> > > another argument to mean open the editor or not, though as Jake said
> > > I guess it's not that common.
> > 
> > I actually like the idea of re-using commit message options like -C,
> > -c,  and -m, so we could do:
> > 
> > merge -C <commit> ... to take message from commit
> 
> That is exactly how the Git garden shears do it.
> 
> I found it not very readable. That is why I wanted to get away from it in
> --recreate-merges.

I made up my mind. Even if it is not very readable, it is still better
than the `merge A B` where the order of A and B magically determines their
respective roles.

> > merge -c <commit> ...  to take the message from commit and open editor to edit
> > merge -m "<message>" ... to take the message from the quoted test
> > merge ... to merge and open commit editor with default message

I will probably implement -c, but not -m, and will handle the absence of
the -C and -c options to construct a default merge message which can then
be edited.

The -m option just opens such a can of worms with dequoting, that's why I
do not want to do that.

BTW I am still trying to figure out how to present the oneline of the
commit to merge (which is sometimes really helpful because the label might
be less than meaningful) while *still* allowing for octopus merges.

So far, what I have is this:

	merge <original> <to-merge> <oneline>

and for octopus:

	merge <original> "<to-merge> <to-merge2>..." <oneline>...

I think with the -C syntax, it would become something like

	merge -C <original> <to-merge> # <oneline>

and

	merge -C <original> <to-merge> <to-merge2>...
	# Merging: <oneline>
	# Merging: <oneline2>
	# ...

The only qualm I have about this is that `#` really *is* a valid ref name.
(Seriously, it is...). So that would mean that I'd have to disallow `#`
as a label specificially.

Thoughts?

Ciao,
Dscho

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 2/8] sequencer: introduce the `merge` command
  2018-01-31 13:48         ` Johannes Schindelin
@ 2018-01-31 17:58           ` Phillip Wood
  2018-02-01  6:40           ` Jacob Keller
  1 sibling, 0 replies; 276+ messages in thread
From: Phillip Wood @ 2018-01-31 17:58 UTC (permalink / raw)
  To: Johannes Schindelin, Jacob Keller; +Cc: Phillip Wood, Git mailing list, Junio C Hamano, Eric Sunshine

On 31/01/18 13:48, Johannes Schindelin wrote:
> Hi Jake & Phillip,
> 
> On Mon, 29 Jan 2018, Johannes Schindelin wrote:
> 
>> On Sat, 20 Jan 2018, Jacob Keller wrote:
>>
>>> On Fri, Jan 19, 2018 at 6:45 AM, Phillip Wood <phillip.wood@talktalk.net> wrote:
>>>> On 18/01/18 15:35, Johannes Schindelin wrote:
>>>>>
>>>>> This patch adds the `merge` command, with the following syntax:
>>>>>
>>>>>       merge <commit> <rev> <oneline>
>>>>
>>>> I'm concerned that this will be confusing for users. All of the other
>>>> rebase commands replay the changes in the commit hash immediately
>>>> following the command name. This command instead uses the first
>>>> commit to specify the message which is different to both 'git merge'
>>>> and the existing rebase commands. I wonder if it would be clearer to
>>>> have 'merge -C <commit> <rev> ...' instead so it's clear which
>>>> argument specifies the message and which the remote head to merge.
>>>> It would also allow for 'merge -c <commit> <rev> ...' in the future
>>>> for rewording an existing merge message and also avoid the slightly
>>>> odd 'merge - <rev> ...'. Where it's creating new merges I'm not sure
>>>> it's a good idea to encourage people to only have oneline commit
>>>> messages by making it harder to edit them, perhaps it could take
>>>> another argument to mean open the editor or not, though as Jake said
>>>> I guess it's not that common.
>>>
>>> I actually like the idea of re-using commit message options like -C,
>>> -c,  and -m, so we could do:
>>>
>>> merge -C <commit> ... to take message from commit
>>
>> That is exactly how the Git garden shears do it.
>>
>> I found it not very readable. That is why I wanted to get away from it in
>> --recreate-merges.
> 
> I made up my mind. Even if it is not very readable, it is still better
> than the `merge A B` where the order of A and B magically determines their
> respective roles.
> 
>>> merge -c <commit> ...  to take the message from commit and open editor to edit
>>> merge -m "<message>" ... to take the message from the quoted test
>>> merge ... to merge and open commit editor with default message
> 
> I will probably implement -c, but not -m, and will handle the absence of
> the -C and -c options to construct a default merge message which can then
> be edited.

That sounds like a good plan (-c can always be added later), I'm really
pleased you changed your mind on this, having the -C may be a bit ugly
but I think it is valuable to have some way of distinguishing the
message commit from the merge heads.

> The -m option just opens such a can of worms with dequoting, that's why I
> do not want to do that.
> 
> BTW I am still trying to figure out how to present the oneline of the
> commit to merge (which is sometimes really helpful because the label might
> be less than meaningful) while *still* allowing for octopus merges.
> 
> So far, what I have is this:
> 
> 	merge <original> <to-merge> <oneline>
> 
> and for octopus:
> 
> 	merge <original> "<to-merge> <to-merge2>..." <oneline>...
> 
> I think with the -C syntax, it would become something like
> 
> 	merge -C <original> <to-merge> # <oneline>
> 
> and
> 
> 	merge -C <original> <to-merge> <to-merge2>...
> 	# Merging: <oneline>
> 	# Merging: <oneline2>
> 	# ...
> 
> The only qualm I have about this is that `#` really *is* a valid ref name.
> (Seriously, it is...). So that would mean that I'd have to disallow `#`
> as a label specificially.
> 
> Thoughts?

As ':' is not a valid ref if you want a separator you could have

	merge -C <original> <to-merge> : <oneline>

personally I'm not sure what value having a separator adds in this case.
I think in the octopus case have a separate comment line for the subject
of each merge head is a good idea - maybe the two head merge could just
have the subject of the remote head in a comment below. I wonder if
having the subject of the commit that is going to be used for the
message may be a useful prompt in some cases but that's just making
things more complicated.

Best Wishes

Phillip

> Ciao,
> Dscho
> 


^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH v2 02/10] sequencer: introduce new commands to reset therevision
  2018-01-31 13:21       ` Johannes Schindelin
@ 2018-01-31 18:02         ` Phillip Wood
  2018-02-10 21:49           ` Johannes Schindelin
  0 siblings, 1 reply; 276+ messages in thread
From: Phillip Wood @ 2018-01-31 18:02 UTC (permalink / raw)
  To: Johannes Schindelin, Stefan Beller; +Cc: git, Junio C Hamano, Jacob Keller, Philip Oakley, Eric Sunshine, Phillip Wood

On 31/01/18 13:21, Johannes Schindelin wrote:
> 
> Hi Stefan,
> 
> On Tue, 30 Jan 2018, Stefan Beller wrote:
> 
>> On Mon, Jan 29, 2018 at 2:54 PM, Johannes Schindelin
>> <johannes.schindelin@gmx.de> wrote:
>>> @@ -116,6 +118,13 @@ static GIT_PATH_FUNC(rebase_path_stopped_sha, "rebase-merge/stopped-sha")
>>>  static GIT_PATH_FUNC(rebase_path_rewritten_list, "rebase-merge/rewritten-list")
>>>  static GIT_PATH_FUNC(rebase_path_rewritten_pending,
>>>         "rebase-merge/rewritten-pending")
>>> +
>>> +/*
>>> + * The path of the file listing refs that need to be deleted after the rebase
>>> + * finishes. This is used by the `merge` command.
>>> + */
> 
> Whoops. The comment "This is used by the `merge` command`" is completely
> wrong. Will fix.
> 
>> So this file contains (label -> commit),
> 
> Only `label`. No `commit`.
> 
>> which is appended in do_label, it uses refs to store the commits in
>> refs/rewritten.  We do not have to worry about the contents of that file
>> getting too long, or label re-use, because the directory containing all
>> these helper files will be deleted upon successful rebase in
>> `sequencer_remove_state()`.
> 
> Yes.
>
It might be a good idea to have 'git rebase --abort' delete the refs as
well as the file though

Best Wishes

Phillip

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH v2 00/10] rebase -i: offer to recreate merge commits
  2018-01-31 13:29     ` Johannes Schindelin
@ 2018-02-01  6:37       ` Jacob Keller
  0 siblings, 0 replies; 276+ messages in thread
From: Jacob Keller @ 2018-02-01  6:37 UTC (permalink / raw)
  To: Johannes Schindelin; +Cc: Junio C Hamano, Git mailing list, Stefan Beller, Philip Oakley, Eric Sunshine, Phillip Wood

On Wed, Jan 31, 2018 at 5:29 AM, Johannes Schindelin
<Johannes.Schindelin@gmx.de> wrote:
> I think I may want to introduce a bigger change, still. I forgot who
> exactly came up with the suggestion to use `merge -C <original-commit>
> <to-merge>` (I think it was Jake), and I reacted too forcefully in
> rejecting it.
>

I believe someone else suggested it, but I replied that I liked it.

> This design had been my original design in the Git garden shears, and I
> did not like it because it felt clunky and it also broke the style of
> <command> <commit>.
>

I agree it's a bit weird it breaks the style of "<command> <commit>",
but on some level merge does this anyways as it's the first one to
take more than one argument.

> But the longer I think about this, the more I come to the conclusion that
> I was wrong, and that the -C way is the way that leaves the door open to
> the pretty elegant `-c <commit>` (imitating `git commit`'s option to
> borrow the commit message from elsewhere but still allowing to edit it).
>

The other reason I liked this, is that it matches merge syntax on the
command line, so users don't need to learn a special new syntax for
the todo file.

> And it also leaves open the door to just write `merge <to-merge>` and have
> the sequencer come up with a default merge message that the user can then
> edit.

I like that we could completely forgo the -C and -c in order to allow
the normal default merge commit message that is auto generated as
well.

>
> I'll probably refrain from implementing support for -m because I do not
> want to implement the dequoting.
>

Yea, I don't think that is necessary either.

Thanks,
Jake

> Ciao,
> Dscho

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 2/8] sequencer: introduce the `merge` command
  2018-01-31 13:48         ` Johannes Schindelin
  2018-01-31 17:58           ` Phillip Wood
@ 2018-02-01  6:40           ` Jacob Keller
  1 sibling, 0 replies; 276+ messages in thread
From: Jacob Keller @ 2018-02-01  6:40 UTC (permalink / raw)
  To: Johannes Schindelin; +Cc: Phillip Wood, Git mailing list, Junio C Hamano, Eric Sunshine

On Wed, Jan 31, 2018 at 5:48 AM, Johannes Schindelin
<Johannes.Schindelin@gmx.de> wrote:
> Hi Jake & Phillip,
>
> On Mon, 29 Jan 2018, Johannes Schindelin wrote:
>
>> On Sat, 20 Jan 2018, Jacob Keller wrote:
>>
>> > On Fri, Jan 19, 2018 at 6:45 AM, Phillip Wood <phillip.wood@talktalk.net> wrote:
>> > > On 18/01/18 15:35, Johannes Schindelin wrote:
>> > >>
>> > >> This patch adds the `merge` command, with the following syntax:
>> > >>
>> > >>       merge <commit> <rev> <oneline>
>> > >
>> > > I'm concerned that this will be confusing for users. All of the other
>> > > rebase commands replay the changes in the commit hash immediately
>> > > following the command name. This command instead uses the first
>> > > commit to specify the message which is different to both 'git merge'
>> > > and the existing rebase commands. I wonder if it would be clearer to
>> > > have 'merge -C <commit> <rev> ...' instead so it's clear which
>> > > argument specifies the message and which the remote head to merge.
>> > > It would also allow for 'merge -c <commit> <rev> ...' in the future
>> > > for rewording an existing merge message and also avoid the slightly
>> > > odd 'merge - <rev> ...'. Where it's creating new merges I'm not sure
>> > > it's a good idea to encourage people to only have oneline commit
>> > > messages by making it harder to edit them, perhaps it could take
>> > > another argument to mean open the editor or not, though as Jake said
>> > > I guess it's not that common.
>> >
>> > I actually like the idea of re-using commit message options like -C,
>> > -c,  and -m, so we could do:
>> >
>> > merge -C <commit> ... to take message from commit
>>
>> That is exactly how the Git garden shears do it.
>>
>> I found it not very readable. That is why I wanted to get away from it in
>> --recreate-merges.
>
> I made up my mind. Even if it is not very readable, it is still better
> than the `merge A B` where the order of A and B magically determines their
> respective roles.
>
>> > merge -c <commit> ...  to take the message from commit and open editor to edit
>> > merge -m "<message>" ... to take the message from the quoted test
>> > merge ... to merge and open commit editor with default message
>
> I will probably implement -c, but not -m, and will handle the absence of
> the -C and -c options to construct a default merge message which can then
> be edited.
>
> The -m option just opens such a can of worms with dequoting, that's why I
> do not want to do that.
>

I agree, I don't see a need for "-m".

> BTW I am still trying to figure out how to present the oneline of the
> commit to merge (which is sometimes really helpful because the label might
> be less than meaningful) while *still* allowing for octopus merges.
>
> So far, what I have is this:
>
>         merge <original> <to-merge> <oneline>
>
> and for octopus:
>
>         merge <original> "<to-merge> <to-merge2>..." <oneline>...
>
> I think with the -C syntax, it would become something like
>
>         merge -C <original> <to-merge> # <oneline>
>

I like this, especially given you added the "#" for one of the other
new commands as well, (reset I think?)

> and
>
>         merge -C <original> <to-merge> <to-merge2>...
>         # Merging: <oneline>
>         # Merging: <oneline2>
>         # ...
>

I really like this, since you can show each oneline for all the
to-merges for an octopus.

> The only qualm I have about this is that `#` really *is* a valid ref name.
> (Seriously, it is...). So that would mean that I'd have to disallow `#`
> as a label specifically.
>
> Thoughts?
>

I think it's fine to disallow # as a label.

Thanks,
Jake

> Ciao,
> Dscho

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 5/8] rebase: introduce the --recreate-merges option
  2018-01-18 15:35 ` [PATCH 5/8] rebase: introduce the --recreate-merges option Johannes Schindelin
  2018-01-19 10:55   ` Eric Sunshine
  2018-01-23 20:22   ` Junio C Hamano
@ 2018-02-07  6:16   ` Sergey Organov
  2018-02-07  7:26     ` Jacob Keller
                       ` (2 more replies)
  2018-02-09  6:50   ` Sergey Organov
  3 siblings, 3 replies; 276+ messages in thread
From: Sergey Organov @ 2018-02-07  6:16 UTC (permalink / raw)
  To: Johannes Schindelin; +Cc: git, Junio C Hamano, Jacob Keller, Johannes Sixt

Johannes Schindelin <johannes.schindelin@gmx.de> writes:

[...]

> +--recreate-merges::
> +	Recreate merge commits instead of flattening the history by replaying
> +	merges. Merge conflict resolutions or manual amendments to merge
> +	commits are not preserved.

I wonder why you guys still hold on replaying "merge-the-operation"
instead of replaying "merge-the-result"? The latter, the merge commit
itself, no matter how exactly it was created in the first place, is the
most valuable thing git keeps about the merge, and you silently drop it
entirely! OTOH, git keeps almost no information about
"merge-the-operation", so it's virtually impossible to reliably replay
the operation automatically, and yet you try to.

IMHO that was severe mistake in the original --preserve-merges, and you
bring with you to this new --recreate-merges... It's sad. Even more sad
as solution is already known for years:

    bc00341838a8faddcd101da9e746902994eef38a
    Author: Johannes Sixt <j6t@kdbg.org>
    Date:   Sun Jun 16 15:50:42 2013 +0200
    
        rebase -p --first-parent: redo merge by cherry-picking first-parent change

and it works like a charm.

-- Sergey


^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 5/8] rebase: introduce the --recreate-merges option
  2018-02-07  6:16   ` Sergey Organov
@ 2018-02-07  7:26     ` Jacob Keller
  2018-02-07  9:47       ` Sergey Organov
  2018-02-07  7:27     ` Johannes Sixt
  2018-02-07 17:36     ` Johannes Schindelin
  2 siblings, 1 reply; 276+ messages in thread
From: Jacob Keller @ 2018-02-07  7:26 UTC (permalink / raw)
  To: Sergey Organov; +Cc: Johannes Schindelin, Git mailing list, Junio C Hamano, Johannes Sixt

On Tue, Feb 6, 2018 at 10:16 PM, Sergey Organov <sorganov@gmail.com> wrote:
> Johannes Schindelin <johannes.schindelin@gmx.de> writes:
>
> [...]
>
>> +--recreate-merges::
>> +     Recreate merge commits instead of flattening the history by replaying
>> +     merges. Merge conflict resolutions or manual amendments to merge
>> +     commits are not preserved.
>
> I wonder why you guys still hold on replaying "merge-the-operation"
> instead of replaying "merge-the-result"? The latter, the merge commit
> itself, no matter how exactly it was created in the first place, is the
> most valuable thing git keeps about the merge, and you silently drop it
> entirely! OTOH, git keeps almost no information about
> "merge-the-operation", so it's virtually impossible to reliably replay
> the operation automatically, and yet you try to.
>

I'm not sure I follow what you mean here?

You mean that you'd want this to actually attempt to re-create the
original merge including conflict resolutions by taking the contents
of the result?

How do you handle if that result has conflicts? What UX do you present
to the user to handle such conflicts? I don't think the normal 3-way
conflicts would even be possible in this case?

Thanks,
Jake

> IMHO that was severe mistake in the original --preserve-merges, and you
> bring with you to this new --recreate-merges... It's sad. Even more sad
> as solution is already known for years:
>
>     bc00341838a8faddcd101da9e746902994eef38a
>     Author: Johannes Sixt <j6t@kdbg.org>
>     Date:   Sun Jun 16 15:50:42 2013 +0200
>
>         rebase -p --first-parent: redo merge by cherry-picking first-parent change
>
> and it works like a charm.
>
> -- Sergey
>

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 5/8] rebase: introduce the --recreate-merges option
  2018-02-07  6:16   ` Sergey Organov
  2018-02-07  7:26     ` Jacob Keller
@ 2018-02-07  7:27     ` Johannes Sixt
  2018-02-07 17:36     ` Johannes Schindelin
  2 siblings, 0 replies; 276+ messages in thread
From: Johannes Sixt @ 2018-02-07  7:27 UTC (permalink / raw)
  To: Sergey Organov, Johannes Schindelin; +Cc: git, Junio C Hamano, Jacob Keller

Am 07.02.2018 um 07:16 schrieb Sergey Organov:
> Johannes Schindelin <johannes.schindelin@gmx.de> writes:
> 
> [...]
> 
>> +--recreate-merges::
>> +	Recreate merge commits instead of flattening the history by replaying
>> +	merges. Merge conflict resolutions or manual amendments to merge
>> +	commits are not preserved.
> 
> I wonder why you guys still hold on replaying "merge-the-operation"
> instead of replaying "merge-the-result"? The latter, the merge commit
> itself, no matter how exactly it was created in the first place, is the
> most valuable thing git keeps about the merge, and you silently drop it
> entirely! OTOH, git keeps almost no information about
> "merge-the-operation", so it's virtually impossible to reliably replay
> the operation automatically, and yet you try to.

Very well put. I share your concerns.

-- Hannes

> IMHO that was severe mistake in the original --preserve-merges, and you
> bring with you to this new --recreate-merges... It's sad. Even more sad
> as solution is already known for years:
> 
>      bc00341838a8faddcd101da9e746902994eef38a
>      Author: Johannes Sixt <j6t@kdbg.org>
>      Date:   Sun Jun 16 15:50:42 2013 +0200
>      
>          rebase -p --first-parent: redo merge by cherry-picking first-parent change
> 
> and it works like a charm.
> 
> -- Sergey

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 5/8] rebase: introduce the --recreate-merges option
  2018-02-07  7:26     ` Jacob Keller
@ 2018-02-07  9:47       ` Sergey Organov
  0 siblings, 0 replies; 276+ messages in thread
From: Sergey Organov @ 2018-02-07  9:47 UTC (permalink / raw)
  To: Jacob Keller; +Cc: Johannes Schindelin, Git mailing list, Junio C Hamano, Johannes Sixt

Jacob Keller <jacob.keller@gmail.com> writes:

> On Tue, Feb 6, 2018 at 10:16 PM, Sergey Organov <sorganov@gmail.com> wrote:
>> Johannes Schindelin <johannes.schindelin@gmx.de> writes:
>>
>> [...]
>>
>>> +--recreate-merges::
>>> +     Recreate merge commits instead of flattening the history by replaying
>>> +     merges. Merge conflict resolutions or manual amendments to merge
>>> +     commits are not preserved.
>>
>> I wonder why you guys still hold on replaying "merge-the-operation"
>> instead of replaying "merge-the-result"? The latter, the merge commit
>> itself, no matter how exactly it was created in the first place, is the
>> most valuable thing git keeps about the merge, and you silently drop it
>> entirely! OTOH, git keeps almost no information about
>> "merge-the-operation", so it's virtually impossible to reliably replay
>> the operation automatically, and yet you try to.
>>
>
> I'm not sure I follow what you mean here?
>
> You mean that you'd want this to actually attempt to re-create the
> original merge including conflict resolutions by taking the contents
> of the result?

I mean just cherry-pick the merge the same way all other commits are
essentially cherry-picked during rebase. That's what Johannes Sixt did
in his patch I was reffering to.

> How do you handle if that result has conflicts? What UX do you present
> to the user to handle such conflicts? I don't think the normal 3-way
> conflicts would even be possible in this case?

No problem here. It goes exactly the same way as for non-merge commits
that are being rebased. You can try it right now using

$ git cherry-pick -m1 <merge_commit>

that will induce conflicts.

The (somewhat tricky) functional difference is only in recording correct
additional parents to the final commit, but that part is hidden from the
user.

-- Sergey

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 5/8] rebase: introduce the --recreate-merges option
  2018-02-07  6:16   ` Sergey Organov
  2018-02-07  7:26     ` Jacob Keller
  2018-02-07  7:27     ` Johannes Sixt
@ 2018-02-07 17:36     ` Johannes Schindelin
  2018-02-07 22:58       ` Øyvind Rønningstad
  2018-02-09  6:11       ` Sergey Organov
  2 siblings, 2 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-02-07 17:36 UTC (permalink / raw)
  To: Sergey Organov; +Cc: git, Junio C Hamano, Jacob Keller, Johannes Sixt

Hi,

On Wed, 7 Feb 2018, Sergey Organov wrote:

> Johannes Schindelin <johannes.schindelin@gmx.de> writes:
> 
> [...]
> 
> > +--recreate-merges::
> > +	Recreate merge commits instead of flattening the history by replaying
> > +	merges. Merge conflict resolutions or manual amendments to merge
> > +	commits are not preserved.
> 
> I wonder why you guys still hold on replaying "merge-the-operation"
> instead of replaying "merge-the-result"?

This misses the point of rebasing: you want to replay the changes.

> The latter, the merge commit itself, no matter how exactly it was
> created in the first place, is the most valuable thing git keeps about
> the merge, and you silently drop it entirely!

You miss another very crucial point. I don't blame you, as you certainly
have not used the Git garden shears for years.

Let me explain the scenario which comes up plenty of times in my work with
Git for Windows. We have a thicket of some 70 branches on top of git.git's
latest release. These branches often include fixup! and squash! commits
and even more complicated constructs that rebase cannot handle at all at
the moment, such as reorder-before! and reorder-after! (for commits that
really need to go into a different branch).

Even if you do not have such a complicated setup, it is quite possible
that you need to include a commit in your development that needs to be
dropped before contributing your work. Think e.g. removing the `-O2` flag
when compiling with GCC because GDB gets utterly confused with executables
compiled with `-O2` while single-stepping. This could be an initial commit
called `TO-DROP` or some such.

And guess what happens if you drop that `pick` line in your todo list and
then the `merge` command simply tries to re-create the original merge
commit's changes?

Exactly. The merge will become an evil merge, and will introduce that very
much not-wanted and therefore-dropped changes.

> OTOH, git keeps almost no information about "merge-the-operation", so
> it's virtually impossible to reliably replay the operation
> automatically, and yet you try to.

That is true. However, the intended use case is not to allow you to
recreate funny merges. Its use case is to allow you to recreate merges.

At a later stage, I might introduce support to detect `-s ours` merges,
because they are easy to detect. But even then, it will be an opt-in.

> IMHO that was severe mistake in the original --preserve-merges, and you
> bring with you to this new --recreate-merges... It's sad.

Please refrain from drawing this discussion into an emotional direction.
That is definitely not helpful.

> Even more sad as solution is already known for years:
> 
>     bc00341838a8faddcd101da9e746902994eef38a
>     Author: Johannes Sixt <j6t@kdbg.org>
>     Date:   Sun Jun 16 15:50:42 2013 +0200
>     
>         rebase -p --first-parent: redo merge by cherry-picking first-parent change
> 
> and it works like a charm.

It might work for you, as you probably used --preserve-merges, and dealt
with the fact that you could neither drop nor reorder commits.

So --preserve-merges --first-parent is probably what you were looking for.

Instead, --recreate-merges is all about allowing the same level of freedom
as with regular interactive rebases, but recreating the original commit
topology (and allowing to change it, too).

Therefore, I think that it would be even harmful to allow
--recreate-merges --first-parent *because it would cause evil merges*!

And I totally could see myself being vexed again about options that worked
perfectly well (just like --preserve-merges) being completely messed up by
allowing it to be combined with options *that they cannot work with* (just
like --preserve-merges --interactive, a *huge* mistake causing so many
annoying "bug" reports: I *never intended it that way because I knew it
would not work as users expect*).

So no, I do not think that --recreate-merges --first-parent is a good idea
at all. Unless you try to do that non-interactively only, *and disallow it
in interactive mode*. Because the entire point of the interactive rebase
is to allow reordering and dropping commits, in --recreate-merges even
moving, introducing and dropping merge commits. The --first-parent option
flies in the face of this idea.

Ciao,
Johannes

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 5/8] rebase: introduce the --recreate-merges option
  2018-02-07 17:36     ` Johannes Schindelin
@ 2018-02-07 22:58       ` Øyvind Rønningstad
  2018-02-07 23:31         ` Junio C Hamano
  2018-02-09  6:11       ` Sergey Organov
  1 sibling, 1 reply; 276+ messages in thread
From: Øyvind Rønningstad @ 2018-02-07 22:58 UTC (permalink / raw)
  To: Johannes Schindelin, Sergey Organov; +Cc: git, Junio C Hamano, Jacob Keller, Johannes Sixt

edit: Sending again, hopefully without HTML :). Sorry for spamming.

Hi, I think --recreate-merges is a very exciting feature.

I've also been puzzled by why we can't just pick merge commits directly
including
conflict resolutions, so allow me to join the discussion.

On Wed, Feb 7, 2018 at 6:36 PM, Johannes Schindelin <Johannes.Schindeli
n@gmx.de> wrote:
>
> Hi,
>
> [...]
>
> And guess what happens if you drop that `pick` line in your todo list
and
> then the `merge` command simply tries to re-create the original merge
> commit's changes?
>
> Exactly. The merge will become an evil merge, and will introduce that
very
> much not-wanted and therefore-dropped changes.

I think I understand. Evil merges happen when we change the branch
that is not the mainline..? Is there any reason why the following
wouldn't work?

Imagine rebase is about to pick a merge commit, and we have edited at
least one
commit in each branch to be merged.

1. apply patch mainline_orig..merge_orig
2. apply patch branch1_orig..branch1
...
N. apply patch branchN_orig..branchN
N+1. Commit merge

I do see complications, like the fact that steps 2-N can be done in any
order, with
possibly quite different results. Moving commits from one branch to
another might
not work very well. And what to do when you remove branches or create
new ones?

These problems might be prohibitive, but picking merge commits seems
like
something that should be possible to do.

>
> [...]
>
> So --preserve-merges --first-parent is probably what you were looking
for.

I want this as well :). I don't quite see the risk if it's not used
with --interactive.

> [...]
>
> So no, I do not think that --recreate-merges --first-parent is a good
idea
> at all. Unless you try to do that non-interactively only, *and
disallow it
> in interactive mode*. Because the entire point of the interactive
rebase
> is to allow reordering and dropping commits, in --recreate-merges
even
> moving, introducing and dropping merge commits. The --first-parent
option
> flies in the face of this idea.

FWIW I'd be totally fine with disallowing it in --interactive. It would
be incredibly useful 
e.g. with pull --rebase in merge-based workflows.

BTW what is the difference between --recreate-merges and --preserve-
merges when
--interactive is not present? I apologize if you have explained this
somewhere
else in the patch series.

>
> Ciao,
> Johannes

Thanks,
Øyvind

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 5/8] rebase: introduce the --recreate-merges option
  2018-02-07 22:58       ` Øyvind Rønningstad
@ 2018-02-07 23:31         ` Junio C Hamano
  2018-02-08 12:34           ` Johannes Schindelin
  0 siblings, 1 reply; 276+ messages in thread
From: Junio C Hamano @ 2018-02-07 23:31 UTC (permalink / raw)
  To: Øyvind Rønningstad; +Cc: Johannes Schindelin, Sergey Organov, git, Jacob Keller, Johannes Sixt

Øyvind Rønningstad <ronningstad@gmail.com> writes:

>> So no, I do not think that --recreate-merges --first-parent is a good
> idea
>> at all. Unless you try to do that non-interactively only, *and
> disallow it
>> in interactive mode*.

Correct.  If the original side branch has commits A, B and C, you
are rebuilding the topic to have only A and C but not B and then
recreate the merge of that rebuilt topic, then you absolutely do not
want "cherry-pick -m1" of the original merge when recreating the
merge, as that would resurrect the effect of having B.  The same
argument applies if you rebuilt the topic with A and C and then a
new commit D.  "cherry-pick -m1" of the original would do a wrong
thing.

When there is no such fixing up, "cherry-pick -m1" is the right
thing to do, though, so it probably makes sense to pick merges that
way when the side topic being merged consists of the same commits as
the original.  I do not think that the code structure in the topic
as posted makes it impossible (or unnecessarily hard) to give an
enhancement like that in the future as a follow-up series.

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 5/8] rebase: introduce the --recreate-merges option
  2018-02-07 23:31         ` Junio C Hamano
@ 2018-02-08 12:34           ` Johannes Schindelin
  2018-02-14  5:41             ` Sergey Organov
  0 siblings, 1 reply; 276+ messages in thread
From: Johannes Schindelin @ 2018-02-08 12:34 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: Øyvind Rønningstad, Sergey Organov, git, Jacob Keller, Johannes Sixt

[-- Attachment #1: Type: text/plain, Size: 4062 bytes --]

Hi Junio,

On Wed, 7 Feb 2018, Junio C Hamano wrote:

> Øyvind Rønningstad <ronningstad@gmail.com> writes:
> 
> >> So no, I do not think that --recreate-merges --first-parent is a good
> > idea
> >> at all. Unless you try to do that non-interactively only, *and
> > disallow it
> >> in interactive mode*.
> 
> Correct.  If the original side branch has commits A, B and C, you
> are rebuilding the topic to have only A and C but not B and then
> recreate the merge of that rebuilt topic, then you absolutely do not
> want "cherry-pick -m1" of the original merge when recreating the
> merge, as that would resurrect the effect of having B.  The same
> argument applies if you rebuilt the topic with A and C and then a
> new commit D.  "cherry-pick -m1" of the original would do a wrong
> thing.
> 
> When there is no such fixing up, "cherry-pick -m1" is the right
> thing to do, though, so it probably makes sense to pick merges that
> way when the side topic being merged consists of the same commits as
> the original.

Please note that there are a lot of conditions of "fixing up". A lot more
than just dropping, reordering or adding `pick`s.

> I do not think that the code structure in the topic as posted makes it
> impossible (or unnecessarily hard) to give an enhancement like that in
> the future as a follow-up series.

Just to give you one concrete example: when I recently rebased some
patches (no reording or dropping involved here!) and one of the picks
failed with merge conflicts, I realized that that particular commit
introduced incorrect formatting and fixed that right away (verifying that
no other commits introduced incorrect formatting, of course).

With your new cute idea to magically cherry-pick -m1, this change would
have been magically dropped from the subsequent merge commits!

And let me pick a bit on the statement "I do not think that ... makes it
impossible (or unnecessarily hard) ...": I absolutely agree. I absolutely
agree that it is not impossible or unnecessarily hard to introduce
features *that are confusing the users because they are inconsistent with
the expectations how such a command should operate*.

So the question is not so much whether we can introduce a feature that
makes no sense. Of course we can, we are decent software developers.

The question is: will this be confusing, inconsistent behavior that
violates the Principle of Least Surprise?

In that respect, introducing conditional code that would `cherry-pick -m1`
when the todo list is unchanged so far (and only then) is an absolute no,
no, no. It would be all three: confusing, inconsistent and violating the
Principle of Least Surprise.

So how about introducing support for `--recreate-merges --first-parent`
and allowing to combine it with `--interactive`? Also violating all three.

I can see how you *could* argue that `--recreate-merges --first-parent` is
a Good Thing. I really can. It would even recreate evil merges.

But in interactive mode?

Nope. It would cause all kind of pain, not the least on me, because I know
how many people ask me about `--preserve-merges --interactive` and its
confusing and inconsistent behavior that violates the Principle of Least
Surprise.

So as long as y'all don't go anywhere near "oh, let's just introduce
--recreate-merges --first-parent and *then also support it in
--interactive because we can even if it hurts the user experience", I
agree that it could be a good follow-up patch series.

Taking a step back, I have to wonder, though, why we stumble over our feet
trying to cross bridges that are one farther than the one we currently
have to cross.

By now, it should be clear why the default mode of --recreate-merges
*cannot* be --first-parent.

And before --recreate-merges is said and done, we should maybe work on
getting it said and done, rather than musing about what comes next? I, for
one, would really like to focus my time on getting *this* patch series
reviewed and included.

Thanks,
Johannes

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 5/8] rebase: introduce the --recreate-merges option
  2018-02-07 17:36     ` Johannes Schindelin
  2018-02-07 22:58       ` Øyvind Rønningstad
@ 2018-02-09  6:11       ` Sergey Organov
  2018-02-09  7:13         ` Johannes Sixt
  1 sibling, 1 reply; 276+ messages in thread
From: Sergey Organov @ 2018-02-09  6:11 UTC (permalink / raw)
  To: Johannes Schindelin; +Cc: git, Junio C Hamano, Jacob Keller, Johannes Sixt

Hi,

Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:
> On Wed, 7 Feb 2018, Sergey Organov wrote:
>> Johannes Schindelin <johannes.schindelin@gmx.de> writes:
>> > +--recreate-merges::
>> > +	Recreate merge commits instead of flattening the history by replaying
>> > +	merges. Merge conflict resolutions or manual amendments to merge
>> > +	commits are not preserved.
>> 
>> I wonder why you guys still hold on replaying "merge-the-operation"
>> instead of replaying "merge-the-result"?
>
> This misses the point of rebasing: you want to replay the changes.

What this comment has to do with the statement to which it's supposed to
be a reply? Sounds like topic change to me. Please clarify if it isn't.

>
>> The latter, the merge commit itself, no matter how exactly it was
>> created in the first place, is the most valuable thing git keeps about
>> the merge, and you silently drop it entirely!
>
> You miss another very crucial point.

What was the first crucial point I miss? Do you rather agree that the
point you are replying to with this is very crucial one as well?

> I don't blame you, as you certainly have not used the Git garden
> shears for years.

Thanks a lot!

> Let me explain the scenario which comes up plenty of times in my work with
> Git for Windows. We have a thicket of some 70 branches on top of git.git's
> latest release. These branches often include fixup! and squash! commits
> and even more complicated constructs that rebase cannot handle at all at
> the moment, such as reorder-before! and reorder-after! (for commits that
> really need to go into a different branch).

I sympathize, but a solution that breaks even in simple cases can't be
used reliably to solve more complex problems, sorry. Being so deep
into your problems, I think you maybe just aren't seeing forest for the
trees [1].

> Even if you do not have such a complicated setup, it is quite possible
> that you need to include a commit in your development that needs to be
> dropped before contributing your work. Think e.g. removing the `-O2` flag
> when compiling with GCC because GDB gets utterly confused with executables
> compiled with `-O2` while single-stepping. This could be an initial commit
> called `TO-DROP` or some such.
>
> And guess what happens if you drop that `pick` line in your todo list and
> then the `merge` command simply tries to re-create the original merge
> commit's changes?
>
> Exactly. The merge will become an evil merge, and will introduce that very
> much not-wanted and therefore-dropped changes.

Okay, Houston, we've had a problem here.

I'm sure you'll be able to come-up with suitable solution once you start
to think about it positively, but automatic unguided silent re-merge is
still not the right answer, for the same reason of distortion of user
changes.

As for "evil merges"... I don't want to get too far from original
subject to even start discussing this.

>> OTOH, git keeps almost no information about "merge-the-operation", so
>> it's virtually impossible to reliably replay the operation
>> automatically, and yet you try to.
>
> That is true. However, the intended use case is not to allow you to
> recreate funny merges. Its use case is to allow you to recreate
> merges.

Then it at least should behave accordingly, e.g., stop after every such
occurrence, for user assistance. As an example, see what rerere does
when it fires, even though it's much more reliable than this blind
re-merge.

But the actual problem here is that almost any merge but those made with
pure "git merge", no options, no conflicts, no edits, no nothing,
becomes "funny" and is being destroyed, sometimes in a weird way, by
silently creating something different instead of original.

> At a later stage, I might introduce support to detect `-s ours` merges,
> because they are easy to detect. But even then, it will be an opt-in.

So you are going to fix one particular case that is "easy to detect"
(and fix). Does it mean you do realize it's a problem, but fail to see that
it's _fundamental_ problem with current approach?

I think you start from the wrong end. I think that any merge should be
made reproducible first (with possible guidance from the user when
required, as usual), and then advanced features for complex history
tweaking should come, not the other way around.

I feel that any solution that fails to exactly reproduce original
history, unless it is to be actually changed, is flawed, and we will
continue to hit our heads on sharp corners like "merge -s ours", most of
which will be not that simple to detect and fix, for an unforeseeable
future.

>
>> IMHO that was severe mistake in the original --preserve-merges, and you
>> bring with you to this new --recreate-merges... It's sad.
>
> Please refrain from drawing this discussion into an emotional direction.
> That is definitely not helpful.

I don't actually blame anybody for that original implementation in the
first place. It was made based on the actual needs, and that's perfectly
fine with me, however quick-and-dirty it was. Keeping it quick-and-dirty
for years isn't that fine though.

And I do get somewhat emotional seeing this, all right. Look what you
did. We had one pet that strikes badly (with corresponding warning label
put on it in the manual), and now we have another one that would strike
as bad! I'm even afraid that I'm unfortunately not that young anymore to
get /properly/ emotional about it.

>> Even more sad as solution is already known for years:
>> 
>>     bc00341838a8faddcd101da9e746902994eef38a
>>     Author: Johannes Sixt <j6t@kdbg.org>
>>     Date:   Sun Jun 16 15:50:42 2013 +0200
>>     
>>         rebase -p --first-parent: redo merge by cherry-picking first-parent change
>> 
>> and it works like a charm.
>
> It might work for you, as you probably used --preserve-merges, and dealt
> with the fact that you could neither drop nor reorder commits.
>
> So --preserve-merges --first-parent is probably what you were looking
> for.

No. What I'm looking for is for my history to be kept as intact as
possible during rebase, unless I explicitly ask to change it, be it
--preserve-merges, or --recreate-merges, interactive or not. Is it too
much to ask for?

And no, I don't think --preserve-merges --first-parent is the right
answer either, in general, even though it did suit most of my purposes
indeed (in fact I simply patched --preserve-merges in my local git).

> Instead, --recreate-merges is all about allowing the same level of
> freedom as with regular interactive rebases, but recreating the
> original commit topology (and allowing to change it, too).

That's a very good thing and a very nice job as a whole, sure! If it
weren't I'd not even bother to raise this topic. But provided you
realize what problem "--preserve-merges --first-parent" would solve for
non-interactive use, you should realize that you have exactly the same
problem unsolved with the new --recreate-merges.

> Therefore, I think that it would be even harmful to allow
> --recreate-merges --first-parent *because it would cause evil merges*!

Once again, for me it seems you are thinking about it from the wrong
end, and this indeed won't work, but for different reasons than you
think.

[And please, stop frightening us with those "evil merge" thingy!]

> And I totally could see myself being vexed again about options that worked
> perfectly well (just like --preserve-merges) being completely messed up by
> allowing it to be combined with options *that they cannot work with* (just
> like --preserve-merges --interactive, a *huge* mistake causing so many
> annoying "bug" reports: I *never intended it that way because I knew it
> would not work as users expect*).

IMHO it's a minor problem. At least there was a warning there in the
manual, and nobody actually claimed reliable support for the feature.

> So no, I do not think that --recreate-merges --first-parent is a good idea
> at all. Unless you try to do that non-interactively only, *and disallow it
> in interactive mode*. Because the entire point of the interactive rebase
> is to allow reordering and dropping commits, in --recreate-merges even
> moving, introducing and dropping merge commits. The --first-parent option
> flies in the face of this idea.

Forget about --first-parent, please! The patch that introduced it was
only meant to show the general way of replaying a merge. Actual
suggestion is to implement proper support right with --preserve-merges,
or call it --recreate-merges, or --keep-topology, or
--no-flatten-history, whatever [2].

-- Sergey

[1] Actually I wonder how, with such a complex setup, you seem to never
step onto the problem I regularly have? Don't you ever have sub-topic
branches on top of those 70? How do you ensure none of the merges you
are rebasing are "funny", as you call them?

[2] Proper implementation of this will likely need some tweaks to the
todo list, such as "rm" command for a commit to be removed, explicit
distinguish between "merge-redo" and "merge-pick" (or even "merge-dwim")
in the list, or something like that. But to start actually thinking
about implementation, we need to agree that silent dropping user changes
is a huge problem, be it "evil merge" or something else that sounds even
more "dangerous".

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 5/8] rebase: introduce the --recreate-merges option
  2018-01-18 15:35 ` [PATCH 5/8] rebase: introduce the --recreate-merges option Johannes Schindelin
                     ` (2 preceding siblings ...)
  2018-02-07  6:16   ` Sergey Organov
@ 2018-02-09  6:50   ` Sergey Organov
  2018-02-10 23:06     ` Johannes Schindelin
  3 siblings, 1 reply; 276+ messages in thread
From: Sergey Organov @ 2018-02-09  6:50 UTC (permalink / raw)
  To: Johannes Schindelin; +Cc: git, Junio C Hamano, Jacob Keller

Johannes Schindelin <johannes.schindelin@gmx.de> writes:

[...]

> With this patch, the goodness of the Git garden shears comes to `git
> rebase -i` itself. Passing the `--recreate-merges` option will generate
> a todo list that can be understood readily, and where it is obvious
> how to reorder commits. New branches can be introduced by inserting
> `label` commands and calling `merge - <label> <oneline>`. And once this
> mode has become stable and universally accepted, we can deprecate the
> design mistake that was `--preserve-merges`.

This doesn't explain why you introduced this new --recreate-merges. Why
didn't you rather fix --preserve-merges to generate and use new todo
list format?

It doesn't seem likely that todo list created by one Git version is to
be ever used by another, right? Is there some hidden reason here? Some
tools outside of Git that use old todo list format, maybe?

Then, if new option indeed required, please look at the resulting manual:

--recreate-merges::
	Recreate merge commits instead of flattening the history by replaying
	merges. Merge conflict resolutions or manual amendments to merge
	commits are not preserved.

-p::
--preserve-merges::
	Recreate merge commits instead of flattening the history by replaying
	commits a merge commit introduces. Merge conflict resolutions or manual
	amendments to merge commits are not preserved.


Don't you think more explanations are needed there in the manual on
why do we have 2 separate options with almost the same yet subtly
different description? Is this subtle difference even important? How?

I also have trouble making sense of "Recreate merge commits instead of
flattening the history by replaying merges." Is it "<Recreate merge
commits by replaying merges> instead of <flattening the history>" or is it
rather "<Recreate merge commits> instead of <flattening the history by
replaying merges>?

-- Sergey

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 5/8] rebase: introduce the --recreate-merges option
  2018-02-09  6:11       ` Sergey Organov
@ 2018-02-09  7:13         ` Johannes Sixt
  2018-02-11 10:16           ` Jacob Keller
  2018-02-12  7:38           ` Sergey Organov
  0 siblings, 2 replies; 276+ messages in thread
From: Johannes Sixt @ 2018-02-09  7:13 UTC (permalink / raw)
  To: Sergey Organov; +Cc: Johannes Schindelin, git, Junio C Hamano, Jacob Keller

Am 09.02.2018 um 07:11 schrieb Sergey Organov:
> Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:
>> Let me explain the scenario which comes up plenty of times in my work with
>> Git for Windows. We have a thicket of some 70 branches on top of git.git's
>> latest release. These branches often include fixup! and squash! commits
>> and even more complicated constructs that rebase cannot handle at all at
>> the moment, such as reorder-before! and reorder-after! (for commits that
>> really need to go into a different branch).
> 
> I sympathize, but a solution that breaks even in simple cases can't be
> used reliably to solve more complex problems, sorry. Being so deep
> into your problems, I think you maybe just aren't seeing forest for the
> trees [1].

Hold your horses! Dscho has a point here. --preserve-merges 
--first-parent works only as long as you don't tamper with the side 
branches. If you make changes in the side branches during the same 
rebase operation, this --first-parent mode would undo that change. (And, 
yes, its result would be called an "evil merge", and that scary name 
_should_ frighten you!)

-- Hannes

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 5/8] rebase: introduce the --recreate-merges option
  2018-01-23 20:22   ` Junio C Hamano
@ 2018-02-10 19:31     ` Johannes Schindelin
  0 siblings, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-02-10 19:31 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: git, Jacob Keller

Hi Junio,

On Tue, 23 Jan 2018, Junio C Hamano wrote:

> Johannes Schindelin <johannes.schindelin@gmx.de> writes:
> 
> > diff --git a/Documentation/git-rebase.txt b/Documentation/git-rebase.txt
> > index 8a861c1e0d6..1d061373288 100644
> > --- a/Documentation/git-rebase.txt
> > +++ b/Documentation/git-rebase.txt
> > @@ -368,6 +368,11 @@ The commit list format can be changed by setting the configuration option
> >  rebase.instructionFormat.  A customized instruction format will automatically
> >  have the long commit hash prepended to the format.
> >  
> > +--recreate-merges::
> > +	Recreate merge commits instead of flattening the history by replaying
> > +	merges. Merge conflict resolutions or manual amendments to merge
> > +	commits are not preserved.
> > +
> 
> It is sensible to postpone tackling "evil merges" in this initial
> iteration of the series, and "manual amendments ... not preserved"
> is a reasonable thing to document.  But do we want to say a bit more
> about conflicting merges?  "conflict resolutions ... not preserved"
> sounds as if it does not stop and instead record the result with
> conflict markers without even letting rerere to kick in, which
> certainly is not the impression you wanted to give to the readers.
> 
> I am imagining that it will stop and give control back to the end
> user just like a conflicted "pick" would, and allow "rebase
> --continue" to record resolution from the working tree, and just
> like conflicted "pick", it would allow rerere() to help end users
> recall previous resolution.

This is my current version:

--recreate-merges[=(rebase-cousins|no-rebase-cousins)]::
        Recreate merge commits instead of flattening the history by replaying
        merges. Merge conflict resolutions or manual amendments to merge
        commits are not recreated automatically, but have to be recreated
        manually.

Ciao,
Dscho

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH v2 02/10] sequencer: introduce new commands to reset the revision
  2018-01-30  8:06     ` Eric Sunshine
@ 2018-02-10 20:58       ` Johannes Schindelin
  0 siblings, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-02-10 20:58 UTC (permalink / raw)
  To: Eric Sunshine; +Cc: Git List, Junio C Hamano, Jacob Keller, Stefan Beller, Philip Oakley, Phillip Wood

Hi Eric,

On Tue, 30 Jan 2018, Eric Sunshine wrote:

> On Mon, Jan 29, 2018 at 5:54 PM, Johannes Schindelin
> <johannes.schindelin@gmx.de> wrote:
> > [...]
> > This commit implements the commands to label, and to reset to, given
> > revisions. The syntax is:
> >
> >         label <name>
> >         reset <name>
> > [...]
> >
> > Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
> > ---
> > diff --git a/sequencer.c b/sequencer.c
> > @@ -1253,7 +1266,8 @@ static int parse_insn_line(struct todo_item *item, const char *bol, char *eol)
> >                 if (skip_prefix(bol, todo_command_info[i].str, &bol)) {
> >                         item->command = i;
> >                         break;
> > -               } else if (bol[1] == ' ' && *bol == todo_command_info[i].c) {
> > +               } else if ((bol + 1 == eol || bol[1] == ' ') &&
> > +                          *bol == todo_command_info[i].c) {
> 
> This adds support for commands which have no arguments, however, now
> that the "bud" command has been retired, this can go away too, right?

Good point. Fixed.

> >                         bol++;
> >                         item->command = i;
> >                         break;
> > @@ -1919,6 +1934,144 @@ static int do_exec(const char *command_line)
> > +static int safe_append(const char *filename, const char *fmt, ...)
> > +{
> > +       va_list ap;
> > +       struct lock_file lock = LOCK_INIT;
> > +       int fd = hold_lock_file_for_update(&lock, filename, 0);
> > +       struct strbuf buf = STRBUF_INIT;
> > +
> > +       if (fd < 0)
> > +               return error_errno(_("could not lock '%s'"), filename);
> 
> Minor: unable_to_lock_message() can provide a more detailed
> explanation of the failure.

That is true. Due to its awkward signature (returning void, using a
strbuf), it would add a whopping 4 lines, too.

There is a better solution, though, adding only one line: passing
LOCK_REPORT_ON_ERROR as flag to hold_lock_file_for_update().

> > +
> > +       if (strbuf_read_file(&buf, filename, 0) < 0 && errno != ENOENT)
> > +               return error_errno(_("could not read '%s'"), filename);
> > +       strbuf_complete(&buf, '\n');
> > +       va_start(ap, fmt);
> > +       strbuf_vaddf(&buf, fmt, ap);
> > +       va_end(ap);
> 
> Would it make sense to also
> 
>     strbuf_complete(&buf, '\n')
> 
> here, as well, to be a bit more robust against lazy callers?

I'd rather not make that assumption. It *may* be true that the current
sole user wants the last line of the file to end in a newline. I try to
design my code for maximum reusability, though. And who is to say whether
my next use case for the safe_append() function wants the semantics you
suggest, if it wants to append less than entire lines at a time, maybe?
Let's not optimize prematurely, okay?

> > +
> > +       if (write_in_full(fd, buf.buf, buf.len) < 0) {
> > +               rollback_lock_file(&lock);
> > +               return error_errno(_("could not write to '%s'"), filename);
> 
> Reading lockfile.h & tempfile.c, I see that rollback_lock_file()
> clobbers write_in_full()'s errno before error_errno() is called.

True. Fixed.

I also fixed the code from where I copy-edited this pattern (increasing
the patch series by yet another patch).

> > +       }
> > +       if (commit_lock_file(&lock) < 0) {
> > +               rollback_lock_file(&lock);
> > +               return error(_("failed to finalize '%s'"), filename);
> > +       }
> > +
> > +       return 0;
> > +}
> > +
> > +static int do_reset(const char *name, int len)
> > +{
> > +       [...]
> > +       strbuf_addf(&ref_name, "refs/rewritten/%.*s", len, name);
> > +       if (get_oid(ref_name.buf, &oid) &&
> > +           get_oid(ref_name.buf + strlen("refs/rewritten/"), &oid)) {
> > +               error(_("could not read '%s'"), ref_name.buf);
> 
> Checking my understanding: The two get_oid() calls allow the argument
> to 'reset' to be a label created with the 'label' command or any other
> way to name an object, right? If so, then I wonder if the error
> invocation should instead be:
> 
>     error(_("could not read '%.*s'"), len, name);

I would rather give the preferred form: refs/rewritten/<label>.

The main reason this code falls back to getting the OID of `<label>`
directly is to support the `no-rebase-cousins` code: in that mode, topic
branches may be based on commits other than the one labeled `onto`, but
the original, unchanged one. In this case, we have no way of labeling the
base commit, and therefore use a unique abbreviation of that base commit's
OID.

But this is really a very special use case, and the more common use case
should be the one using refs/rewritten/<label>.

Ciao,
Dscho

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH v2 02/10] sequencer: introduce new commands to reset therevision
  2018-01-31 18:02         ` [PATCH v2 02/10] sequencer: introduce new commands to reset therevision Phillip Wood
@ 2018-02-10 21:49           ` Johannes Schindelin
  0 siblings, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-02-10 21:49 UTC (permalink / raw)
  To: Phillip Wood; +Cc: Stefan Beller, git, Junio C Hamano, Jacob Keller, Philip Oakley, Eric Sunshine

Hi Phillip,

On Wed, 31 Jan 2018, Phillip Wood wrote:

> On 31/01/18 13:21, Johannes Schindelin wrote:
> > 
> > On Tue, 30 Jan 2018, Stefan Beller wrote:
> > 
> >> On Mon, Jan 29, 2018 at 2:54 PM, Johannes Schindelin
> >> <johannes.schindelin@gmx.de> wrote:
> >>> @@ -116,6 +118,13 @@ static GIT_PATH_FUNC(rebase_path_stopped_sha, "rebase-merge/stopped-sha")
> >>>  static GIT_PATH_FUNC(rebase_path_rewritten_list, "rebase-merge/rewritten-list")
> >>>  static GIT_PATH_FUNC(rebase_path_rewritten_pending,
> >>>         "rebase-merge/rewritten-pending")
> >>> +
> >>> +/*
> >>> + * The path of the file listing refs that need to be deleted after the rebase
> >>> + * finishes. This is used by the `merge` command.
> >>> + */
> > 
> > Whoops. The comment "This is used by the `merge` command`" is completely
> > wrong. Will fix.
> > 
> >> So this file contains (label -> commit),
> > 
> > Only `label`. No `commit`.
> > 
> >> which is appended in do_label, it uses refs to store the commits in
> >> refs/rewritten.  We do not have to worry about the contents of that file
> >> getting too long, or label re-use, because the directory containing all
> >> these helper files will be deleted upon successful rebase in
> >> `sequencer_remove_state()`.
> > 
> > Yes.
> >
> It might be a good idea to have 'git rebase --abort' delete the refs as
> well as the file though

That makes sense. I made it so.

Ciao,
Dscho

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 5/8] rebase: introduce the --recreate-merges option
  2018-02-09  6:50   ` Sergey Organov
@ 2018-02-10 23:06     ` Johannes Schindelin
  2018-02-12  4:58       ` Sergey Organov
  2018-02-12  5:22       ` Sergey Organov
  0 siblings, 2 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-02-10 23:06 UTC (permalink / raw)
  To: Sergey Organov; +Cc: git, Junio C Hamano, Jacob Keller

Hi Sergey,

On Fri, 9 Feb 2018, Sergey Organov wrote:

> Johannes Schindelin <johannes.schindelin@gmx.de> writes:
> 
> [...]
> 
> > With this patch, the goodness of the Git garden shears comes to `git
> > rebase -i` itself. Passing the `--recreate-merges` option will generate
> > a todo list that can be understood readily, and where it is obvious
> > how to reorder commits. New branches can be introduced by inserting
> > `label` commands and calling `merge - <label> <oneline>`. And once this
> > mode has become stable and universally accepted, we can deprecate the
> > design mistake that was `--preserve-merges`.
> 
> This doesn't explain why you introduced this new --recreate-merges. Why
> didn't you rather fix --preserve-merges to generate and use new todo
> list format?

Because that would of course break existing users of --preserve-merges.

So why not --preserve-merges=v2? Because that would force me to maintain
--preserve-merges forever. And I don't want to.

> It doesn't seem likely that todo list created by one Git version is to
> be ever used by another, right?

No. But by scripts based on `git rebase -p`.

> Is there some hidden reason here? Some tools outside of Git that use old
> todo list format, maybe?

Exactly.

I did mention such a tool: the Git garden shears:

	https://github.com/git-for-windows/build-extra/blob/master/shears.sh

Have a look at it. It will inform the discussion.

> Then, if new option indeed required, please look at the resulting manual:
> 
> --recreate-merges::
> 	Recreate merge commits instead of flattening the history by replaying
> 	merges. Merge conflict resolutions or manual amendments to merge
> 	commits are not preserved.
> 
> -p::
> --preserve-merges::
> 	Recreate merge commits instead of flattening the history by replaying
> 	commits a merge commit introduces. Merge conflict resolutions or manual
> 	amendments to merge commits are not preserved.

As I stated in the cover letter, there are more patches lined up after
this patch series.

Have a look at https://github.com/git/git/pull/447, especially the latest
commit in there which is an early version of the deprecation I intend to
bring about.

Also, please refrain from saying things like... "Don't you think ..."

If you don't like the wording, I wold much more appreciate it if a better
alternative was suggested.

> Don't you think more explanations are needed there in the manual on
> why do we have 2 separate options with almost the same yet subtly
> different description? Is this subtle difference even important? How?
> 
> I also have trouble making sense of "Recreate merge commits instead of
> flattening the history by replaying merges." Is it "<Recreate merge
> commits by replaying merges> instead of <flattening the history>" or is it
> rather "<Recreate merge commits> instead of <flattening the history by
> replaying merges>?

The documentation of the --recreate-merges option is not meant to explain
the difference to --preserve-merges. It is meant to explain the difference
to regular `git rebase -i`, which flattens the commit history into a
single branch without merge commits (in fact, all merge commits are simply
ignored).

And I would rather not start to describe the difference between
--recreate-merges and --preserve-merges because I want to deprecate the
latter, and describing the difference as I get the sense is your wish
would simply mean more work because it would have to be added and then
removed again.

If you still think it would be a good idea to describe the difference
between --recreate-merges and --preserve-merges, then please provide a
suggestion, preferably in the form of a patch, that adds appropriate
paragraphs to *both* options' documentation, so that your proposal can be
discussed properly.

Ciao,
Johannes

^ permalink raw reply	[flat|nested] 276+ messages in thread

* [PATCH v3 00/12] rebase -i: offer to recreate merge commits
  2018-01-29 22:54 ` [PATCH v2 00/10] " Johannes Schindelin
                     ` (11 preceding siblings ...)
  2018-01-30 21:36   ` Junio C Hamano
@ 2018-02-11  0:09   ` " Johannes Schindelin
  2018-02-11  0:10     ` [PATCH v3 01/12] sequencer: avoid using errno clobbered by rollback_lock_file() Johannes Schindelin
                       ` (13 more replies)
  12 siblings, 14 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-02-11  0:09 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Jacob Keller, Stefan Beller, Philip Oakley, Eric Sunshine, Phillip Wood

Once upon a time, I dreamt of an interactive rebase that would not
flatten branch structure, but instead recreate the commit topology
faithfully.

My original attempt was --preserve-merges, but that design was so
limited that I did not even enable it in interactive mode.

Subsequently, it *was* enabled in interactive mode, with the predictable
consequences: as the --preserve-merges design does not allow for
specifying the parents of merge commits explicitly, all the new commits'
parents are defined *implicitly* by the previous commit history, and
hence it is *not possible to even reorder commits*.

This design flaw cannot be fixed. Not without a complete re-design, at
least. This patch series offers such a re-design.

Think of --recreate-merges as "--preserve-merges done right". It
introduces new verbs for the todo list, `label`, `reset` and `merge`.
For a commit topology like this:

            A - B - C
              \   /
                D

the generated todo list would look like this:

            # branch D
            pick 0123 A
            label branch-point
            pick 1234 D
            label D

            reset branch-point
            pick 2345 B
            merge -C 3456 D # C

There are more patches in the pipeline, based on this patch series, but
left for later in the interest of reviewable patch series: one mini
series to use the sequencer even for `git rebase -i --root`, and another
one to add support for octopus merges to --recreate-merges.

Changes since v2:

- fixed the incorrect comment for rebase_path_refs_to_delete.

- we now error out properly if read_cache_unmerged() fails.

- if there are unresolved merge conflicts, the `reset` command now errors out
  (even if the current design should not allow for such a scenario to occur).

- a diff hunk that was necessary to support `bud` was dropped from 2/10.

- changed all `rollback_lock_file(); return error_errno(...);` patterns to
  first show the errors (i.e. using the correct errno). This added 1/11.

- The temporary refs are now also cleaned up upon `git rebase --abort`.

- Reworked the entire patch series to support

	merge -C <commit> <tip> # <oneline>

  instead of the previous `merge <commit> <tip> <oneline>`.

- Dropped the octopus part of the description of the `merge` command in
  the usage at the bottom of the todo list, as it is subject to change.

- The autosquash handling was not elegant, and cuddled into the same
  commit as the post-rewrite changes. Now, the autosquash handling is a
  lot more elegant, and a separate introductory patch (as it arguably
  improves the current code on its own).


Johannes Schindelin (11):
  sequencer: avoid using errno clobbered by rollback_lock_file()
  sequencer: make rearrange_squash() a bit more obvious
  sequencer: introduce new commands to reset the revision
  sequencer: introduce the `merge` command
  sequencer: fast-forward merge commits, if possible
  rebase-helper --make-script: introduce a flag to recreate merges
  rebase: introduce the --recreate-merges option
  sequencer: make refs generated by the `label` command worktree-local
  sequencer: handle post-rewrite for merge commands
  pull: accept --rebase=recreate to recreate the branch topology
  rebase -i: introduce --recreate-merges=[no-]rebase-cousins

Stefan Beller (1):
  git-rebase--interactive: clarify arguments

 Documentation/config.txt               |   8 +
 Documentation/git-pull.txt             |   5 +-
 Documentation/git-rebase.txt           |  14 +-
 builtin/pull.c                         |  14 +-
 builtin/rebase--helper.c               |  13 +-
 builtin/remote.c                       |   2 +
 contrib/completion/git-completion.bash |   4 +-
 git-rebase--interactive.sh             |  22 +-
 git-rebase.sh                          |  16 +
 refs.c                                 |   3 +-
 sequencer.c                            | 736 ++++++++++++++++++++++++++++++++-
 sequencer.h                            |   7 +
 t/t3430-rebase-recreate-merges.sh      | 208 ++++++++++
 13 files changed, 1021 insertions(+), 31 deletions(-)
 create mode 100755 t/t3430-rebase-recreate-merges.sh


base-commit: 5be1f00a9a701532232f57958efab4be8c959a29
Published-As: https://github.com/dscho/git/releases/tag/recreate-merges-v3
Fetch-It-Via: git fetch https://github.com/dscho/git recreate-merges-v3

Interdiff vs v2:
 diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh
 index 5e21e4cf269..e199fe1cca5 100644
 --- a/git-rebase--interactive.sh
 +++ b/git-rebase--interactive.sh
 @@ -164,10 +164,10 @@ x, exec <commit> = run command (the rest of the line) using shell
  d, drop <commit> = remove commit
  l, label <label> = label current HEAD with a name
  t, reset <label> = reset HEAD to a label
 -m, merge <original-merge-commit> ( <label> | \"<label>...\" ) [<oneline>]
 +m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
  .       create a merge commit using the original merge commit's
 -.       message (or the oneline, if "-" is given). Use a quoted
 -.       list of commits to be merged for octopus merges.
 +.       message (or the oneline, if no original merge commit was
 +.       specified). Use -c <commit> to reword the commit message.
  
  These lines can be re-ordered; they are executed from top to bottom.
  " | git stripspace --comment-lines >>"$todo"
 diff --git a/sequencer.c b/sequencer.c
 index cd2f2ae5d53..c877432d7b4 100644
 --- a/sequencer.c
 +++ b/sequencer.c
 @@ -123,7 +123,7 @@ static GIT_PATH_FUNC(rebase_path_rewritten_pending,
  
  /*
   * The path of the file listing refs that need to be deleted after the rebase
 - * finishes. This is used by the `merge` command.
 + * finishes. This is used by the `label` command to record the need for cleanup.
   */
  static GIT_PATH_FUNC(rebase_path_refs_to_delete, "rebase-merge/refs-to-delete")
  
 @@ -206,18 +206,33 @@ static const char *gpg_sign_opt_quoted(struct replay_opts *opts)
  
  int sequencer_remove_state(struct replay_opts *opts)
  {
 -	struct strbuf dir = STRBUF_INIT;
 +	struct strbuf buf = STRBUF_INIT;
  	int i;
  
 +	if (strbuf_read_file(&buf, rebase_path_refs_to_delete(), 0) > 0) {
 +		char *p = buf.buf;
 +		while (*p) {
 +			char *eol = strchr(p, '\n');
 +			if (eol)
 +				*eol = '\0';
 +			if (delete_ref("(rebase -i) cleanup", p, NULL, 0) < 0)
 +				warning(_("could not delete '%s'"), p);
 +			if (!eol)
 +				break;
 +			p = eol + 1;
 +		}
 +	}
 +
  	free(opts->gpg_sign);
  	free(opts->strategy);
  	for (i = 0; i < opts->xopts_nr; i++)
  		free(opts->xopts[i]);
  	free(opts->xopts);
  
 -	strbuf_addstr(&dir, get_dir(opts));
 -	remove_dir_recursively(&dir, 0);
 -	strbuf_release(&dir);
 +	strbuf_reset(&buf);
 +	strbuf_addstr(&buf, get_dir(opts));
 +	remove_dir_recursively(&buf, 0);
 +	strbuf_release(&buf);
  
  	return 0;
  }
 @@ -307,12 +322,14 @@ static int write_message(const void *buf, size_t len, const char *filename,
  	if (msg_fd < 0)
  		return error_errno(_("could not lock '%s'"), filename);
  	if (write_in_full(msg_fd, buf, len) < 0) {
 +		error_errno(_("could not write to '%s'"), filename);
  		rollback_lock_file(&msg_file);
 -		return error_errno(_("could not write to '%s'"), filename);
 +		return -1;
  	}
  	if (append_eol && write(msg_fd, "\n", 1) < 0) {
 +		error_errno(_("could not write eol to '%s'"), filename);
  		rollback_lock_file(&msg_file);
 -		return error_errno(_("could not write eol to '%s'"), filename);
 +		return -1;
  	}
  	if (commit_lock_file(&msg_file) < 0) {
  		rollback_lock_file(&msg_file);
 @@ -781,6 +798,7 @@ enum todo_command {
  	TODO_LABEL,
  	TODO_RESET,
  	TODO_MERGE,
 +	TODO_MERGE_AND_EDIT,
  	/* commands that do nothing but are counted for reporting progress */
  	TODO_NOOP,
  	TODO_DROP,
 @@ -802,6 +820,7 @@ static struct {
  	{ 'l', "label" },
  	{ 't', "reset" },
  	{ 'm', "merge" },
 +	{ 0, "merge" }, /* MERGE_AND_EDIT */
  	{ 0,   "noop" },
  	{ 'd', "drop" },
  	{ 0,   NULL }
 @@ -1270,8 +1289,7 @@ static int parse_insn_line(struct todo_item *item, const char *bol, char *eol)
  		if (skip_prefix(bol, todo_command_info[i].str, &bol)) {
  			item->command = i;
  			break;
 -		} else if ((bol + 1 == eol || bol[1] == ' ') &&
 -			   *bol == todo_command_info[i].c) {
 +		} else if (bol[1] == ' ' && *bol == todo_command_info[i].c) {
  			bol++;
  			item->command = i;
  			break;
 @@ -1305,21 +1323,30 @@ static int parse_insn_line(struct todo_item *item, const char *bol, char *eol)
  		return 0;
  	}
  
 -	end_of_object_name = (char *) bol + strcspn(bol, " \t\n");
 -	item->arg = end_of_object_name + strspn(end_of_object_name, " \t");
 -	item->arg_len = (int)(eol - item->arg);
 -
 -	if (item->command == TODO_MERGE && *bol == '-' &&
 -	    bol + 1 == end_of_object_name) {
 -		item->commit = NULL;
 -		return 0;
 +	if (item->command == TODO_MERGE) {
 +		if (skip_prefix(bol, "-C", &bol))
 +			bol += strspn(bol, " \t");
 +		else if (skip_prefix(bol, "-c", &bol)) {
 +			bol += strspn(bol, " \t");
 +			item->command = TODO_MERGE_AND_EDIT;
 +		} else {
 +			item->command = TODO_MERGE_AND_EDIT;
 +			item->commit = NULL;
 +			item->arg = bol;
 +			item->arg_len = (int)(eol - bol);
 +			return 0;
 +		}
  	}
  
 +	end_of_object_name = (char *) bol + strcspn(bol, " \t\n");
  	saved = *end_of_object_name;
  	*end_of_object_name = '\0';
  	status = get_oid(bol, &commit_oid);
  	*end_of_object_name = saved;
  
 +	item->arg = end_of_object_name + strspn(end_of_object_name, " \t");
 +	item->arg_len = (int)(eol - item->arg);
 +
  	if (status < 0)
  		return -1;
  
 @@ -1609,16 +1636,17 @@ static int save_head(const char *head)
  
  	fd = hold_lock_file_for_update(&head_lock, git_path_head_file(), 0);
  	if (fd < 0) {
 +		error_errno(_("could not lock HEAD"));
  		rollback_lock_file(&head_lock);
 -		return error_errno(_("could not lock HEAD"));
 +		return -1;
  	}
  	strbuf_addf(&buf, "%s\n", head);
  	written = write_in_full(fd, buf.buf, buf.len);
  	strbuf_release(&buf);
  	if (written < 0) {
 +		error_errno(_("could not write to '%s'"), git_path_head_file());
  		rollback_lock_file(&head_lock);
 -		return error_errno(_("could not write to '%s'"),
 -				   git_path_head_file());
 +		return -1;
  	}
  	if (commit_lock_file(&head_lock) < 0) {
  		rollback_lock_file(&head_lock);
 @@ -1948,11 +1976,12 @@ static int safe_append(const char *filename, const char *fmt, ...)
  {
  	va_list ap;
  	struct lock_file lock = LOCK_INIT;
 -	int fd = hold_lock_file_for_update(&lock, filename, 0);
 +	int fd = hold_lock_file_for_update(&lock, filename,
 +					   LOCK_REPORT_ON_ERROR);
  	struct strbuf buf = STRBUF_INIT;
  
  	if (fd < 0)
 -		return error_errno(_("could not lock '%s'"), filename);
 +		return -1;
  
  	if (strbuf_read_file(&buf, filename, 0) < 0 && errno != ENOENT)
  		return error_errno(_("could not read '%s'"), filename);
 @@ -1962,8 +1991,9 @@ static int safe_append(const char *filename, const char *fmt, ...)
  	va_end(ap);
  
  	if (write_in_full(fd, buf.buf, buf.len) < 0) {
 +		error_errno(_("could not write to '%s'"), filename);
  		rollback_lock_file(&lock);
 -		return error_errno(_("could not write to '%s'"), filename);
 +		return -1;
  	}
  	if (commit_lock_file(&lock) < 0) {
  		rollback_lock_file(&lock);
 @@ -1982,6 +2012,9 @@ static int do_label(const char *name, int len)
  	int ret = 0;
  	struct object_id head_oid;
  
 +	if (len == 1 && *name == '#')
 +		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);
  
 @@ -2010,14 +2043,14 @@ static int do_label(const char *name, int len)
  	return ret;
  }
  
 -static int do_reset(const char *name, int len)
 +static int do_reset(const char *name, int len, struct replay_opts *opts)
  {
  	struct strbuf ref_name = STRBUF_INIT;
  	struct object_id oid;
  	struct lock_file lock = LOCK_INIT;
  	struct tree_desc desc;
  	struct tree *tree;
 -	struct unpack_trees_options opts;
 +	struct unpack_trees_options unpack_tree_opts;
  	int ret = 0, i;
  
  	if (hold_locked_index(&lock, LOCK_REPORT_ON_ERROR) < 0)
 @@ -2037,16 +2070,18 @@ static int do_reset(const char *name, int len)
  		return -1;
  	}
  
 -	memset(&opts, 0, sizeof(opts));
 -	opts.head_idx = 1;
 -	opts.src_index = &the_index;
 -	opts.dst_index = &the_index;
 -	opts.fn = oneway_merge;
 -	opts.merge = 1;
 -	opts.update = 1;
 -	opts.reset = 1;
 +	memset(&unpack_tree_opts, 0, sizeof(unpack_tree_opts));
 +	unpack_tree_opts.head_idx = 1;
 +	unpack_tree_opts.src_index = &the_index;
 +	unpack_tree_opts.dst_index = &the_index;
 +	unpack_tree_opts.fn = oneway_merge;
 +	unpack_tree_opts.merge = 1;
 +	unpack_tree_opts.update = 1;
 +	unpack_tree_opts.reset = 1;
 +
 +	if (read_cache_unmerged())
 +		return error_resolve_conflict(_(action_name(opts)));
  
 -	read_cache_unmerged();
  	if (!fill_tree_descriptor(&desc, &oid)) {
  		error(_("failed to find tree of %s"), oid_to_hex(&oid));
  		rollback_lock_file(&lock);
 @@ -2055,7 +2090,7 @@ static int do_reset(const char *name, int len)
  		return -1;
  	}
  
 -	if (unpack_trees(1, &desc, &opts)) {
 +	if (unpack_trees(1, &desc, &unpack_tree_opts)) {
  		rollback_lock_file(&lock);
  		free((void *)desc.buffer);
  		strbuf_release(&ref_name);
 @@ -2083,7 +2118,7 @@ static int do_reset(const char *name, int len)
  }
  
  static int do_merge(struct commit *commit, const char *arg, int arg_len,
 -		    struct replay_opts *opts)
 +		    int run_commit_flags, struct replay_opts *opts)
  {
  	int merge_arg_len;
  	struct strbuf ref_name = STRBUF_INIT;
 @@ -2137,6 +2172,8 @@ static int do_merge(struct commit *commit, const char *arg, int arg_len,
  		strbuf_reset(&buf);
  
  		p += strspn(p, " \t");
 +		if (*p == '#' && isspace(p[1]))
 +			p += 1 + strspn(p + 1, " \t");
  		if (*p)
  			len = strlen(p);
  		else {
 @@ -2221,7 +2258,7 @@ static int do_merge(struct commit *commit, const char *arg, int arg_len,
  	}
  	rollback_lock_file(&lock);
  
 -	ret = run_git_commit(git_path_merge_msg(), opts, 0);
 +	ret = run_git_commit(git_path_merge_msg(), opts, run_commit_flags);
  	strbuf_release(&ref_name);
  
  	return ret;
 @@ -2413,10 +2450,12 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts)
  		} else if (item->command == TODO_LABEL)
  			res = do_label(item->arg, item->arg_len);
  		else if (item->command == TODO_RESET)
 -			res = do_reset(item->arg, item->arg_len);
 -		else if (item->command == TODO_MERGE) {
 -			res = do_merge(item->commit,
 -				       item->arg, item->arg_len, opts);
 +			res = do_reset(item->arg, item->arg_len, opts);
 +		else if (item->command == TODO_MERGE ||
 +			 item->command == TODO_MERGE_AND_EDIT) {
 +			res = do_merge(item->commit, item->arg, item->arg_len,
 +				       item->command == TODO_MERGE_AND_EDIT ?
 +				       EDIT_MSG | VERIFY_MSG : 0, opts);
  			if (item->commit)
  				record_in_rewritten(&item->commit->object.oid,
  						    peek_command(todo_list, 1));
 @@ -2525,23 +2564,6 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts)
  		}
  		apply_autostash(opts);
  
 -		strbuf_reset(&buf);
 -		if (strbuf_read_file(&buf, rebase_path_refs_to_delete(), 0)
 -		    > 0) {
 -			char *p = buf.buf;
 -			while (*p) {
 -				char *eol = strchr(p, '\n');
 -				if (eol)
 -					*eol = '\0';
 -				if (delete_ref("(rebase -i) cleanup",
 -					       p, NULL, 0) < 0)
 -					warning(_("could not delete '%s'"), p);
 -				if (!eol)
 -					break;
 -				p = eol + 1;
 -			}
 -		}
 -
  		fprintf(stderr, "Successfully rebased and updated %s.\n",
  			head_ref.buf);
  
 @@ -2867,11 +2889,14 @@ static const char *label_oid(struct object_id *oid, const char *label,
  		}
  	} else if (((len = strlen(label)) == GIT_SHA1_RAWSZ &&
  		    !get_oid_hex(label, &dummy)) ||
 +		   (len == 1 && *label == '#') ||
  		   hashmap_get_from_hash(&state->labels,
  					 strihash(label), label)) {
  		/*
  		 * If the label already exists, or if the label is a valid full
 -		 * OID, we append a dash and a number to make it unique.
 +		 * OID, or the label is a '#' (which we use as a separator
 +		 * between merge heads and oneline), we append a dash and a
 +		 * number to make it unique.
  		 */
  		struct strbuf *buf = &state->buf;
  
 @@ -2997,7 +3022,7 @@ static int make_script_with_merges(struct pretty_print_context *pp,
  				*(char *)p1 = '-';
  
  		strbuf_reset(&buf);
 -		strbuf_addf(&buf, "%s %s",
 +		strbuf_addf(&buf, "%s -C %s",
  			    cmd_merge, oid_to_hex(&commit->object.oid));
  
  		/* label the tip of merged branch */
 @@ -3012,7 +3037,7 @@ static int make_script_with_merges(struct pretty_print_context *pp,
  
  			strbuf_addstr(&buf, label_oid(oid, label.buf, &state));
  		}
 -		strbuf_addf(&buf, " %s", oneline.buf);
 +		strbuf_addf(&buf, " # %s", oneline.buf);
  
  		FLEX_ALLOC_STR(entry, string, buf.buf);
  		oidcpy(&entry->entry.oid, &commit->object.oid);
 @@ -3262,9 +3287,14 @@ int transform_todos(unsigned flags)
  					  short_commit_name(item->commit) :
  					  oid_to_hex(&item->commit->object.oid);
  
 +			if (item->command == TODO_MERGE)
 +				strbuf_addstr(&buf, " -C");
 +			else if (item->command == TODO_MERGE_AND_EDIT)
 +				strbuf_addstr(&buf, " -c");
 +
  			strbuf_addf(&buf, " %s", oid);
 -		} else if (item->command == TODO_MERGE)
 -			strbuf_addstr(&buf, " -");
 +		}
 +
  		/* add all the rest */
  		if (!item->arg_len)
  			strbuf_addch(&buf, '\n');
 @@ -3567,8 +3597,7 @@ int rearrange_squash(void)
  		struct subject2item_entry *entry;
  
  		next[i] = tail[i] = -1;
 -		if (item->command >= TODO_EXEC &&
 -		    (item->command != TODO_MERGE || !item->commit)) {
 +		if (!item->commit || item->command == TODO_DROP) {
  			subjects[i] = NULL;
  			continue;
  		}
 diff --git a/t/t3430-rebase-recreate-merges.sh b/t/t3430-rebase-recreate-merges.sh
 index ab51b584ff9..9a59f12b670 100755
 --- a/t/t3430-rebase-recreate-merges.sh
 +++ b/t/t3430-rebase-recreate-merges.sh
 @@ -59,8 +59,8 @@ pick B
  label second
  
  reset onto
 -merge H second
 -merge - onebranch Merge the topic branch 'onebranch'
 +merge -C H second
 +merge onebranch # Merge the topic branch 'onebranch'
  EOF
  
  test_cmp_graph () {
 @@ -106,8 +106,8 @@ test_expect_success 'generate correct todo list' '
  
  	reset branch-point # C
  	pick 12bd07b D
 -	merge 2051b56 E E
 -	merge 233d48a H H
 +	merge -C 2051b56 E # E
 +	merge -C 233d48a H # H
  
  	EOF
  
-- 
2.16.1.windows.1


^ permalink raw reply	[flat|nested] 276+ messages in thread

* [PATCH v3 01/12] sequencer: avoid using errno clobbered by rollback_lock_file()
  2018-02-11  0:09   ` [PATCH v3 00/12] " Johannes Schindelin
@ 2018-02-11  0:10     ` Johannes Schindelin
  2018-02-11  0:10     ` [PATCH v3 02/12] sequencer: make rearrange_squash() a bit more obvious Johannes Schindelin
                       ` (12 subsequent siblings)
  13 siblings, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-02-11  0:10 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Jacob Keller, Stefan Beller, Philip Oakley, Eric Sunshine, Phillip Wood

As pointed out in a review of the `--recreate-merges` patch series,
`rollback_lock_file()` clobbers errno. Therefore, we have to report the
error message that uses errno before calling said function.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 sequencer.c | 13 ++++++++-----
 1 file changed, 8 insertions(+), 5 deletions(-)

diff --git a/sequencer.c b/sequencer.c
index 4d3f60594cb..114db3b2775 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -296,12 +296,14 @@ static int write_message(const void *buf, size_t len, const char *filename,
 	if (msg_fd < 0)
 		return error_errno(_("could not lock '%s'"), filename);
 	if (write_in_full(msg_fd, buf, len) < 0) {
+		error_errno(_("could not write to '%s'"), filename);
 		rollback_lock_file(&msg_file);
-		return error_errno(_("could not write to '%s'"), filename);
+		return -1;
 	}
 	if (append_eol && write(msg_fd, "\n", 1) < 0) {
+		error_errno(_("could not write eol to '%s'"), filename);
 		rollback_lock_file(&msg_file);
-		return error_errno(_("could not write eol to '%s'"), filename);
+		return -1;
 	}
 	if (commit_lock_file(&msg_file) < 0) {
 		rollback_lock_file(&msg_file);
@@ -1584,16 +1586,17 @@ static int save_head(const char *head)
 
 	fd = hold_lock_file_for_update(&head_lock, git_path_head_file(), 0);
 	if (fd < 0) {
+		error_errno(_("could not lock HEAD"));
 		rollback_lock_file(&head_lock);
-		return error_errno(_("could not lock HEAD"));
+		return -1;
 	}
 	strbuf_addf(&buf, "%s\n", head);
 	written = write_in_full(fd, buf.buf, buf.len);
 	strbuf_release(&buf);
 	if (written < 0) {
+		error_errno(_("could not write to '%s'"), git_path_head_file());
 		rollback_lock_file(&head_lock);
-		return error_errno(_("could not write to '%s'"),
-				   git_path_head_file());
+		return -1;
 	}
 	if (commit_lock_file(&head_lock) < 0) {
 		rollback_lock_file(&head_lock);
-- 
2.16.1.windows.1



^ permalink raw reply	[flat|nested] 276+ messages in thread

* [PATCH v3 02/12] sequencer: make rearrange_squash() a bit more obvious
  2018-02-11  0:09   ` [PATCH v3 00/12] " Johannes Schindelin
  2018-02-11  0:10     ` [PATCH v3 01/12] sequencer: avoid using errno clobbered by rollback_lock_file() Johannes Schindelin
@ 2018-02-11  0:10     ` Johannes Schindelin
  2018-02-11  0:10     ` [PATCH v3 03/12] git-rebase--interactive: clarify arguments Johannes Schindelin
                       ` (11 subsequent siblings)
  13 siblings, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-02-11  0:10 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Jacob Keller, Stefan Beller, Philip Oakley, Eric Sunshine, Phillip Wood

There are some commands that have to be skipped from rearranging by virtue
of not handling any commits.

However, the logic was not quite obvious: it skipped commands based on
their position in the enum todo_command.

Instead, let's make it explicit that we skip all commands that do not
handle any commit. With one exception: the `drop` command, because it,
well, drops the commit and is therefore not eligible to rearranging.

Note: this is a bit academic at the moment because the only time we call
`rearrange_squash()` is directly after generating the todo list, when we
have nothing but `pick` commands anyway.

However, the upcoming `merge` command *will* want to be handled by that
function, and it *can* handle commits.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 sequencer.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/sequencer.c b/sequencer.c
index 114db3b2775..764ad43388f 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -2890,7 +2890,7 @@ int rearrange_squash(void)
 		struct subject2item_entry *entry;
 
 		next[i] = tail[i] = -1;
-		if (item->command >= TODO_EXEC) {
+		if (!item->commit || item->command == TODO_DROP) {
 			subjects[i] = NULL;
 			continue;
 		}
-- 
2.16.1.windows.1



^ permalink raw reply	[flat|nested] 276+ messages in thread

* [PATCH v3 03/12] git-rebase--interactive: clarify arguments
  2018-02-11  0:09   ` [PATCH v3 00/12] " Johannes Schindelin
  2018-02-11  0:10     ` [PATCH v3 01/12] sequencer: avoid using errno clobbered by rollback_lock_file() Johannes Schindelin
  2018-02-11  0:10     ` [PATCH v3 02/12] sequencer: make rearrange_squash() a bit more obvious Johannes Schindelin
@ 2018-02-11  0:10     ` Johannes Schindelin
  2018-02-11  0:10     ` [PATCH v3 04/12] sequencer: introduce new commands to reset the revision Johannes Schindelin
                       ` (10 subsequent siblings)
  13 siblings, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-02-11  0:10 UTC (permalink / raw)
  To: git; +Cc: Stefan Beller, Junio C Hamano, Jacob Keller, Stefan Beller, Philip Oakley, Eric Sunshine, Phillip Wood

From: Stefan Beller <stefanbeller@gmail.com>

Up to now each command took a commit as its first argument and ignored
the rest of the line (usually the subject of the commit)

Now that we are about to introduce commands that take different
arguments, clarify each command by giving the argument list.

Signed-off-by: Stefan Beller <sbeller@google.com>
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 git-rebase--interactive.sh | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh
index d47bd29593a..fcedece1860 100644
--- a/git-rebase--interactive.sh
+++ b/git-rebase--interactive.sh
@@ -155,13 +155,13 @@ reschedule_last_action () {
 append_todo_help () {
 	gettext "
 Commands:
-p, pick = use commit
-r, reword = use commit, but edit the commit message
-e, edit = use commit, but stop for amending
-s, squash = use commit, but meld into previous commit
-f, fixup = like \"squash\", but discard this commit's log message
-x, exec = run command (the rest of the line) using shell
-d, drop = remove commit
+p, pick <commit> = use commit
+r, reword <commit> = use commit, but edit the commit message
+e, edit <commit> = use commit, but stop for amending
+s, squash <commit> = use commit, but meld into previous commit
+f, fixup <commit> = like \"squash\", but discard this commit's log message
+x, exec <commit> = run command (the rest of the line) using shell
+d, drop <commit> = remove commit
 
 These lines can be re-ordered; they are executed from top to bottom.
 " | git stripspace --comment-lines >>"$todo"
-- 
2.16.1.windows.1



^ permalink raw reply	[flat|nested] 276+ messages in thread

* [PATCH v3 04/12] sequencer: introduce new commands to reset the revision
  2018-02-11  0:09   ` [PATCH v3 00/12] " Johannes Schindelin
                       ` (2 preceding siblings ...)
  2018-02-11  0:10     ` [PATCH v3 03/12] git-rebase--interactive: clarify arguments Johannes Schindelin
@ 2018-02-11  0:10     ` Johannes Schindelin
  2018-02-12 19:26       ` Eric Sunshine
  2018-02-11  0:10     ` [PATCH v3 05/12] sequencer: introduce the `merge` command Johannes Schindelin
                       ` (9 subsequent siblings)
  13 siblings, 1 reply; 276+ messages in thread
From: Johannes Schindelin @ 2018-02-11  0:10 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Jacob Keller, Stefan Beller, Philip Oakley, Eric Sunshine, Phillip Wood

In the upcoming commits, we will teach the sequencer to recreate merges.
This will be done in a very different way from the unfortunate design of
`git rebase --preserve-merges` (which does not allow for reordering
commits, or changing the branch topology).

The main idea is to introduce new todo list commands, to support
labeling the current revision with a given name, resetting the current
revision to a previous state, and  merging labeled revisions.

This idea was developed in Git for Windows' Git garden shears (that are
used to maintain the "thicket of branches" on top of upstream Git), and
this patch is part of the effort to make it available to a wider
audience, as well as to make the entire process more robust (by
implementing it in a safe and portable language rather than a Unix shell
script).

This commit implements the commands to label, and to reset to, given
revisions. The syntax is:

	label <name>
	reset <name>

Internally, the `label <name>` command creates the ref
`refs/rewritten/<name>`. This makes it possible to work with the labeled
revisions interactively, or in a scripted fashion (e.g. via the todo
list command `exec`).

These temporary refs are removed upon sequencer_remove_state(), so that
even a `git rebase --abort` cleans them up.

We disallow '#' as label because that character will be used as separator
in the upcoming `merge` command.

Later in this patch series, we will mark the `refs/rewritten/` refs as
worktree-local, to allow for interactive rebases to be run in parallel in
worktrees linked to the same repository.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 git-rebase--interactive.sh |   2 +
 sequencer.c                | 190 +++++++++++++++++++++++++++++++++++++++++++--
 2 files changed, 186 insertions(+), 6 deletions(-)

diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh
index fcedece1860..7e5281e74aa 100644
--- a/git-rebase--interactive.sh
+++ b/git-rebase--interactive.sh
@@ -162,6 +162,8 @@ s, squash <commit> = use commit, but meld into previous commit
 f, fixup <commit> = like \"squash\", but discard this commit's log message
 x, exec <commit> = run command (the rest of the line) using shell
 d, drop <commit> = remove commit
+l, label <label> = label current HEAD with a name
+t, reset <label> = reset HEAD to a label
 
 These lines can be re-ordered; they are executed from top to bottom.
 " | git stripspace --comment-lines >>"$todo"
diff --git a/sequencer.c b/sequencer.c
index 764ad43388f..8638086f667 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -21,6 +21,8 @@
 #include "log-tree.h"
 #include "wt-status.h"
 #include "hashmap.h"
+#include "unpack-trees.h"
+#include "worktree.h"
 
 #define GIT_REFLOG_ACTION "GIT_REFLOG_ACTION"
 
@@ -116,6 +118,13 @@ static GIT_PATH_FUNC(rebase_path_stopped_sha, "rebase-merge/stopped-sha")
 static GIT_PATH_FUNC(rebase_path_rewritten_list, "rebase-merge/rewritten-list")
 static GIT_PATH_FUNC(rebase_path_rewritten_pending,
 	"rebase-merge/rewritten-pending")
+
+/*
+ * The path of the file listing refs that need to be deleted after the rebase
+ * finishes. This is used by the `label` command to record the need for cleanup.
+ */
+static GIT_PATH_FUNC(rebase_path_refs_to_delete, "rebase-merge/refs-to-delete")
+
 /*
  * The following files are written by git-rebase just after parsing the
  * command-line (and are only consumed, not modified, by the sequencer).
@@ -195,18 +204,33 @@ static const char *gpg_sign_opt_quoted(struct replay_opts *opts)
 
 int sequencer_remove_state(struct replay_opts *opts)
 {
-	struct strbuf dir = STRBUF_INIT;
+	struct strbuf buf = STRBUF_INIT;
 	int i;
 
+	if (strbuf_read_file(&buf, rebase_path_refs_to_delete(), 0) > 0) {
+		char *p = buf.buf;
+		while (*p) {
+			char *eol = strchr(p, '\n');
+			if (eol)
+				*eol = '\0';
+			if (delete_ref("(rebase -i) cleanup", p, NULL, 0) < 0)
+				warning(_("could not delete '%s'"), p);
+			if (!eol)
+				break;
+			p = eol + 1;
+		}
+	}
+
 	free(opts->gpg_sign);
 	free(opts->strategy);
 	for (i = 0; i < opts->xopts_nr; i++)
 		free(opts->xopts[i]);
 	free(opts->xopts);
 
-	strbuf_addstr(&dir, get_dir(opts));
-	remove_dir_recursively(&dir, 0);
-	strbuf_release(&dir);
+	strbuf_reset(&buf);
+	strbuf_addstr(&buf, get_dir(opts));
+	remove_dir_recursively(&buf, 0);
+	strbuf_release(&buf);
 
 	return 0;
 }
@@ -769,6 +793,8 @@ enum todo_command {
 	TODO_SQUASH,
 	/* commands that do something else than handling a single commit */
 	TODO_EXEC,
+	TODO_LABEL,
+	TODO_RESET,
 	/* commands that do nothing but are counted for reporting progress */
 	TODO_NOOP,
 	TODO_DROP,
@@ -787,6 +813,8 @@ static struct {
 	{ 'f', "fixup" },
 	{ 's', "squash" },
 	{ 'x', "exec" },
+	{ 'l', "label" },
+	{ 't', "reset" },
 	{ 0,   "noop" },
 	{ 'd', "drop" },
 	{ 0,   NULL }
@@ -1281,7 +1309,8 @@ static int parse_insn_line(struct todo_item *item, const char *bol, char *eol)
 		return error(_("missing arguments for %s"),
 			     command_to_string(item->command));
 
-	if (item->command == TODO_EXEC) {
+	if (item->command == TODO_EXEC || item->command == TODO_LABEL ||
+	    item->command == TODO_RESET) {
 		item->commit = NULL;
 		item->arg = bol;
 		item->arg_len = (int)(eol - bol);
@@ -1922,6 +1951,151 @@ static int do_exec(const char *command_line)
 	return status;
 }
 
+static int safe_append(const char *filename, const char *fmt, ...)
+{
+	va_list ap;
+	struct lock_file lock = LOCK_INIT;
+	int fd = hold_lock_file_for_update(&lock, filename,
+					   LOCK_REPORT_ON_ERROR);
+	struct strbuf buf = STRBUF_INIT;
+
+	if (fd < 0)
+		return -1;
+
+	if (strbuf_read_file(&buf, filename, 0) < 0 && errno != ENOENT)
+		return error_errno(_("could not read '%s'"), filename);
+	strbuf_complete(&buf, '\n');
+	va_start(ap, fmt);
+	strbuf_vaddf(&buf, fmt, ap);
+	va_end(ap);
+
+	if (write_in_full(fd, buf.buf, buf.len) < 0) {
+		error_errno(_("could not write to '%s'"), filename);
+		rollback_lock_file(&lock);
+		return -1;
+	}
+	if (commit_lock_file(&lock) < 0) {
+		rollback_lock_file(&lock);
+		return error(_("failed to finalize '%s'"), filename);
+	}
+
+	return 0;
+}
+
+static int do_label(const char *name, int len)
+{
+	struct ref_store *refs = get_main_ref_store();
+	struct ref_transaction *transaction;
+	struct strbuf ref_name = STRBUF_INIT, err = STRBUF_INIT;
+	struct strbuf msg = STRBUF_INIT;
+	int ret = 0;
+	struct object_id head_oid;
+
+	if (len == 1 && *name == '#')
+		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);
+
+	transaction = ref_store_transaction_begin(refs, &err);
+	if (!transaction) {
+		error("%s", err.buf);
+		ret = -1;
+	} else if (get_oid("HEAD", &head_oid)) {
+		error(_("could not read HEAD"));
+		ret = -1;
+	} else if (ref_transaction_update(transaction, ref_name.buf, &head_oid,
+					  NULL, 0, msg.buf, &err) < 0 ||
+		   ref_transaction_commit(transaction, &err)) {
+		error("%s", err.buf);
+		ret = -1;
+	}
+	ref_transaction_free(transaction);
+	strbuf_release(&err);
+	strbuf_release(&msg);
+
+	if (!ret)
+		ret = safe_append(rebase_path_refs_to_delete(),
+				  "%s\n", ref_name.buf);
+	strbuf_release(&ref_name);
+
+	return ret;
+}
+
+static int do_reset(const char *name, int len, struct replay_opts *opts)
+{
+	struct strbuf ref_name = STRBUF_INIT;
+	struct object_id oid;
+	struct lock_file lock = LOCK_INIT;
+	struct tree_desc desc;
+	struct tree *tree;
+	struct unpack_trees_options unpack_tree_opts;
+	int ret = 0, i;
+
+	if (hold_locked_index(&lock, LOCK_REPORT_ON_ERROR) < 0)
+		return -1;
+
+	/* Determine the length of the label */
+	for (i = 0; i < len; i++)
+		if (isspace(name[i]))
+			len = i;
+
+	strbuf_addf(&ref_name, "refs/rewritten/%.*s", len, name);
+	if (get_oid(ref_name.buf, &oid) &&
+	    get_oid(ref_name.buf + strlen("refs/rewritten/"), &oid)) {
+		error(_("could not read '%s'"), ref_name.buf);
+		rollback_lock_file(&lock);
+		strbuf_release(&ref_name);
+		return -1;
+	}
+
+	memset(&unpack_tree_opts, 0, sizeof(unpack_tree_opts));
+	unpack_tree_opts.head_idx = 1;
+	unpack_tree_opts.src_index = &the_index;
+	unpack_tree_opts.dst_index = &the_index;
+	unpack_tree_opts.fn = oneway_merge;
+	unpack_tree_opts.merge = 1;
+	unpack_tree_opts.update = 1;
+	unpack_tree_opts.reset = 1;
+
+	if (read_cache_unmerged())
+		return error_resolve_conflict(_(action_name(opts)));
+
+	if (!fill_tree_descriptor(&desc, &oid)) {
+		error(_("failed to find tree of %s"), oid_to_hex(&oid));
+		rollback_lock_file(&lock);
+		free((void *)desc.buffer);
+		strbuf_release(&ref_name);
+		return -1;
+	}
+
+	if (unpack_trees(1, &desc, &unpack_tree_opts)) {
+		rollback_lock_file(&lock);
+		free((void *)desc.buffer);
+		strbuf_release(&ref_name);
+		return -1;
+	}
+
+	tree = parse_tree_indirect(&oid);
+	prime_cache_tree(&the_index, tree);
+
+	if (write_locked_index(&the_index, &lock, COMMIT_LOCK) < 0)
+		ret = error(_("could not write index"));
+	free((void *)desc.buffer);
+
+	if (!ret) {
+		struct strbuf msg = STRBUF_INIT;
+
+		strbuf_addf(&msg, "(rebase -i) reset '%.*s'", len, name);
+		ret = update_ref(msg.buf, "HEAD", &oid, NULL, 0,
+				 UPDATE_REFS_MSG_ON_ERR);
+		strbuf_release(&msg);
+	}
+
+	strbuf_release(&ref_name);
+	return ret;
+}
+
 static int is_final_fixup(struct todo_list *todo_list)
 {
 	int i = todo_list->current;
@@ -2105,7 +2279,11 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts)
 				/* `current` will be incremented below */
 				todo_list->current = -1;
 			}
-		} else if (!is_noop(item->command))
+		} else if (item->command == TODO_LABEL)
+			res = do_label(item->arg, item->arg_len);
+		else if (item->command == TODO_RESET)
+			res = do_reset(item->arg, item->arg_len, opts);
+		else if (!is_noop(item->command))
 			return error(_("unknown command %d"), item->command);
 
 		todo_list->current++;
-- 
2.16.1.windows.1



^ permalink raw reply	[flat|nested] 276+ messages in thread

* [PATCH v3 05/12] sequencer: introduce the `merge` command
  2018-02-11  0:09   ` [PATCH v3 00/12] " Johannes Schindelin
                       ` (3 preceding siblings ...)
  2018-02-11  0:10     ` [PATCH v3 04/12] sequencer: introduce new commands to reset the revision Johannes Schindelin
@ 2018-02-11  0:10     ` Johannes Schindelin
  2018-02-12  8:48       ` Eric Sunshine
  2018-02-11  0:10     ` [PATCH v3 06/12] sequencer: fast-forward merge commits, if possible Johannes Schindelin
                       ` (8 subsequent siblings)
  13 siblings, 1 reply; 276+ messages in thread
From: Johannes Schindelin @ 2018-02-11  0:10 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Jacob Keller, Stefan Beller, Philip Oakley, Eric Sunshine, Phillip Wood

This patch is part of the effort to reimplement `--preserve-merges` with
a substantially improved design, a design that has been developed in the
Git for Windows project to maintain the dozens of Windows-specific patch
series on top of upstream Git.

The previous patch implemented the `label` and `reset` commands to label
commits and to reset to a labeled commits. This patch adds the `merge`
command, with the following syntax:

	merge [-C <commit>] <rev> # <oneline>

The <commit> parameter in this instance is the *original* merge commit,
whose author and message will be used for the merge commit that is about
to be created.

The <rev> parameter refers to the (possibly rewritten) revision to
merge. Let's see an example of a todo list:

	label onto

	# Branch abc
	reset onto
	pick deadbeef Hello, world!
	label abc

	reset onto
	pick cafecafe And now for something completely different
	merge -C baaabaaa abc # Merge the branch 'abc' into master

To edit the merge commit's message (a "reword" for merges, if you will),
use `-c` (lower-case) instead of `-C`; this convention was borrowed from
`git commit` that also supports `-c` and `-C` with similar meanings.

To create *new* merges, i.e. without copying the commit message from an
existing commit, simply omit the `-C <commit>` parameter (which will
open an editor for the merge message):

	merge abc

This comes in handy when splitting a branch into two or more branches.

Note: this patch only adds support for recursive merges, to keep things
simple. Support for octopus merges will be added later in a separate
patch series, support for merges using strategies other than the
recursive merge is left for the future.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 git-rebase--interactive.sh |   4 ++
 sequencer.c                | 158 +++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 162 insertions(+)

diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh
index 7e5281e74aa..9d9d91f25e3 100644
--- a/git-rebase--interactive.sh
+++ b/git-rebase--interactive.sh
@@ -164,6 +164,10 @@ x, exec <commit> = run command (the rest of the line) using shell
 d, drop <commit> = remove commit
 l, label <label> = label current HEAD with a name
 t, reset <label> = reset HEAD to a label
+m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
+.       create a merge commit using the original merge commit's
+.       message (or the oneline, if no original merge commit was
+.       specified). Use -c <commit> to reword the commit message.
 
 These lines can be re-ordered; they are executed from top to bottom.
 " | git stripspace --comment-lines >>"$todo"
diff --git a/sequencer.c b/sequencer.c
index 8638086f667..e577c213494 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -795,6 +795,8 @@ enum todo_command {
 	TODO_EXEC,
 	TODO_LABEL,
 	TODO_RESET,
+	TODO_MERGE,
+	TODO_MERGE_AND_EDIT,
 	/* commands that do nothing but are counted for reporting progress */
 	TODO_NOOP,
 	TODO_DROP,
@@ -815,6 +817,8 @@ static struct {
 	{ 'x', "exec" },
 	{ 'l', "label" },
 	{ 't', "reset" },
+	{ 'm', "merge" },
+	{ 0, "merge" }, /* MERGE_AND_EDIT */
 	{ 0,   "noop" },
 	{ 'd', "drop" },
 	{ 0,   NULL }
@@ -1317,6 +1321,21 @@ static int parse_insn_line(struct todo_item *item, const char *bol, char *eol)
 		return 0;
 	}
 
+	if (item->command == TODO_MERGE) {
+		if (skip_prefix(bol, "-C", &bol))
+			bol += strspn(bol, " \t");
+		else if (skip_prefix(bol, "-c", &bol)) {
+			bol += strspn(bol, " \t");
+			item->command = TODO_MERGE_AND_EDIT;
+		} else {
+			item->command = TODO_MERGE_AND_EDIT;
+			item->commit = NULL;
+			item->arg = bol;
+			item->arg_len = (int)(eol - bol);
+			return 0;
+		}
+	}
+
 	end_of_object_name = (char *) bol + strcspn(bol, " \t\n");
 	saved = *end_of_object_name;
 	*end_of_object_name = '\0';
@@ -2096,6 +2115,134 @@ static int do_reset(const char *name, int len, struct replay_opts *opts)
 	return ret;
 }
 
+static int do_merge(struct commit *commit, const char *arg, int arg_len,
+		    int run_commit_flags, struct replay_opts *opts)
+{
+	int merge_arg_len;
+	struct strbuf ref_name = STRBUF_INIT;
+	struct commit *head_commit, *merge_commit, *i;
+	struct commit_list *common, *j, *reversed = NULL;
+	struct merge_options o;
+	int ret;
+	static struct lock_file lock;
+
+	for (merge_arg_len = 0; merge_arg_len < arg_len; merge_arg_len++)
+		if (isspace(arg[merge_arg_len]))
+			break;
+
+	if (hold_locked_index(&lock, LOCK_REPORT_ON_ERROR) < 0)
+		return -1;
+
+	head_commit = lookup_commit_reference_by_name("HEAD");
+	if (!head_commit) {
+		rollback_lock_file(&lock);
+		return error(_("cannot merge without a current revision"));
+	}
+
+	if (commit) {
+		const char *message = get_commit_buffer(commit, NULL);
+		const char *body;
+		int len;
+
+		if (!message) {
+			rollback_lock_file(&lock);
+			return error(_("could not get commit message of '%s'"),
+				     oid_to_hex(&commit->object.oid));
+		}
+		write_author_script(message);
+		find_commit_subject(message, &body);
+		len = strlen(body);
+		if (write_message(body, len, git_path_merge_msg(), 0) < 0) {
+			error_errno(_("could not write '%s'"),
+				    git_path_merge_msg());
+			unuse_commit_buffer(commit, message);
+			rollback_lock_file(&lock);
+			return -1;
+		}
+		unuse_commit_buffer(commit, message);
+	} else {
+		const char *p = arg + merge_arg_len;
+		struct strbuf buf = STRBUF_INIT;
+		int len;
+
+		strbuf_addf(&buf, "author %s", git_author_info(0));
+		write_author_script(buf.buf);
+		strbuf_reset(&buf);
+
+		p += strspn(p, " \t");
+		if (*p == '#' && isspace(p[1]))
+			p += 1 + strspn(p + 1, " \t");
+		if (*p)
+			len = strlen(p);
+		else {
+			strbuf_addf(&buf, "Merge branch '%.*s'",
+				    merge_arg_len, arg);
+			p = buf.buf;
+			len = buf.len;
+		}
+
+		if (write_message(p, len, git_path_merge_msg(), 0) < 0) {
+			error_errno(_("could not write '%s'"),
+				    git_path_merge_msg());
+			strbuf_release(&buf);
+			rollback_lock_file(&lock);
+			return -1;
+		}
+		strbuf_release(&buf);
+	}
+
+	strbuf_addf(&ref_name, "refs/rewritten/%.*s", merge_arg_len, arg);
+	merge_commit = lookup_commit_reference_by_name(ref_name.buf);
+	if (!merge_commit) {
+		/* fall back to non-rewritten ref or commit */
+		strbuf_splice(&ref_name, 0, strlen("refs/rewritten/"), "", 0);
+		merge_commit = lookup_commit_reference_by_name(ref_name.buf);
+	}
+	if (!merge_commit) {
+		error(_("could not resolve '%s'"), ref_name.buf);
+		strbuf_release(&ref_name);
+		rollback_lock_file(&lock);
+		return -1;
+	}
+	write_message(oid_to_hex(&merge_commit->object.oid), GIT_SHA1_HEXSZ,
+		      git_path_merge_head(), 0);
+	write_message("no-ff", 5, git_path_merge_mode(), 0);
+
+	common = get_merge_bases(head_commit, merge_commit);
+	for (j = common; j; j = j->next)
+		commit_list_insert(j->item, &reversed);
+	free_commit_list(common);
+
+	read_cache();
+	init_merge_options(&o);
+	o.branch1 = "HEAD";
+	o.branch2 = ref_name.buf;
+	o.buffer_output = 2;
+
+	ret = merge_recursive(&o, head_commit, merge_commit, reversed, &i);
+	if (ret <= 0)
+		fputs(o.obuf.buf, stdout);
+	strbuf_release(&o.obuf);
+	if (ret < 0) {
+		strbuf_release(&ref_name);
+		rollback_lock_file(&lock);
+		return error(_("conflicts while merging '%.*s'"),
+			     merge_arg_len, arg);
+	}
+
+	if (active_cache_changed &&
+	    write_locked_index(&the_index, &lock, COMMIT_LOCK)) {
+		strbuf_release(&ref_name);
+		return error(_("merge: Unable to write new index file"));
+	}
+	rollback_lock_file(&lock);
+
+	ret = run_git_commit(git_path_merge_msg(), opts, run_commit_flags);
+	strbuf_release(&ref_name);
+
+	return ret;
+}
+
 static int is_final_fixup(struct todo_list *todo_list)
 {
 	int i = todo_list->current;
@@ -2283,6 +2430,11 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts)
 			res = do_label(item->arg, item->arg_len);
 		else if (item->command == TODO_RESET)
 			res = do_reset(item->arg, item->arg_len, opts);
+		else if (item->command == TODO_MERGE ||
+			 item->command == TODO_MERGE_AND_EDIT)
+			res = do_merge(item->commit, item->arg, item->arg_len,
+				       item->command == TODO_MERGE_AND_EDIT ?
+				       EDIT_MSG | VERIFY_MSG : 0, opts);
 		else if (!is_noop(item->command))
 			return error(_("unknown command %d"), item->command);
 
@@ -2764,8 +2916,14 @@ int transform_todos(unsigned flags)
 					  short_commit_name(item->commit) :
 					  oid_to_hex(&item->commit->object.oid);
 
+			if (item->command == TODO_MERGE)
+				strbuf_addstr(&buf, " -C");
+			else if (item->command == TODO_MERGE_AND_EDIT)
+				strbuf_addstr(&buf, " -c");
+
 			strbuf_addf(&buf, " %s", oid);
 		}
+
 		/* add all the rest */
 		if (!item->arg_len)
 			strbuf_addch(&buf, '\n');
-- 
2.16.1.windows.1



^ permalink raw reply	[flat|nested] 276+ messages in thread

* [PATCH v3 06/12] sequencer: fast-forward merge commits, if possible
  2018-02-11  0:09   ` [PATCH v3 00/12] " Johannes Schindelin
                       ` (4 preceding siblings ...)
  2018-02-11  0:10     ` [PATCH v3 05/12] sequencer: introduce the `merge` command Johannes Schindelin
@ 2018-02-11  0:10     ` Johannes Schindelin
  2018-02-11  0:10     ` [PATCH v3 07/12] rebase-helper --make-script: introduce a flag to recreate merges Johannes Schindelin
                       ` (7 subsequent siblings)
  13 siblings, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-02-11  0:10 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Jacob Keller, Stefan Beller, Philip Oakley, Eric Sunshine, Phillip Wood

Just like with regular `pick` commands, if we are trying to recreate a
merge commit, we now test whether the parents of said commit match HEAD
and the commits to be merged, and fast-forward if possible.

This is not only faster, but also avoids unnecessary proliferation of
new objects.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 sequencer.c | 21 ++++++++++++++++++++-
 1 file changed, 20 insertions(+), 1 deletion(-)

diff --git a/sequencer.c b/sequencer.c
index e577c213494..27d582479d1 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -2123,7 +2123,7 @@ static int do_merge(struct commit *commit, const char *arg, int arg_len,
 	struct commit *head_commit, *merge_commit, *i;
 	struct commit_list *common, *j, *reversed = NULL;
 	struct merge_options o;
-	int ret;
+	int can_fast_forward, ret;
 	static struct lock_file lock;
 
 	for (merge_arg_len = 0; merge_arg_len < arg_len; merge_arg_len++)
@@ -2191,6 +2191,14 @@ static int do_merge(struct commit *commit, const char *arg, int arg_len,
 		strbuf_release(&buf);
 	}
 
+	/*
+	 * If HEAD is not identical to the parent of the original merge commit,
+	 * we cannot fast-forward.
+	 */
+	can_fast_forward = opts->allow_ff && commit && commit->parents &&
+		!oidcmp(&commit->parents->item->object.oid,
+			&head_commit->object.oid);
+
 	strbuf_addf(&ref_name, "refs/rewritten/%.*s", merge_arg_len, arg);
 	merge_commit = lookup_commit_reference_by_name(ref_name.buf);
 	if (!merge_commit) {
@@ -2204,6 +2212,17 @@ static int do_merge(struct commit *commit, const char *arg, int arg_len,
 		rollback_lock_file(&lock);
 		return -1;
 	}
+
+	if (can_fast_forward && commit->parents->next &&
+	    !commit->parents->next->next &&
+	    !oidcmp(&commit->parents->next->item->object.oid,
+		    &merge_commit->object.oid)) {
+		strbuf_release(&ref_name);
+		rollback_lock_file(&lock);
+		return fast_forward_to(&commit->object.oid,
+				       &head_commit->object.oid, 0, opts);
+	}
+
 	write_message(oid_to_hex(&merge_commit->object.oid), GIT_SHA1_HEXSZ,
 		      git_path_merge_head(), 0);
 	write_message("no-ff", 5, git_path_merge_mode(), 0);
-- 
2.16.1.windows.1



^ permalink raw reply	[flat|nested] 276+ messages in thread

* [PATCH v3 07/12] rebase-helper --make-script: introduce a flag to recreate merges
  2018-02-11  0:09   ` [PATCH v3 00/12] " Johannes Schindelin
                       ` (5 preceding siblings ...)
  2018-02-11  0:10     ` [PATCH v3 06/12] sequencer: fast-forward merge commits, if possible Johannes Schindelin
@ 2018-02-11  0:10     ` Johannes Schindelin
  2018-02-11  0:10     ` [PATCH v3 08/12] rebase: introduce the --recreate-merges option Johannes Schindelin
                       ` (6 subsequent siblings)
  13 siblings, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-02-11  0:10 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Jacob Keller, Stefan Beller, Philip Oakley, Eric Sunshine, Phillip Wood

The sequencer just learned new commands intended to recreate branch
structure (similar in spirit to --preserve-merges, but with a
substantially less-broken design).

Let's allow the rebase--helper to generate todo lists making use of
these commands, triggered by the new --recreate-merges option. For a
commit topology like this (where the HEAD points to C):

	- A - B - C
	    \   /
	      D

the generated todo list would look like this:

	# branch D
	pick 0123 A
	label branch-point
	pick 1234 D
	label D

	reset branch-point
	pick 2345 B
	merge -C 3456 D # C

To keep things simple, we first only implement support for merge commits
with exactly two parents, leaving support for octopus merges to a later
patch in this patch series.

As a special, hard-coded label, all merge-recreating todo lists start with
the command `label onto` so that we can later always refer to the revision
onto which everything is rebased.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 builtin/rebase--helper.c |   4 +-
 sequencer.c              | 349 ++++++++++++++++++++++++++++++++++++++++++++++-
 sequencer.h              |   1 +
 3 files changed, 351 insertions(+), 3 deletions(-)

diff --git a/builtin/rebase--helper.c b/builtin/rebase--helper.c
index 7daee544b7b..a34ab5c0655 100644
--- a/builtin/rebase--helper.c
+++ b/builtin/rebase--helper.c
@@ -12,7 +12,7 @@ static const char * const builtin_rebase_helper_usage[] = {
 int cmd_rebase__helper(int argc, const char **argv, const char *prefix)
 {
 	struct replay_opts opts = REPLAY_OPTS_INIT;
-	unsigned flags = 0, keep_empty = 0;
+	unsigned flags = 0, keep_empty = 0, recreate_merges = 0;
 	int abbreviate_commands = 0;
 	enum {
 		CONTINUE = 1, ABORT, MAKE_SCRIPT, SHORTEN_OIDS, EXPAND_OIDS,
@@ -22,6 +22,7 @@ int cmd_rebase__helper(int argc, const char **argv, const char *prefix)
 	struct option options[] = {
 		OPT_BOOL(0, "ff", &opts.allow_ff, N_("allow fast-forward")),
 		OPT_BOOL(0, "keep-empty", &keep_empty, N_("keep empty commits")),
+		OPT_BOOL(0, "recreate-merges", &recreate_merges, N_("recreate merge commits")),
 		OPT_CMDMODE(0, "continue", &command, N_("continue rebase"),
 				CONTINUE),
 		OPT_CMDMODE(0, "abort", &command, N_("abort rebase"),
@@ -55,6 +56,7 @@ int cmd_rebase__helper(int argc, const char **argv, const char *prefix)
 
 	flags |= keep_empty ? TODO_LIST_KEEP_EMPTY : 0;
 	flags |= abbreviate_commands ? TODO_LIST_ABBREVIATE_CMDS : 0;
+	flags |= recreate_merges ? TODO_LIST_RECREATE_MERGES : 0;
 	flags |= command == SHORTEN_OIDS ? TODO_LIST_SHORTEN_IDS : 0;
 
 	if (command == CONTINUE && argc == 1)
diff --git a/sequencer.c b/sequencer.c
index 27d582479d1..7cd091a9fd6 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -23,6 +23,8 @@
 #include "hashmap.h"
 #include "unpack-trees.h"
 #include "worktree.h"
+#include "oidmap.h"
+#include "oidset.h"
 
 #define GIT_REFLOG_ACTION "GIT_REFLOG_ACTION"
 
@@ -2808,6 +2810,341 @@ void append_signoff(struct strbuf *msgbuf, int ignore_footer, unsigned flag)
 	strbuf_release(&sob);
 }
 
+struct labels_entry {
+	struct hashmap_entry entry;
+	char label[FLEX_ARRAY];
+};
+
+static int labels_cmp(const void *fndata, const struct labels_entry *a,
+		      const struct labels_entry *b, const void *key)
+{
+	return key ? strcmp(a->label, key) : strcmp(a->label, b->label);
+}
+
+struct string_entry {
+	struct oidmap_entry entry;
+	char string[FLEX_ARRAY];
+};
+
+struct label_state {
+	struct oidmap commit2label;
+	struct hashmap labels;
+	struct strbuf buf;
+};
+
+static const char *label_oid(struct object_id *oid, const char *label,
+			     struct label_state *state)
+{
+	struct labels_entry *labels_entry;
+	struct string_entry *string_entry;
+	struct object_id dummy;
+	size_t len;
+	int i;
+
+	string_entry = oidmap_get(&state->commit2label, oid);
+	if (string_entry)
+		return string_entry->string;
+
+	/*
+	 * For "uninteresting" commits, i.e. commits that are not to be
+	 * rebased, and which can therefore not be labeled, we use a unique
+	 * abbreviation of the commit name. This is slightly more complicated
+	 * than calling find_unique_abbrev() because we also need to make
+	 * sure that the abbreviation does not conflict with any other
+	 * label.
+	 *
+	 * We disallow "interesting" commits to be labeled by a string that
+	 * is a valid full-length hash, to ensure that we always can find an
+	 * abbreviation for any uninteresting commit's names that does not
+	 * clash with any other label.
+	 */
+	if (!label) {
+		char *p;
+
+		strbuf_reset(&state->buf);
+		strbuf_grow(&state->buf, GIT_SHA1_HEXSZ);
+		label = p = state->buf.buf;
+
+		find_unique_abbrev_r(p, oid->hash, default_abbrev);
+
+		/*
+		 * We may need to extend the abbreviated hash so that there is
+		 * no conflicting label.
+		 */
+		if (hashmap_get_from_hash(&state->labels, strihash(p), p)) {
+			size_t i = strlen(p) + 1;
+
+			oid_to_hex_r(p, oid);
+			for (; i < GIT_SHA1_HEXSZ; i++) {
+				char save = p[i];
+				p[i] = '\0';
+				if (!hashmap_get_from_hash(&state->labels,
+							   strihash(p), p))
+					break;
+				p[i] = save;
+			}
+		}
+	} else if (((len = strlen(label)) == GIT_SHA1_RAWSZ &&
+		    !get_oid_hex(label, &dummy)) ||
+		   (len == 1 && *label == '#') ||
+		   hashmap_get_from_hash(&state->labels,
+					 strihash(label), label)) {
+		/*
+		 * If the label already exists, or if the label is a valid full
+		 * OID, or the label is a '#' (which we use as a separator
+		 * between merge heads and oneline), we append a dash and a
+		 * number to make it unique.
+		 */
+		struct strbuf *buf = &state->buf;
+
+		strbuf_reset(buf);
+		strbuf_add(buf, label, len);
+
+		for (i = 2; ; i++) {
+			strbuf_setlen(buf, len);
+			strbuf_addf(buf, "-%d", i);
+			if (!hashmap_get_from_hash(&state->labels,
+						   strihash(buf->buf),
+						   buf->buf))
+				break;
+		}
+
+		label = buf->buf;
+	}
+
+	FLEX_ALLOC_STR(labels_entry, label, label);
+	hashmap_entry_init(labels_entry, strihash(label));
+	hashmap_add(&state->labels, labels_entry);
+
+	FLEX_ALLOC_STR(string_entry, string, label);
+	oidcpy(&string_entry->entry.oid, oid);
+	oidmap_put(&state->commit2label, string_entry);
+
+	return string_entry->string;
+}
+
+static int make_script_with_merges(struct pretty_print_context *pp,
+				   struct rev_info *revs, FILE *out,
+				   unsigned flags)
+{
+	int keep_empty = flags & TODO_LIST_KEEP_EMPTY;
+	struct strbuf buf = STRBUF_INIT, oneline = STRBUF_INIT;
+	struct strbuf label = STRBUF_INIT;
+	struct commit_list *commits = NULL, **tail = &commits, *iter;
+	struct commit_list *tips = NULL, **tips_tail = &tips;
+	struct commit *commit;
+	struct oidmap commit2todo = OIDMAP_INIT;
+	struct string_entry *entry;
+	struct oidset interesting = OIDSET_INIT, child_seen = OIDSET_INIT,
+		shown = OIDSET_INIT;
+	struct label_state state = { OIDMAP_INIT, { NULL }, STRBUF_INIT };
+
+	int abbr = flags & TODO_LIST_ABBREVIATE_CMDS;
+	const char *cmd_pick = abbr ? "p" : "pick",
+		*cmd_label = abbr ? "l" : "label",
+		*cmd_reset = abbr ? "t" : "reset",
+		*cmd_merge = abbr ? "m" : "merge";
+
+	oidmap_init(&commit2todo, 0);
+	oidmap_init(&state.commit2label, 0);
+	hashmap_init(&state.labels, (hashmap_cmp_fn) labels_cmp, NULL, 0);
+	strbuf_init(&state.buf, 32);
+
+	if (revs->cmdline.nr && (revs->cmdline.rev[0].flags & BOTTOM)) {
+		struct object_id *oid = &revs->cmdline.rev[0].item->oid;
+		FLEX_ALLOC_STR(entry, string, "onto");
+		oidcpy(&entry->entry.oid, oid);
+		oidmap_put(&state.commit2label, entry);
+	}
+
+	/*
+	 * First phase:
+	 * - get onelines for all commits
+	 * - gather all branch tips (i.e. 2nd or later parents of merges)
+	 * - label all branch tips
+	 */
+	while ((commit = get_revision(revs))) {
+		struct commit_list *to_merge;
+		int is_octopus;
+		const char *p1, *p2;
+		struct object_id *oid;
+
+		tail = &commit_list_insert(commit, tail)->next;
+		oidset_insert(&interesting, &commit->object.oid);
+
+		if ((commit->object.flags & PATCHSAME))
+			continue;
+
+		strbuf_reset(&oneline);
+		pretty_print_commit(pp, commit, &oneline);
+
+		to_merge = commit->parents ? commit->parents->next : NULL;
+		if (!to_merge) {
+			/* non-merge commit: easy case */
+			strbuf_reset(&buf);
+			if (!keep_empty && is_original_commit_empty(commit))
+				strbuf_addf(&buf, "%c ", comment_line_char);
+			strbuf_addf(&buf, "%s %s %s", cmd_pick,
+				    oid_to_hex(&commit->object.oid),
+				    oneline.buf);
+
+			FLEX_ALLOC_STR(entry, string, buf.buf);
+			oidcpy(&entry->entry.oid, &commit->object.oid);
+			oidmap_put(&commit2todo, entry);
+
+			continue;
+		}
+
+		is_octopus = to_merge && to_merge->next;
+
+		if (is_octopus)
+			BUG("Octopus merges not yet supported");
+
+		/* Create a label */
+		strbuf_reset(&label);
+		if (skip_prefix(oneline.buf, "Merge ", &p1) &&
+		    (p1 = strchr(p1, '\'')) &&
+		    (p2 = strchr(++p1, '\'')))
+			strbuf_add(&label, p1, p2 - p1);
+		else if (skip_prefix(oneline.buf, "Merge pull request ",
+				     &p1) &&
+			 (p1 = strstr(p1, " from ")))
+			strbuf_addstr(&label, p1 + strlen(" from "));
+		else
+			strbuf_addbuf(&label, &oneline);
+
+		for (p1 = label.buf; *p1; p1++)
+			if (isspace(*p1))
+				*(char *)p1 = '-';
+
+		strbuf_reset(&buf);
+		strbuf_addf(&buf, "%s -C %s",
+			    cmd_merge, oid_to_hex(&commit->object.oid));
+
+		/* label the tip of merged branch */
+		oid = &to_merge->item->object.oid;
+		strbuf_addch(&buf, ' ');
+
+		if (!oidset_contains(&interesting, oid))
+			strbuf_addstr(&buf, label_oid(oid, NULL, &state));
+		else {
+			tips_tail = &commit_list_insert(to_merge->item,
+							tips_tail)->next;
+
+			strbuf_addstr(&buf, label_oid(oid, label.buf, &state));
+		}
+		strbuf_addf(&buf, " # %s", oneline.buf);
+
+		FLEX_ALLOC_STR(entry, string, buf.buf);
+		oidcpy(&entry->entry.oid, &commit->object.oid);
+		oidmap_put(&commit2todo, entry);
+	}
+
+	/*
+	 * Second phase:
+	 * - label branch points
+	 * - add HEAD to the branch tips
+	 */
+	for (iter = commits; iter; iter = iter->next) {
+		struct commit_list *parent = iter->item->parents;
+		for (; parent; parent = parent->next) {
+			struct object_id *oid = &parent->item->object.oid;
+			if (!oidset_contains(&interesting, oid))
+				continue;
+			if (!oidset_contains(&child_seen, oid))
+				oidset_insert(&child_seen, oid);
+			else
+				label_oid(oid, "branch-point", &state);
+		}
+
+		/* Add HEAD as implict "tip of branch" */
+		if (!iter->next)
+			tips_tail = &commit_list_insert(iter->item,
+							tips_tail)->next;
+	}
+
+	/*
+	 * Third phase: output the todo list. This is a bit tricky, as we
+	 * want to avoid jumping back and forth between revisions. To
+	 * accomplish that goal, we walk backwards from the branch tips,
+	 * gathering commits not yet shown, reversing the list on the fly,
+	 * then outputting that list (labeling revisions as needed).
+	 */
+	fprintf(out, "%s onto\n", cmd_label);
+	for (iter = tips; iter; iter = iter->next) {
+		struct commit_list *list = NULL, *iter2;
+
+		commit = iter->item;
+		if (oidset_contains(&shown, &commit->object.oid))
+			continue;
+		entry = oidmap_get(&state.commit2label, &commit->object.oid);
+
+		if (entry)
+			fprintf(out, "\n# Branch %s\n", entry->string);
+		else
+			fprintf(out, "\n");
+
+		while (oidset_contains(&interesting, &commit->object.oid) &&
+		       !oidset_contains(&shown, &commit->object.oid)) {
+			commit_list_insert(commit, &list);
+			if (!commit->parents) {
+				commit = NULL;
+				break;
+			}
+			commit = commit->parents->item;
+		}
+
+		if (!commit)
+			fprintf(out, "%s onto\n", cmd_reset);
+		else {
+			const char *to = NULL;
+
+			entry = oidmap_get(&state.commit2label,
+					   &commit->object.oid);
+			if (entry)
+				to = entry->string;
+
+			if (!to || !strcmp(to, "onto"))
+				fprintf(out, "%s onto\n", cmd_reset);
+			else {
+				strbuf_reset(&oneline);
+				pretty_print_commit(pp, commit, &oneline);
+				fprintf(out, "%s %s # %s\n",
+					cmd_reset, to, oneline.buf);
+			}
+		}
+
+		for (iter2 = list; iter2; iter2 = iter2->next) {
+			struct object_id *oid = &iter2->item->object.oid;
+			entry = oidmap_get(&commit2todo, oid);
+			/* only show if not already upstream */
+			if (entry)
+				fprintf(out, "%s\n", entry->string);
+			entry = oidmap_get(&state.commit2label, oid);
+			if (entry)
+				fprintf(out, "%s %s\n",
+					cmd_label, entry->string);
+			oidset_insert(&shown, oid);
+		}
+
+		free_commit_list(list);
+	}
+
+	free_commit_list(commits);
+	free_commit_list(tips);
+
+	strbuf_release(&label);
+	strbuf_release(&oneline);
+	strbuf_release(&buf);
+
+	oidmap_free(&commit2todo, 1);
+	oidmap_free(&state.commit2label, 1);
+	hashmap_free(&state.labels, 1);
+	strbuf_release(&state.buf);
+
+	return 0;
+}
+
 int sequencer_make_script(FILE *out, int argc, const char **argv,
 			  unsigned flags)
 {
@@ -2818,11 +3155,16 @@ int sequencer_make_script(FILE *out, int argc, const char **argv,
 	struct commit *commit;
 	int keep_empty = flags & TODO_LIST_KEEP_EMPTY;
 	const char *insn = flags & TODO_LIST_ABBREVIATE_CMDS ? "p" : "pick";
+	int recreate_merges = flags & TODO_LIST_RECREATE_MERGES;
 
 	init_revisions(&revs, NULL);
 	revs.verbose_header = 1;
-	revs.max_parents = 1;
-	revs.cherry_pick = 1;
+	if (recreate_merges)
+		revs.cherry_mark = 1;
+	else {
+		revs.max_parents = 1;
+		revs.cherry_pick = 1;
+	}
 	revs.limited = 1;
 	revs.reverse = 1;
 	revs.right_only = 1;
@@ -2846,6 +3188,9 @@ int sequencer_make_script(FILE *out, int argc, const char **argv,
 	if (prepare_revision_walk(&revs) < 0)
 		return error(_("make_script: error preparing revisions"));
 
+	if (recreate_merges)
+		return make_script_with_merges(&pp, &revs, out, flags);
+
 	while ((commit = get_revision(&revs))) {
 		strbuf_reset(&buf);
 		if (!keep_empty && is_original_commit_empty(commit))
diff --git a/sequencer.h b/sequencer.h
index 81f6d7d393f..11d1ac925ef 100644
--- a/sequencer.h
+++ b/sequencer.h
@@ -48,6 +48,7 @@ int sequencer_remove_state(struct replay_opts *opts);
 #define TODO_LIST_KEEP_EMPTY (1U << 0)
 #define TODO_LIST_SHORTEN_IDS (1U << 1)
 #define TODO_LIST_ABBREVIATE_CMDS (1U << 2)
+#define TODO_LIST_RECREATE_MERGES (1U << 3)
 int sequencer_make_script(FILE *out, int argc, const char **argv,
 			  unsigned flags);
 
-- 
2.16.1.windows.1



^ permalink raw reply	[flat|nested] 276+ messages in thread

* [PATCH v3 08/12] rebase: introduce the --recreate-merges option
  2018-02-11  0:09   ` [PATCH v3 00/12] " Johannes Schindelin
                       ` (6 preceding siblings ...)
  2018-02-11  0:10     ` [PATCH v3 07/12] rebase-helper --make-script: introduce a flag to recreate merges Johannes Schindelin
@ 2018-02-11  0:10     ` Johannes Schindelin
  2018-02-11  0:10     ` [PATCH v3 09/12] sequencer: make refs generated by the `label` command worktree-local Johannes Schindelin
                       ` (5 subsequent siblings)
  13 siblings, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-02-11  0:10 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Jacob Keller, Stefan Beller, Philip Oakley, Eric Sunshine, Phillip Wood

Once upon a time, this here developer thought: wouldn't it be nice if,
say, Git for Windows' patches on top of core Git could be represented as
a thicket of branches, and be rebased on top of core Git in order to
maintain a cherry-pick'able set of patch series?

The original attempt to answer this was: git rebase --preserve-merges.

However, that experiment was never intended as an interactive option,
and it only piggy-backed on git rebase --interactive because that
command's implementation looked already very, very familiar: it was
designed by the same person who designed --preserve-merges: yours truly.

Some time later, some other developer (I am looking at you, Andreas!
;-)) decided that it would be a good idea to allow --preserve-merges to
be combined with --interactive (with caveats!) and the Git maintainer
(well, the interim Git maintainer during Junio's absence, that is)
agreed, and that is when the glamor of the --preserve-merges design
started to fall apart rather quickly and unglamorously.

The reason? In --preserve-merges mode, the parents of a merge commit (or
for that matter, of *any* commit) were not stated explicitly, but were
*implied* by the commit name passed to the `pick` command.

This made it impossible, for example, to reorder commits. Not to mention
to flatten the branch topology or, deity forbid, to split topic branches
into two.

Alas, these shortcomings also prevented that mode (whose original
purpose was to serve Git for Windows' needs, with the additional hope
that it may be useful to others, too) from serving Git for Windows'
needs.

Five years later, when it became really untenable to have one unwieldy,
big hodge-podge patch series of partly related, partly unrelated patches
in Git for Windows that was rebased onto core Git's tags from time to
time (earning the undeserved wrath of the developer of the ill-fated
git-remote-hg series that first obsoleted Git for Windows' competing
approach, only to be abandoned without maintainer later) was really
untenable, the "Git garden shears" were born [*1*/*2*]: a script,
piggy-backing on top of the interactive rebase, that would first
determine the branch topology of the patches to be rebased, create a
pseudo todo list for further editing, transform the result into a real
todo list (making heavy use of the `exec` command to "implement" the
missing todo list commands) and finally recreate the patch series on
top of the new base commit.

That was in 2013. And it took about three weeks to come up with the
design and implement it as an out-of-tree script. Needless to say, the
implementation needed quite a few years to stabilize, all the while the
design itself proved itself sound.

With this patch, the goodness of the Git garden shears comes to `git
rebase -i` itself. Passing the `--recreate-merges` option will generate
a todo list that can be understood readily, and where it is obvious
how to reorder commits. New branches can be introduced by inserting
`label` commands and calling `merge <label>`. And once this mode will
have become stable and universally accepted, we can deprecate the design
mistake that was `--preserve-merges`.

Link *1*:
https://github.com/msysgit/msysgit/blob/master/share/msysGit/shears.sh
Link *2*:
https://github.com/git-for-windows/build-extra/blob/master/shears.sh

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 Documentation/git-rebase.txt           |   9 +-
 contrib/completion/git-completion.bash |   2 +-
 git-rebase--interactive.sh             |   1 +
 git-rebase.sh                          |   6 ++
 t/t3430-rebase-recreate-merges.sh      | 146 +++++++++++++++++++++++++++++++++
 5 files changed, 162 insertions(+), 2 deletions(-)
 create mode 100755 t/t3430-rebase-recreate-merges.sh

diff --git a/Documentation/git-rebase.txt b/Documentation/git-rebase.txt
index 8a861c1e0d6..e9da7e26329 100644
--- a/Documentation/git-rebase.txt
+++ b/Documentation/git-rebase.txt
@@ -368,6 +368,12 @@ The commit list format can be changed by setting the configuration option
 rebase.instructionFormat.  A customized instruction format will automatically
 have the long commit hash prepended to the format.
 
+--recreate-merges::
+	Recreate merge commits instead of flattening the history by replaying
+	merges. Merge conflict resolutions or manual amendments to merge
+	commits are not recreated automatically, but have to be recreated
+	manually.
+
 -p::
 --preserve-merges::
 	Recreate merge commits instead of flattening the history by replaying
@@ -770,7 +776,8 @@ BUGS
 The todo list presented by `--preserve-merges --interactive` does not
 represent the topology of the revision graph.  Editing commits and
 rewording their commit messages should work fine, but attempts to
-reorder commits tend to produce counterintuitive results.
+reorder commits tend to produce counterintuitive results. Use
+--recreate-merges for a more faithful representation.
 
 For example, an attempt to rearrange
 ------------
diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash
index 3683c772c55..6893c3adabc 100644
--- a/contrib/completion/git-completion.bash
+++ b/contrib/completion/git-completion.bash
@@ -2008,7 +2008,7 @@ _git_rebase ()
 	--*)
 		__gitcomp "
 			--onto --merge --strategy --interactive
-			--preserve-merges --stat --no-stat
+			--recreate-merges --preserve-merges --stat --no-stat
 			--committer-date-is-author-date --ignore-date
 			--ignore-whitespace --whitespace=
 			--autosquash --no-autosquash
diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh
index 9d9d91f25e3..cfe3a537ac2 100644
--- a/git-rebase--interactive.sh
+++ b/git-rebase--interactive.sh
@@ -902,6 +902,7 @@ fi
 if test t != "$preserve_merges"
 then
 	git rebase--helper --make-script ${keep_empty:+--keep-empty} \
+		${recreate_merges:+--recreate-merges} \
 		$revisions ${restrict_revision+^$restrict_revision} >"$todo" ||
 	die "$(gettext "Could not generate todo list")"
 else
diff --git a/git-rebase.sh b/git-rebase.sh
index fd72a35c65b..d69bc7d0e0d 100755
--- a/git-rebase.sh
+++ b/git-rebase.sh
@@ -17,6 +17,7 @@ q,quiet!           be quiet. implies --no-stat
 autostash          automatically stash/stash pop before and after
 fork-point         use 'merge-base --fork-point' to refine upstream
 onto=!             rebase onto given branch instead of upstream
+recreate-merges!   try to recreate merges instead of skipping them
 p,preserve-merges! try to recreate merges instead of ignoring them
 s,strategy=!       use the given merge strategy
 no-ff!             cherry-pick all commits, even if unchanged
@@ -86,6 +87,7 @@ type=
 state_dir=
 # One of {'', continue, skip, abort}, as parsed from command line
 action=
+recreate_merges=
 preserve_merges=
 autosquash=
 keep_empty=
@@ -262,6 +264,10 @@ do
 	--keep-empty)
 		keep_empty=yes
 		;;
+	--recreate-merges)
+		recreate_merges=t
+		test -z "$interactive_rebase" && interactive_rebase=implied
+		;;
 	--preserve-merges)
 		preserve_merges=t
 		test -z "$interactive_rebase" && interactive_rebase=implied
diff --git a/t/t3430-rebase-recreate-merges.sh b/t/t3430-rebase-recreate-merges.sh
new file mode 100755
index 00000000000..0073601a206
--- /dev/null
+++ b/t/t3430-rebase-recreate-merges.sh
@@ -0,0 +1,146 @@
+#!/bin/sh
+#
+# Copyright (c) 2017 Johannes E. Schindelin
+#
+
+test_description='git rebase -i --recreate-merges
+
+This test runs git rebase "interactively", retaining the branch structure by
+recreating merge commits.
+
+Initial setup:
+
+    -- B --                   (first)
+   /       \
+ A - C - D - E - H            (master)
+       \       /
+         F - G                (second)
+'
+. ./test-lib.sh
+. "$TEST_DIRECTORY"/lib-rebase.sh
+
+test_expect_success 'setup' '
+	write_script replace-editor.sh <<-\EOF &&
+	mv "$1" "$(git rev-parse --git-path ORIGINAL-TODO)"
+	cp script-from-scratch "$1"
+	EOF
+
+	test_commit A &&
+	git checkout -b first &&
+	test_commit B &&
+	git checkout master &&
+	test_commit C &&
+	test_commit D &&
+	git merge --no-commit B &&
+	test_tick &&
+	git commit -m E &&
+	git tag -m E E &&
+	git checkout -b second C &&
+	test_commit F &&
+	test_commit G &&
+	git checkout master &&
+	git merge --no-commit G &&
+	test_tick &&
+	git commit -m H &&
+	git tag -m H H
+'
+
+cat >script-from-scratch <<\EOF
+label onto
+
+# onebranch
+pick G
+pick D
+label onebranch
+
+# second
+reset onto
+pick B
+label second
+
+reset onto
+merge -C H second
+merge onebranch # Merge the topic branch 'onebranch'
+EOF
+
+test_cmp_graph () {
+	cat >expect &&
+	git log --graph --boundary --format=%s "$@" >output &&
+	sed "s/ *$//" <output >output.trimmed &&
+	test_cmp expect output.trimmed
+}
+
+test_expect_success 'create completely different structure' '
+	test_config sequence.editor \""$PWD"/replace-editor.sh\" &&
+	test_tick &&
+	git rebase -i --recreate-merges A &&
+	test_cmp_graph <<-\EOF
+	*   Merge the topic branch '\''onebranch'\''
+	|\
+	| * D
+	| * G
+	* |   H
+	|\ \
+	| |/
+	|/|
+	| * B
+	|/
+	* A
+	EOF
+'
+
+test_expect_success 'generate correct todo list' '
+	cat >expect <<-\EOF &&
+	label onto
+
+	reset onto
+	pick d9df450 B
+	label E
+
+	reset onto
+	pick 5dee784 C
+	label branch-point
+	pick ca2c861 F
+	pick 088b00a G
+	label H
+
+	reset branch-point # C
+	pick 12bd07b D
+	merge -C 2051b56 E # E
+	merge -C 233d48a H # H
+
+	EOF
+
+	grep -v "^#" <.git/ORIGINAL-TODO >output &&
+	test_cmp expect output
+'
+
+test_expect_success 'with a branch tip that was cherry-picked already' '
+	git checkout -b already-upstream master &&
+	base="$(git rev-parse --verify HEAD)" &&
+
+	test_commit A1 &&
+	test_commit A2 &&
+	git reset --hard $base &&
+	test_commit B1 &&
+	test_tick &&
+	git merge -m "Merge branch A" A2 &&
+
+	git checkout -b upstream-with-a2 $base &&
+	test_tick &&
+	git cherry-pick A2 &&
+
+	git checkout already-upstream &&
+	test_tick &&
+	git rebase -i --recreate-merges upstream-with-a2 &&
+	test_cmp_graph upstream-with-a2.. <<-\EOF
+	*   Merge branch A
+	|\
+	| * A1
+	* | B1
+	|/
+	o A2
+	EOF
+'
+
+test_done
-- 
2.16.1.windows.1



^ permalink raw reply	[flat|nested] 276+ messages in thread

* [PATCH v3 09/12] sequencer: make refs generated by the `label` command worktree-local
  2018-02-11  0:09   ` [PATCH v3 00/12] " Johannes Schindelin
                       ` (7 preceding siblings ...)
  2018-02-11  0:10     ` [PATCH v3 08/12] rebase: introduce the --recreate-merges option Johannes Schindelin
@ 2018-02-11  0:10     ` Johannes Schindelin
  2018-02-11  0:10     ` [PATCH v3 10/12] sequencer: handle post-rewrite for merge commands Johannes Schindelin
                       ` (4 subsequent siblings)
  13 siblings, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-02-11  0:10 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Jacob Keller, Stefan Beller, Philip Oakley, Eric Sunshine, Phillip Wood

This allows for rebases to be run in parallel in separate worktrees
(think: interrupted in the middle of one rebase, being asked to perform
a different rebase, adding a separate worktree just for that job).

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 refs.c                            |  3 ++-
 t/t3430-rebase-recreate-merges.sh | 14 ++++++++++++++
 2 files changed, 16 insertions(+), 1 deletion(-)

diff --git a/refs.c b/refs.c
index 20ba82b4343..e8b84c189ff 100644
--- a/refs.c
+++ b/refs.c
@@ -600,7 +600,8 @@ int dwim_log(const char *str, int len, struct object_id *oid, char **log)
 static int is_per_worktree_ref(const char *refname)
 {
 	return !strcmp(refname, "HEAD") ||
-		starts_with(refname, "refs/bisect/");
+		starts_with(refname, "refs/bisect/") ||
+		starts_with(refname, "refs/rewritten/");
 }
 
 static int is_pseudoref_syntax(const char *refname)
diff --git a/t/t3430-rebase-recreate-merges.sh b/t/t3430-rebase-recreate-merges.sh
index 0073601a206..1a3e43d66ff 100755
--- a/t/t3430-rebase-recreate-merges.sh
+++ b/t/t3430-rebase-recreate-merges.sh
@@ -143,4 +143,18 @@ test_expect_success 'with a branch tip that was cherry-picked already' '
 	EOF
 '
 
+test_expect_success 'refs/rewritten/* is worktree-local' '
+	git worktree add wt &&
+	cat >wt/script-from-scratch <<-\EOF &&
+	label xyz
+	exec GIT_DIR=../.git git rev-parse --verify refs/rewritten/xyz >a || :
+	exec git rev-parse --verify refs/rewritten/xyz >b
+	EOF
+
+	test_config -C wt sequence.editor \""$PWD"/replace-editor.sh\" &&
+	git -C wt rebase -i HEAD &&
+	test_must_be_empty wt/a &&
+	test_cmp_rev HEAD "$(cat wt/b)"
+'
+
 test_done
-- 
2.16.1.windows.1



^ permalink raw reply	[flat|nested] 276+ messages in thread

* [PATCH v3 10/12] sequencer: handle post-rewrite for merge commands
  2018-02-11  0:09   ` [PATCH v3 00/12] " Johannes Schindelin
                       ` (8 preceding siblings ...)
  2018-02-11  0:10     ` [PATCH v3 09/12] sequencer: make refs generated by the `label` command worktree-local Johannes Schindelin
@ 2018-02-11  0:10     ` Johannes Schindelin
  2018-02-11  0:10     ` [PATCH v3 11/12] pull: accept --rebase=recreate to recreate the branch topology Johannes Schindelin
                       ` (3 subsequent siblings)
  13 siblings, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-02-11  0:10 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Jacob Keller, Stefan Beller, Philip Oakley, Eric Sunshine, Phillip Wood

In the previous patches, we implemented the basic functionality of the
`git rebase -i --recreate-merges` command, in particular the `merge`
command to create merge commits in the sequencer.

The interactive rebase is a lot more these days, though, than a simple
cherry-pick in a loop. For example, it calls the post-rewrite hook (if
any) after rebasing with a mapping of the old->new commits.

This patch implements the post-rewrite handling for the `merge` command
we just introduced. The other commands that were added recently (`label`
and `reset`) do not create new commits, therefore post-rewrite do not
need to handle them.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 sequencer.c                       |  7 +++++--
 t/t3430-rebase-recreate-merges.sh | 25 +++++++++++++++++++++++++
 2 files changed, 30 insertions(+), 2 deletions(-)

diff --git a/sequencer.c b/sequencer.c
index 7cd091a9fd6..306ae014311 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -2452,11 +2452,14 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts)
 		else if (item->command == TODO_RESET)
 			res = do_reset(item->arg, item->arg_len, opts);
 		else if (item->command == TODO_MERGE ||
-			 item->command == TODO_MERGE_AND_EDIT)
+			 item->command == TODO_MERGE_AND_EDIT) {
 			res = do_merge(item->commit, item->arg, item->arg_len,
 				       item->command == TODO_MERGE_AND_EDIT ?
 				       EDIT_MSG | VERIFY_MSG : 0, opts);
-		else if (!is_noop(item->command))
+			if (item->commit)
+				record_in_rewritten(&item->commit->object.oid,
+						    peek_command(todo_list, 1));
+		} else if (!is_noop(item->command))
 			return error(_("unknown command %d"), item->command);
 
 		todo_list->current++;
diff --git a/t/t3430-rebase-recreate-merges.sh b/t/t3430-rebase-recreate-merges.sh
index 1a3e43d66ff..35a61ce90bb 100755
--- a/t/t3430-rebase-recreate-merges.sh
+++ b/t/t3430-rebase-recreate-merges.sh
@@ -157,4 +157,29 @@ test_expect_success 'refs/rewritten/* is worktree-local' '
 	test_cmp_rev HEAD "$(cat wt/b)"
 '
 
+test_expect_success 'post-rewrite hook and fixups work for merges' '
+	git checkout -b post-rewrite &&
+	test_commit same1 &&
+	git reset --hard HEAD^ &&
+	test_commit same2 &&
+	git merge -m "to fix up" same1 &&
+	echo same old same old >same2.t &&
+	test_tick &&
+	git commit --fixup HEAD same2.t &&
+	fixup="$(git rev-parse HEAD)" &&
+
+	mkdir -p .git/hooks &&
+	test_when_finished "rm .git/hooks/post-rewrite" &&
+	echo "cat >actual" | write_script .git/hooks/post-rewrite &&
+
+	test_tick &&
+	git rebase -i --autosquash --recreate-merges HEAD^^^ &&
+	printf "%s %s\n%s %s\n%s %s\n%s %s\n" >expect $(git rev-parse \
+		$fixup^^2 HEAD^2 \
+		$fixup^^ HEAD^ \
+		$fixup^ HEAD \
+		$fixup HEAD) &&
+	test_cmp expect actual
+'
+
 test_done
-- 
2.16.1.windows.1



^ permalink raw reply	[flat|nested] 276+ messages in thread

* [PATCH v3 11/12] pull: accept --rebase=recreate to recreate the branch topology
  2018-02-11  0:09   ` [PATCH v3 00/12] " Johannes Schindelin
                       ` (9 preceding siblings ...)
  2018-02-11  0:10     ` [PATCH v3 10/12] sequencer: handle post-rewrite for merge commands Johannes Schindelin
@ 2018-02-11  0:10     ` Johannes Schindelin
  2018-02-11  0:10     ` [PATCH v3 12/12] rebase -i: introduce --recreate-merges=[no-]rebase-cousins Johannes Schindelin
                       ` (2 subsequent siblings)
  13 siblings, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-02-11  0:10 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Jacob Keller, Stefan Beller, Philip Oakley, Eric Sunshine, Phillip Wood

Similar to the `preserve` mode simply passing the `--preserve-merges`
option to the `rebase` command, the `recreate` mode simply passes the
`--recreate-merges` option.

This will allow users to conveniently rebase non-trivial commit
topologies when pulling new commits, without flattening them.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 Documentation/config.txt               |  8 ++++++++
 Documentation/git-pull.txt             |  5 ++++-
 builtin/pull.c                         | 14 ++++++++++----
 builtin/remote.c                       |  2 ++
 contrib/completion/git-completion.bash |  2 +-
 5 files changed, 25 insertions(+), 6 deletions(-)

diff --git a/Documentation/config.txt b/Documentation/config.txt
index 0e25b2c92b3..da41ab246dc 100644
--- a/Documentation/config.txt
+++ b/Documentation/config.txt
@@ -1058,6 +1058,10 @@ branch.<name>.rebase::
 	"git pull" is run. See "pull.rebase" for doing this in a non
 	branch-specific manner.
 +
+When recreate, also pass `--recreate-merges` along to 'git rebase'
+so that locally committed merge commits will not be flattened
+by running 'git pull'.
++
 When preserve, also pass `--preserve-merges` along to 'git rebase'
 so that locally committed merge commits will not be flattened
 by running 'git pull'.
@@ -2607,6 +2611,10 @@ pull.rebase::
 	pull" is run. See "branch.<name>.rebase" for setting this on a
 	per-branch basis.
 +
+When recreate, also pass `--recreate-merges` along to 'git rebase'
+so that locally committed merge commits will not be flattened
+by running 'git pull'.
++
 When preserve, also pass `--preserve-merges` along to 'git rebase'
 so that locally committed merge commits will not be flattened
 by running 'git pull'.
diff --git a/Documentation/git-pull.txt b/Documentation/git-pull.txt
index ce05b7a5b13..b4f9f057ea9 100644
--- a/Documentation/git-pull.txt
+++ b/Documentation/git-pull.txt
@@ -101,13 +101,16 @@ Options related to merging
 include::merge-options.txt[]
 
 -r::
---rebase[=false|true|preserve|interactive]::
+--rebase[=false|true|recreate|preserve|interactive]::
 	When true, rebase the current branch on top of the upstream
 	branch after fetching. If there is a remote-tracking branch
 	corresponding to the upstream branch and the upstream branch
 	was rebased since last fetched, the rebase uses that information
 	to avoid rebasing non-local changes.
 +
+When set to recreate, rebase with the `--recreate-merges` option passed
+to `git rebase` so that locally created merge commits will not be flattened.
++
 When set to preserve, rebase with the `--preserve-merges` option passed
 to `git rebase` so that locally created merge commits will not be flattened.
 +
diff --git a/builtin/pull.c b/builtin/pull.c
index 511dbbe0f6e..e33c84e0345 100644
--- a/builtin/pull.c
+++ b/builtin/pull.c
@@ -27,14 +27,16 @@ enum rebase_type {
 	REBASE_FALSE = 0,
 	REBASE_TRUE,
 	REBASE_PRESERVE,
+	REBASE_RECREATE,
 	REBASE_INTERACTIVE
 };
 
 /**
  * Parses the value of --rebase. If value is a false value, returns
  * REBASE_FALSE. If value is a true value, returns REBASE_TRUE. If value is
- * "preserve", returns REBASE_PRESERVE. If value is a invalid value, dies with
- * a fatal error if fatal is true, otherwise returns REBASE_INVALID.
+ * "recreate", returns REBASE_RECREATE. If value is "preserve", returns
+ * REBASE_PRESERVE. If value is a invalid value, dies with a fatal error if
+ * fatal is true, otherwise returns REBASE_INVALID.
  */
 static enum rebase_type parse_config_rebase(const char *key, const char *value,
 		int fatal)
@@ -47,6 +49,8 @@ static enum rebase_type parse_config_rebase(const char *key, const char *value,
 		return REBASE_TRUE;
 	else if (!strcmp(value, "preserve"))
 		return REBASE_PRESERVE;
+	else if (!strcmp(value, "recreate"))
+		return REBASE_RECREATE;
 	else if (!strcmp(value, "interactive"))
 		return REBASE_INTERACTIVE;
 
@@ -130,7 +134,7 @@ static struct option pull_options[] = {
 	/* Options passed to git-merge or git-rebase */
 	OPT_GROUP(N_("Options related to merging")),
 	{ OPTION_CALLBACK, 'r', "rebase", &opt_rebase,
-	  "false|true|preserve|interactive",
+	  "false|true|recreate|preserve|interactive",
 	  N_("incorporate changes by rebasing rather than merging"),
 	  PARSE_OPT_OPTARG, parse_opt_rebase },
 	OPT_PASSTHRU('n', NULL, &opt_diffstat, NULL,
@@ -798,7 +802,9 @@ static int run_rebase(const struct object_id *curr_head,
 	argv_push_verbosity(&args);
 
 	/* Options passed to git-rebase */
-	if (opt_rebase == REBASE_PRESERVE)
+	if (opt_rebase == REBASE_RECREATE)
+		argv_array_push(&args, "--recreate-merges");
+	else if (opt_rebase == REBASE_PRESERVE)
 		argv_array_push(&args, "--preserve-merges");
 	else if (opt_rebase == REBASE_INTERACTIVE)
 		argv_array_push(&args, "--interactive");
diff --git a/builtin/remote.c b/builtin/remote.c
index d95bf904c3b..b7d0f7ce596 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -306,6 +306,8 @@ static int config_read_branches(const char *key, const char *value, void *cb)
 				info->rebase = v;
 			else if (!strcmp(value, "preserve"))
 				info->rebase = NORMAL_REBASE;
+			else if (!strcmp(value, "recreate"))
+				info->rebase = NORMAL_REBASE;
 			else if (!strcmp(value, "interactive"))
 				info->rebase = INTERACTIVE_REBASE;
 		}
diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash
index 6893c3adabc..6f98c96fee9 100644
--- a/contrib/completion/git-completion.bash
+++ b/contrib/completion/git-completion.bash
@@ -2182,7 +2182,7 @@ _git_config ()
 		return
 		;;
 	branch.*.rebase)
-		__gitcomp "false true preserve interactive"
+		__gitcomp "false true recreate preserve interactive"
 		return
 		;;
 	remote.pushdefault)
-- 
2.16.1.windows.1



^ permalink raw reply	[flat|nested] 276+ messages in thread

* [PATCH v3 12/12] rebase -i: introduce --recreate-merges=[no-]rebase-cousins
  2018-02-11  0:09   ` [PATCH v3 00/12] " Johannes Schindelin
                       ` (10 preceding siblings ...)
  2018-02-11  0:10     ` [PATCH v3 11/12] pull: accept --rebase=recreate to recreate the branch topology Johannes Schindelin
@ 2018-02-11  0:10     ` Johannes Schindelin
  2018-02-23 12:35     ` [PATCH v4 00/12] rebase -i: offer to recreate merge commits Johannes Schindelin
  2018-02-26 21:29     ` [PATCH v5 " Johannes Schindelin
  13 siblings, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-02-11  0:10 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Jacob Keller, Stefan Beller, Philip Oakley, Eric Sunshine, Phillip Wood

This one is a bit tricky to explain, so let's try with a diagram:

        C
      /   \
A - B - E - F
  \   /
    D

To illustrate what this new mode is all about, let's consider what
happens upon `git rebase -i --recreate-merges B`, in particular to
the commit `D`. So far, the new branch structure would be:

       --- C' --
      /         \
A - B ------ E' - F'
      \    /
        D'

This is not really preserving the branch topology from before! The
reason is that the commit `D` does not have `B` as ancestor, and
therefore it gets rebased onto `B`.

This is unintuitive behavior. Even worse, when recreating branch
structure, most use cases would appear to want cousins *not* to be
rebased onto the new base commit. For example, Git for Windows (the
heaviest user of the Git garden shears, which served as the blueprint
for --recreate-merges) frequently merges branches from `next` early, and
these branches certainly do *not* want to be rebased. In the example
above, the desired outcome would look like this:

       --- C' --
      /         \
A - B ------ E' - F'
  \        /
   -- D' --

Let's introduce the term "cousins" for such commits ("D" in the
example), and let's not rebase them by default, introducing the new
"rebase-cousins" mode for use cases where they should be rebased.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 Documentation/git-rebase.txt      |  7 ++++++-
 builtin/rebase--helper.c          |  9 ++++++++-
 git-rebase--interactive.sh        |  1 +
 git-rebase.sh                     | 12 +++++++++++-
 sequencer.c                       |  4 ++++
 sequencer.h                       |  6 ++++++
 t/t3430-rebase-recreate-merges.sh | 23 +++++++++++++++++++++++
 7 files changed, 59 insertions(+), 3 deletions(-)

diff --git a/Documentation/git-rebase.txt b/Documentation/git-rebase.txt
index e9da7e26329..0e6d020d924 100644
--- a/Documentation/git-rebase.txt
+++ b/Documentation/git-rebase.txt
@@ -368,11 +368,16 @@ The commit list format can be changed by setting the configuration option
 rebase.instructionFormat.  A customized instruction format will automatically
 have the long commit hash prepended to the format.
 
---recreate-merges::
+--recreate-merges[=(rebase-cousins|no-rebase-cousins)]::
 	Recreate merge commits instead of flattening the history by replaying
 	merges. Merge conflict resolutions or manual amendments to merge
 	commits are not recreated automatically, but have to be recreated
 	manually.
++
+By default, or when `no-rebase-cousins` was specified, commits which do not
+have `<upstream>` as direct ancestor keep their original branch point.
+If the `rebase-cousins` mode is turned on, such commits are rebased onto
+`<upstream>` (or `<onto>`, if specified).
 
 -p::
 --preserve-merges::
diff --git a/builtin/rebase--helper.c b/builtin/rebase--helper.c
index a34ab5c0655..cea99cb3235 100644
--- a/builtin/rebase--helper.c
+++ b/builtin/rebase--helper.c
@@ -13,7 +13,7 @@ int cmd_rebase__helper(int argc, const char **argv, const char *prefix)
 {
 	struct replay_opts opts = REPLAY_OPTS_INIT;
 	unsigned flags = 0, keep_empty = 0, recreate_merges = 0;
-	int abbreviate_commands = 0;
+	int abbreviate_commands = 0, rebase_cousins = -1;
 	enum {
 		CONTINUE = 1, ABORT, MAKE_SCRIPT, SHORTEN_OIDS, EXPAND_OIDS,
 		CHECK_TODO_LIST, SKIP_UNNECESSARY_PICKS, REARRANGE_SQUASH,
@@ -23,6 +23,8 @@ int cmd_rebase__helper(int argc, const char **argv, const char *prefix)
 		OPT_BOOL(0, "ff", &opts.allow_ff, N_("allow fast-forward")),
 		OPT_BOOL(0, "keep-empty", &keep_empty, N_("keep empty commits")),
 		OPT_BOOL(0, "recreate-merges", &recreate_merges, N_("recreate merge commits")),
+		OPT_BOOL(0, "rebase-cousins", &rebase_cousins,
+			 N_("keep original branch points of cousins")),
 		OPT_CMDMODE(0, "continue", &command, N_("continue rebase"),
 				CONTINUE),
 		OPT_CMDMODE(0, "abort", &command, N_("abort rebase"),
@@ -57,8 +59,13 @@ int cmd_rebase__helper(int argc, const char **argv, const char *prefix)
 	flags |= keep_empty ? TODO_LIST_KEEP_EMPTY : 0;
 	flags |= abbreviate_commands ? TODO_LIST_ABBREVIATE_CMDS : 0;
 	flags |= recreate_merges ? TODO_LIST_RECREATE_MERGES : 0;
+	flags |= rebase_cousins > 0 ? TODO_LIST_REBASE_COUSINS : 0;
 	flags |= command == SHORTEN_OIDS ? TODO_LIST_SHORTEN_IDS : 0;
 
+	if (rebase_cousins >= 0 && !recreate_merges)
+		warning(_("--[no-]rebase-cousins has no effect without "
+			  "--recreate-merges"));
+
 	if (command == CONTINUE && argc == 1)
 		return !!sequencer_continue(&opts);
 	if (command == ABORT && argc == 1)
diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh
index cfe3a537ac2..e199fe1cca5 100644
--- a/git-rebase--interactive.sh
+++ b/git-rebase--interactive.sh
@@ -903,6 +903,7 @@ if test t != "$preserve_merges"
 then
 	git rebase--helper --make-script ${keep_empty:+--keep-empty} \
 		${recreate_merges:+--recreate-merges} \
+		${rebase_cousins:+--rebase-cousins} \
 		$revisions ${restrict_revision+^$restrict_revision} >"$todo" ||
 	die "$(gettext "Could not generate todo list")"
 else
diff --git a/git-rebase.sh b/git-rebase.sh
index d69bc7d0e0d..58d778a2da0 100755
--- a/git-rebase.sh
+++ b/git-rebase.sh
@@ -17,7 +17,7 @@ q,quiet!           be quiet. implies --no-stat
 autostash          automatically stash/stash pop before and after
 fork-point         use 'merge-base --fork-point' to refine upstream
 onto=!             rebase onto given branch instead of upstream
-recreate-merges!   try to recreate merges instead of skipping them
+recreate-merges?   try to recreate merges instead of skipping them
 p,preserve-merges! try to recreate merges instead of ignoring them
 s,strategy=!       use the given merge strategy
 no-ff!             cherry-pick all commits, even if unchanged
@@ -88,6 +88,7 @@ state_dir=
 # One of {'', continue, skip, abort}, as parsed from command line
 action=
 recreate_merges=
+rebase_cousins=
 preserve_merges=
 autosquash=
 keep_empty=
@@ -268,6 +269,15 @@ do
 		recreate_merges=t
 		test -z "$interactive_rebase" && interactive_rebase=implied
 		;;
+	--recreate-merges=*)
+		recreate_merges=t
+		case "${1#*=}" in
+		rebase-cousins) rebase_cousins=t;;
+		no-rebase-cousins) rebase_cousins=;;
+		*) die "Unknown mode: $1";;
+		esac
+		test -z "$interactive_rebase" && interactive_rebase=implied
+		;;
 	--preserve-merges)
 		preserve_merges=t
 		test -z "$interactive_rebase" && interactive_rebase=implied
diff --git a/sequencer.c b/sequencer.c
index 306ae014311..c877432d7b4 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -2931,6 +2931,7 @@ static int make_script_with_merges(struct pretty_print_context *pp,
 				   unsigned flags)
 {
 	int keep_empty = flags & TODO_LIST_KEEP_EMPTY;
+	int rebase_cousins = flags & TODO_LIST_REBASE_COUSINS;
 	struct strbuf buf = STRBUF_INIT, oneline = STRBUF_INIT;
 	struct strbuf label = STRBUF_INIT;
 	struct commit_list *commits = NULL, **tail = &commits, *iter;
@@ -3106,6 +3107,9 @@ static int make_script_with_merges(struct pretty_print_context *pp,
 					   &commit->object.oid);
 			if (entry)
 				to = entry->string;
+			else if (!rebase_cousins)
+				to = label_oid(&commit->object.oid, NULL,
+					       &state);
 
 			if (!to || !strcmp(to, "onto"))
 				fprintf(out, "%s onto\n", cmd_reset);
diff --git a/sequencer.h b/sequencer.h
index 11d1ac925ef..deebc6e3258 100644
--- a/sequencer.h
+++ b/sequencer.h
@@ -49,6 +49,12 @@ int sequencer_remove_state(struct replay_opts *opts);
 #define TODO_LIST_SHORTEN_IDS (1U << 1)
 #define TODO_LIST_ABBREVIATE_CMDS (1U << 2)
 #define TODO_LIST_RECREATE_MERGES (1U << 3)
+/*
+ * When recreating merges, commits that do have the base commit as ancestor
+ * ("cousins") are *not* rebased onto the new base by default. If those
+ * commits should be rebased onto the new base, this flag needs to be passed.
+ */
+#define TODO_LIST_REBASE_COUSINS (1U << 4)
 int sequencer_make_script(FILE *out, int argc, const char **argv,
 			  unsigned flags);
 
diff --git a/t/t3430-rebase-recreate-merges.sh b/t/t3430-rebase-recreate-merges.sh
index 35a61ce90bb..9a59f12b670 100755
--- a/t/t3430-rebase-recreate-merges.sh
+++ b/t/t3430-rebase-recreate-merges.sh
@@ -143,6 +143,29 @@ test_expect_success 'with a branch tip that was cherry-picked already' '
 	EOF
 '
 
+test_expect_success 'do not rebase cousins unless asked for' '
+	write_script copy-editor.sh <<-\EOF &&
+	cp "$1" "$(git rev-parse --git-path ORIGINAL-TODO)"
+	EOF
+
+	test_config sequence.editor \""$PWD"/copy-editor.sh\" &&
+	git checkout -b cousins master &&
+	before="$(git rev-parse --verify HEAD)" &&
+	test_tick &&
+	git rebase -i --recreate-merges HEAD^ &&
+	test_cmp_rev HEAD $before &&
+	test_tick &&
+	git rebase -i --recreate-merges=rebase-cousins HEAD^ &&
+	test_cmp_graph HEAD^.. <<-\EOF
+	*   Merge the topic branch '\''onebranch'\''
+	|\
+	| * D
+	| * G
+	|/
+	o H
+	EOF
+'
+
 test_expect_success 'refs/rewritten/* is worktree-local' '
 	git worktree add wt &&
 	cat >wt/script-from-scratch <<-\EOF &&
-- 
2.16.1.windows.1

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 5/8] rebase: introduce the --recreate-merges option
  2018-02-09  7:13         ` Johannes Sixt
@ 2018-02-11 10:16           ` Jacob Keller
  2018-02-12  7:38           ` Sergey Organov
  1 sibling, 0 replies; 276+ messages in thread
From: Jacob Keller @ 2018-02-11 10:16 UTC (permalink / raw)
  To: Johannes Sixt; +Cc: Sergey Organov, Johannes Schindelin, Git mailing list, Junio C Hamano

On Thu, Feb 8, 2018 at 11:13 PM, Johannes Sixt <j6t@kdbg.org> wrote:
> Am 09.02.2018 um 07:11 schrieb Sergey Organov:
>>
>> Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:
>>>
>>> Let me explain the scenario which comes up plenty of times in my work
>>> with
>>> Git for Windows. We have a thicket of some 70 branches on top of
>>> git.git's
>>> latest release. These branches often include fixup! and squash! commits
>>> and even more complicated constructs that rebase cannot handle at all at
>>> the moment, such as reorder-before! and reorder-after! (for commits that
>>> really need to go into a different branch).
>>
>>
>> I sympathize, but a solution that breaks even in simple cases can't be
>> used reliably to solve more complex problems, sorry. Being so deep
>> into your problems, I think you maybe just aren't seeing forest for the
>> trees [1].
>
>
> Hold your horses! Dscho has a point here. --preserve-merges --first-parent
> works only as long as you don't tamper with the side branches. If you make
> changes in the side branches during the same rebase operation, this
> --first-parent mode would undo that change. (And, yes, its result would be
> called an "evil merge", and that scary name _should_ frighten you!)
>
> -- Hannes

This is the reason I agree with Johannes, in regards to why
recreate-merges approach is correct.

Yes, an ideal system would be one which correctly, automatically
re-creates the merge *as if* a human had re-merged the two newly
re-created side branches, and preserves any changes in the result of
the merge, such as cases we call "evil merges" which includes
necessary changes to resolve conflicts properly.

However, I would state that such a system, in order to cause the least
surprise to a user must be correct against arbitrary removal, reorder,
and addition of new commits on both the main and topic side branches
for which we are re-creating the merges.

This is problematic, because something like how --preserve-merges
--first-parent does not work under this case.

As a user of the tool, I may be biased because I already read and
understand how recreate-merges is expected to work, but it makes sense
to me that the re-creation of the merge merely re-does the merge and
any modfications in the original would have to be carried over.

I don't know what process we could use to essentially move the changes
from the original merge into the new copy. What ever solution we have
would need to have a coherent user interface and be presentable in
some manner.

One way to think about the contents we're wanting to keep, rather than
the full tree result of the merge which we had before, what we
actually want to keep in some sense is the resulting "diff" as shown
by something like the condensed --combined output. This is obviously
not really a diff that we can apply.

And even if we could apply it, since the merge is occurring, we can't
exactly use 3-way merge conflict in order to actually apply the old
changes into the new merged setup? Could something like rerere logic
work here to track what was done and then re-apply it to the new merge
we create? And once we apply it, we need to be able to handle any
conflicts that occur because of deleting, adding, or re-ordering
commits on the branches we're merging... so in some sense we could
have "conflicts of conflicts" which is a scenario that I don't yet
have a good handle on how this would be presented to the user.

Thanks,
Jake

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 5/8] rebase: introduce the --recreate-merges option
  2018-02-10 23:06     ` Johannes Schindelin
@ 2018-02-12  4:58       ` Sergey Organov
  2018-02-12 20:21         ` Johannes Schindelin
  2018-02-12  5:22       ` Sergey Organov
  1 sibling, 1 reply; 276+ messages in thread
From: Sergey Organov @ 2018-02-12  4:58 UTC (permalink / raw)
  To: Johannes Schindelin; +Cc: git, Junio C Hamano, Jacob Keller

Hi Johannes,

Thanks for explanations, and could you please answer this one:

[...]

>> I also have trouble making sense of "Recreate merge commits instead of
>> flattening the history by replaying merges." Is it "<Recreate merge
>> commits by replaying merges> instead of <flattening the history>" or is it
>> rather "<Recreate merge commits> instead of <flattening the history by
>> replaying merges>?

-- Sergey

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 5/8] rebase: introduce the --recreate-merges option
  2018-02-10 23:06     ` Johannes Schindelin
  2018-02-12  4:58       ` Sergey Organov
@ 2018-02-12  5:22       ` Sergey Organov
  2018-02-12 20:39         ` Johannes Schindelin
  1 sibling, 1 reply; 276+ messages in thread
From: Sergey Organov @ 2018-02-12  5:22 UTC (permalink / raw)
  To: Johannes Schindelin; +Cc: git, Junio C Hamano, Jacob Keller

Hi Johannes,

Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:
> Hi Sergey,
>
> On Fri, 9 Feb 2018, Sergey Organov wrote:
>
>> Johannes Schindelin <johannes.schindelin@gmx.de> writes:
>> 
>> [...]
>> 
>> > With this patch, the goodness of the Git garden shears comes to `git
>> > rebase -i` itself. Passing the `--recreate-merges` option will generate
>> > a todo list that can be understood readily, and where it is obvious
>> > how to reorder commits. New branches can be introduced by inserting
>> > `label` commands and calling `merge - <label> <oneline>`. And once this
>> > mode has become stable and universally accepted, we can deprecate the
>> > design mistake that was `--preserve-merges`.
>> 
>> This doesn't explain why you introduced this new --recreate-merges. Why
>> didn't you rather fix --preserve-merges to generate and use new todo
>> list format?
>
> Because that would of course break existing users of
> --preserve-merges.

How exactly? Doesn't "--recreate-merges" produce the same result as
"--preserve-merges" if run non-interactively?

> So why not --preserve-merges=v2? Because that would force me to maintain
> --preserve-merges forever. And I don't want to.
>
>> It doesn't seem likely that todo list created by one Git version is to
>> be ever used by another, right?
>
> No. But by scripts based on `git rebase -p`.
>
>> Is there some hidden reason here? Some tools outside of Git that use old
>> todo list format, maybe?
>
> Exactly.
>
> I did mention such a tool: the Git garden shears:
>
> 	https://github.com/git-for-windows/build-extra/blob/master/shears.sh
>
> Have a look at it. It will inform the discussion.

I've searched for "-p" in the script, but didn't find positives for
either "-p" or "--preserve-merges". How it would break if it doesn't use
them? What am I missing?

>
>> Then, if new option indeed required, please look at the resulting manual:
>> 
>> --recreate-merges::
>> 	Recreate merge commits instead of flattening the history by replaying
>> 	merges. Merge conflict resolutions or manual amendments to merge
>> 	commits are not preserved.
>> 
>> -p::
>> --preserve-merges::
>> 	Recreate merge commits instead of flattening the history by replaying
>> 	commits a merge commit introduces. Merge conflict resolutions or manual
>> 	amendments to merge commits are not preserved.
>
> As I stated in the cover letter, there are more patches lined up after
> this patch series.

Good, but I thought this one should better be self-consistent anyway.
What if those that come later aren't included?

>
> Have a look at https://github.com/git/git/pull/447, especially the latest
> commit in there which is an early version of the deprecation I intend to
> bring about.

You shouldn't want a deprecation at all should you have re-used
--preserve-merges in the first place, and I still don't see why you
haven't. 

>
> Also, please refrain from saying things like... "Don't you think ..."
>
> If you don't like the wording, I wold much more appreciate it if a better
> alternative was suggested.

Sorry, but how can I suggest one if I don't understand what you are
doing here in the first place? That's why I ask you.

>
>> Don't you think more explanations are needed there in the manual on
>> why do we have 2 separate options with almost the same yet subtly
>> different description? Is this subtle difference even important? How?
>> 
>> I also have trouble making sense of "Recreate merge commits instead of
>> flattening the history by replaying merges." Is it "<Recreate merge
>> commits by replaying merges> instead of <flattening the history>" or is it
>> rather "<Recreate merge commits> instead of <flattening the history by
>> replaying merges>?
>
> The documentation of the --recreate-merges option is not meant to explain
> the difference to --preserve-merges. It is meant to explain the difference
> to regular `git rebase -i`, which flattens the commit history into a
> single branch without merge commits (in fact, all merge commits are simply
> ignored).

Yeah, that's obvious, but the point is that resulting manual is ended
up being confusing.

> And I would rather not start to describe the difference between
> --recreate-merges and --preserve-merges because I want to deprecate the
> latter, and describing the difference as I get the sense is your wish
> would simply mean more work because it would have to be added and then
> removed again.

I suspect you actually didn't need those new option in the first place,
and that's the core reason of these troubles.

-- Sergey

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 5/8] rebase: introduce the --recreate-merges option
  2018-02-09  7:13         ` Johannes Sixt
  2018-02-11 10:16           ` Jacob Keller
@ 2018-02-12  7:38           ` Sergey Organov
  1 sibling, 0 replies; 276+ messages in thread
From: Sergey Organov @ 2018-02-12  7:38 UTC (permalink / raw)
  To: Johannes Sixt; +Cc: Johannes Schindelin, git, Junio C Hamano, Jacob Keller

Johannes Sixt <j6t@kdbg.org> writes:

> Am 09.02.2018 um 07:11 schrieb Sergey Organov:
>> Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:
>>> Let me explain the scenario which comes up plenty of times in my work with
>>> Git for Windows. We have a thicket of some 70 branches on top of git.git's
>>> latest release. These branches often include fixup! and squash! commits
>>> and even more complicated constructs that rebase cannot handle at all at
>>> the moment, such as reorder-before! and reorder-after! (for commits that
>>> really need to go into a different branch).
>>
>> I sympathize, but a solution that breaks even in simple cases can't be
>> used reliably to solve more complex problems, sorry. Being so deep
>> into your problems, I think you maybe just aren't seeing forest for the
>> trees [1].
>
> Hold your horses! Dscho has a point here. --preserve-merges
> --first-parent works only as long as you don't tamper with the side
> branches. If you make changes in the side branches during the same
> rebase operation, this --first-parent mode would undo that change.

He has a point indeed, but it must not be used as an excuse to silently
damage user data, as if there are no other options!

Simple --first-parent won't always fit, it's obvious. I used
--first-parent patch as mere illustration of concept, it's rather
"rebase [-i] --keep-the-f*g-shape" itself that should behave. There
should be no need for actual --first-parent that only fits
no-manual-editing use-cases.

Look at it as if it's a scale where --first-parent is on one side, and
"blind re-merge" is on the other. The right answer(s) lie somewhere
in-between, but I think they are much closer to --first-parent than they
are to "blind re-merge".

> (And, yes, its result would be called an "evil merge", and that scary
> name _should_ frighten you!)

(It won't always be "evil merge", and it still doesn't frighten even if
it will, provided git stops making them more evil then they actually
deserve, and it isn't an excuse to silently distort user data anyway!)

-- Sergey

[1] The "--first-parent" here would rather keep that change from
propagation to the main-line, not undo it, and sometimes it's even the
right thing to do ("-x ours" for the original merge being one example).
Frequently though it is needed on main-line indeed, and there should be
a way to tell git to propagate the change to the main-line, but even
then automatic blind unattended re-merge is wrong answer and I'm sure
git can be made to do better than that.

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH v3 05/12] sequencer: introduce the `merge` command
  2018-02-11  0:10     ` [PATCH v3 05/12] sequencer: introduce the `merge` command Johannes Schindelin
@ 2018-02-12  8:48       ` Eric Sunshine
  2018-02-12 20:17         ` Johannes Schindelin
  0 siblings, 1 reply; 276+ messages in thread
From: Eric Sunshine @ 2018-02-12  8:48 UTC (permalink / raw)
  To: Johannes Schindelin; +Cc: Git List, Junio C Hamano, Jacob Keller, Stefan Beller, Philip Oakley, Phillip Wood

On Sat, Feb 10, 2018 at 7:10 PM, Johannes Schindelin
<johannes.schindelin@gmx.de> wrote:
> This patch is part of the effort to reimplement `--preserve-merges` with
> a substantially improved design, a design that has been developed in the
> Git for Windows project to maintain the dozens of Windows-specific patch
> series on top of upstream Git.
>
> The previous patch implemented the `label` and `reset` commands to label
> commits and to reset to a labeled commits. This patch adds the `merge`

s/to a/to/

> command, with the following syntax:
>
>         merge [-C <commit>] <rev> # <oneline>
>
> The <commit> parameter in this instance is the *original* merge commit,
> whose author and message will be used for the merge commit that is about
> to be created.
>
> The <rev> parameter refers to the (possibly rewritten) revision to
> merge. Let's see an example of a todo list:
>
>         label onto
>
>         # Branch abc
>         reset onto
>         pick deadbeef Hello, world!
>         label abc
>
>         reset onto
>         pick cafecafe And now for something completely different
>         merge -C baaabaaa abc # Merge the branch 'abc' into master
>
> To edit the merge commit's message (a "reword" for merges, if you will),
> use `-c` (lower-case) instead of `-C`; this convention was borrowed from
> `git commit` that also supports `-c` and `-C` with similar meanings.
>
> To create *new* merges, i.e. without copying the commit message from an
> existing commit, simply omit the `-C <commit>` parameter (which will
> open an editor for the merge message):
>
>         merge abc
>
> This comes in handy when splitting a branch into two or more branches.
>
> Note: this patch only adds support for recursive merges, to keep things
> simple. Support for octopus merges will be added later in a separate
> patch series, support for merges using strategies other than the
> recursive merge is left for the future.
>
> Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH v3 04/12] sequencer: introduce new commands to reset the revision
  2018-02-11  0:10     ` [PATCH v3 04/12] sequencer: introduce new commands to reset the revision Johannes Schindelin
@ 2018-02-12 19:26       ` Eric Sunshine
  2018-02-12 20:46         ` Johannes Schindelin
  0 siblings, 1 reply; 276+ messages in thread
From: Eric Sunshine @ 2018-02-12 19:26 UTC (permalink / raw)
  To: Johannes Schindelin; +Cc: Git List, Junio C Hamano, Jacob Keller, Stefan Beller, Philip Oakley, Phillip Wood

On Sat, Feb 10, 2018 at 7:10 PM, Johannes Schindelin
<johannes.schindelin@gmx.de> wrote:
> [...]
> This commit implements the commands to label, and to reset to, given
> revisions. The syntax is:
>
>         label <name>
>         reset <name>
> [...]
> Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
> ---
> diff --git a/sequencer.c b/sequencer.c
> @@ -1922,6 +1951,151 @@ static int do_exec(const char *command_line)
> +static int safe_append(const char *filename, const char *fmt, ...)
> +{
> +       [...]
> +       if (write_in_full(fd, buf.buf, buf.len) < 0) {
> +               error_errno(_("could not write to '%s'"), filename);
> +               rollback_lock_file(&lock);

strbuf_release(&buf);

> +               return -1;
> +       }
> +       if (commit_lock_file(&lock) < 0) {
> +               rollback_lock_file(&lock);

strbuf_release(&buf);

> +               return error(_("failed to finalize '%s'"), filename);
> +       }
> +

strbuf_release(&buf);

> +       return 0;
> +}
> +
> +static int do_reset(const char *name, int len, struct replay_opts *opts)
> +{
> +       [...]
> +       unpack_tree_opts.reset = 1;
> +
> +       if (read_cache_unmerged())

rollback_lock_file(&lock);
strbuf_release(&ref_name);

> +               return error_resolve_conflict(_(action_name(opts)));
> +
> +       if (!fill_tree_descriptor(&desc, &oid)) {
> +               error(_("failed to find tree of %s"), oid_to_hex(&oid));
> +               rollback_lock_file(&lock);
> +               free((void *)desc.buffer);
> +               strbuf_release(&ref_name);
> +               return -1;
> +       }

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH v3 05/12] sequencer: introduce the `merge` command
  2018-02-12  8:48       ` Eric Sunshine
@ 2018-02-12 20:17         ` Johannes Schindelin
  0 siblings, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-02-12 20:17 UTC (permalink / raw)
  To: Eric Sunshine; +Cc: Git List, Junio C Hamano, Jacob Keller, Stefan Beller, Philip Oakley, Phillip Wood

Hi Eric,

On Mon, 12 Feb 2018, Eric Sunshine wrote:

> On Sat, Feb 10, 2018 at 7:10 PM, Johannes Schindelin
> <johannes.schindelin@gmx.de> wrote:
> > This patch is part of the effort to reimplement `--preserve-merges` with
> > a substantially improved design, a design that has been developed in the
> > Git for Windows project to maintain the dozens of Windows-specific patch
> > series on top of upstream Git.
> >
> > The previous patch implemented the `label` and `reset` commands to label
> > commits and to reset to a labeled commits. This patch adds the `merge`
> 
> s/to a/to/

Fixed locally. Will be part of the next iteration, if one is necessary.
Otherwise I will first ask Junio whether he can touch up the commit
message before applying.

Ciao,
Dscho

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 5/8] rebase: introduce the --recreate-merges option
  2018-02-12  4:58       ` Sergey Organov
@ 2018-02-12 20:21         ` Johannes Schindelin
  2018-02-13  6:44           ` Sergey Organov
  0 siblings, 1 reply; 276+ messages in thread
From: Johannes Schindelin @ 2018-02-12 20:21 UTC (permalink / raw)
  To: Sergey Organov; +Cc: git, Junio C Hamano, Jacob Keller

Hi Sergey,

On Mon, 12 Feb 2018, Sergey Organov wrote:

> Thanks for explanations, and could you please answer this one:
> 
> [...]
> 
> >> I also have trouble making sense of "Recreate merge commits instead of
> >> flattening the history by replaying merges." Is it "<Recreate merge
> >> commits by replaying merges> instead of <flattening the history>" or is it
> >> rather "<Recreate merge commits> instead of <flattening the history by
> >> replaying merges>?

I thought I had answered that one.

Flattening the history is what happens in regular rebase (i.e. without
--recreate-merges and without --preserve-merges).

The idea to recreate merges is of course to *not* flatten the history.

Maybe there should have been a comma after "history" to clarify what the
sentence means.

The wording is poor either way, but you are also not a native speaker so
we have to rely on, say, Eric to help us out here.

Ciao,
Johannes

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 5/8] rebase: introduce the --recreate-merges option
  2018-02-12  5:22       ` Sergey Organov
@ 2018-02-12 20:39         ` Johannes Schindelin
  2018-02-13  4:39           ` Jacob Keller
  2018-02-13  6:43           ` Sergey Organov
  0 siblings, 2 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-02-12 20:39 UTC (permalink / raw)
  To: Sergey Organov; +Cc: git, Junio C Hamano, Jacob Keller

Hi Sergey,

On Mon, 12 Feb 2018, Sergey Organov wrote:

> Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:
> >
> > On Fri, 9 Feb 2018, Sergey Organov wrote:
> >
> >> Johannes Schindelin <johannes.schindelin@gmx.de> writes:
> >> 
> >> [...]
> >> 
> >> > With this patch, the goodness of the Git garden shears comes to `git
> >> > rebase -i` itself. Passing the `--recreate-merges` option will generate
> >> > a todo list that can be understood readily, and where it is obvious
> >> > how to reorder commits. New branches can be introduced by inserting
> >> > `label` commands and calling `merge - <label> <oneline>`. And once this
> >> > mode has become stable and universally accepted, we can deprecate the
> >> > design mistake that was `--preserve-merges`.
> >> 
> >> This doesn't explain why you introduced this new --recreate-merges. Why
> >> didn't you rather fix --preserve-merges to generate and use new todo
> >> list format?
> >
> > Because that would of course break existing users of
> > --preserve-merges.
> 
> How exactly?

Power users of interactive rebase use scripting to augment Git's
functionality. One particularly powerful trick is to override
GIT_SEQUENCER_EDITOR with an invocation of such a script, to perform
automated edits. Such a script breaks when we change the format of the
content to edit. If we change the format of the todo list generated in
--preserve-merges mode, that is exactly what happens. We break existing
users.

BTW it seems that you did not really read my previous reply carefully
because I referenced such a use case: the Git garden shears. They do
override the sequencer editor, and while they do not exactly edit the todo
list (they simply through the generated one away), they generate a new
todo list and would break if that format changes. Of course, the shears do
not use the --preserve-merges mode, but from just reading about the way
how the Git garden shears work, it is quite obvious how similar users of
--preserve-merges are likely to exist?

> Doesn't "--recreate-merges" produce the same result as
> "--preserve-merges" if run non-interactively?

The final result of a rebase where you do not edit the todo list? Should
be identical, indeed.

But that is the most boring, most uninteresting, and least important use
case. So we might just as well forget about it when we focus on keeping
Git's usage stable.

> > So why not --preserve-merges=v2? Because that would force me to
> > maintain --preserve-merges forever. And I don't want to.
> >
> >> It doesn't seem likely that todo list created by one Git version is
> >> to be ever used by another, right?
> >
> > No. But by scripts based on `git rebase -p`.
> >
> >> Is there some hidden reason here? Some tools outside of Git that use
> >> old todo list format, maybe?
> >
> > Exactly.
> >
> > I did mention such a tool: the Git garden shears:
> >
> > 	https://github.com/git-for-windows/build-extra/blob/master/shears.sh
> >
> > Have a look at it. It will inform the discussion.
> 
> I've searched for "-p" in the script, but didn't find positives for
> either "-p" or "--preserve-merges". How it would break if it doesn't use
> them? What am I missing?

*This* particular script does not use -p.

But it is not *this* particular script that I do not want to break! It is
*all* scripts that use interactive rebase! Don't you also care about not
breaking existing users?

> >> Then, if new option indeed required, please look at the resulting manual:
> >> 
> >> --recreate-merges::
> >> 	Recreate merge commits instead of flattening the history by replaying
> >> 	merges. Merge conflict resolutions or manual amendments to merge
> >> 	commits are not preserved.
> >> 
> >> -p::
> >> --preserve-merges::
> >> 	Recreate merge commits instead of flattening the history by replaying
> >> 	commits a merge commit introduces. Merge conflict resolutions or manual
> >> 	amendments to merge commits are not preserved.
> >
> > As I stated in the cover letter, there are more patches lined up after
> > this patch series.
> 
> Good, but I thought this one should better be self-consistent anyway.
> What if those that come later aren't included?

Right, let's just rip apart the partial progress because the latter
patches might not make it in?

I cannot work on that basis, and I also do not want to work on that basis.

If you do not like how the documentation is worded, fine, suggest a better
alternative.

> > Have a look at https://github.com/git/git/pull/447, especially the
> > latest commit in there which is an early version of the deprecation I
> > intend to bring about.
> 
> You shouldn't want a deprecation at all should you have re-used
> --preserve-merges in the first place, and I still don't see why you
> haven't. 

Keep repeating it, and it won't become truer.

If you break formats, you break scripts. Git has *so* many users, there
are very likely some who script *every* part of it.

We simply cannot do that.

What we can is deprecate designs which we learned on the way were not only
incomplete from the get-go, but bad overall and hard (or impossible) to
fix. Like --preserve-merges.

Or for that matter like the design you proposed, to use --first-parent for
--recreate-merges. Or to use --first-parent for some --recreate-merges,
surprising users in very bad ways when it is not used (or when it is
used). I get the impression that you still think it would be a good idea,
even if it should be obvious that it is not.

> > Also, please refrain from saying things like... "Don't you think ..."
> >
> > If you don't like the wording, I wold much more appreciate it if a better
> > alternative was suggested.
> 
> Sorry, but how can I suggest one if I don't understand what you are
> doing here in the first place? That's why I ask you.

There are ways to put the person you ask on trial. And there are ways to
genuinely show interest and seek education.

I am a really poor example how to communicate properly, of course, so
don't try to learn from me. I am trying myself to learn better ways to
express what I mean clearly, and to express it in a direct yet kind
manner.

> >> Don't you think more explanations are needed there in the manual on
> >> why do we have 2 separate options with almost the same yet subtly
> >> different description? Is this subtle difference even important? How?
> >> 
> >> I also have trouble making sense of "Recreate merge commits instead of
> >> flattening the history by replaying merges." Is it "<Recreate merge
> >> commits by replaying merges> instead of <flattening the history>" or is it
> >> rather "<Recreate merge commits> instead of <flattening the history by
> >> replaying merges>?
> >
> > The documentation of the --recreate-merges option is not meant to explain
> > the difference to --preserve-merges. It is meant to explain the difference
> > to regular `git rebase -i`, which flattens the commit history into a
> > single branch without merge commits (in fact, all merge commits are simply
> > ignored).
> 
> Yeah, that's obvious, but the point is that resulting manual is ended
> up being confusing.

Again, just saying something is bad, is bad. Saying something leaves room
for improvement and then suggesting how to improve it, is good.

> > And I would rather not start to describe the difference between
> > --recreate-merges and --preserve-merges because I want to deprecate the
> > latter, and describing the difference as I get the sense is your wish
> > would simply mean more work because it would have to be added and then
> > removed again.
> 
> I suspect you actually didn't need those new option in the first place,
> and that's the core reason of these troubles.

Are you suspecting that I, myself, do not use --recreate-merges?

If so, please read the cover letter again, in particular the part where I
describe how this entire series of patch series arose from the Git garden
shears, which I invented myself to help with maintaining Git for Windows,
and which I use for five years now. This should help disperse that
suspicion rather quickly: the intent of --recreate-merges is to allow me
to simplify the shears by quite a bit, and maybe eventually even get rid
of the script altogether (if I ever manage to convince myself that the
concept of a merging-rebase should be official enough to enter core Git).

I am a heavy user of --recreate-merges, even if it does not really exist
yet. I have five years of experience with it, which is the reason why I am
so confident about its design, and why I can tell you a lot about typical
use cases and common pitfalls, and where the original design had to be
adjusted.

Ciao,
Johannes

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH v3 04/12] sequencer: introduce new commands to reset the revision
  2018-02-12 19:26       ` Eric Sunshine
@ 2018-02-12 20:46         ` Johannes Schindelin
  0 siblings, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-02-12 20:46 UTC (permalink / raw)
  To: Eric Sunshine; +Cc: Git List, Junio C Hamano, Jacob Keller, Stefan Beller, Philip Oakley, Phillip Wood

Hi Eric,

On Mon, 12 Feb 2018, Eric Sunshine wrote:

> On Sat, Feb 10, 2018 at 7:10 PM, Johannes Schindelin
> <johannes.schindelin@gmx.de> wrote:
> > [...]
> > This commit implements the commands to label, and to reset to, given
> > revisions. The syntax is:
> >
> >         label <name>
> >         reset <name>
> > [...]
> > Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
> > ---
> > diff --git a/sequencer.c b/sequencer.c
> > @@ -1922,6 +1951,151 @@ static int do_exec(const char *command_line)
> > +static int safe_append(const char *filename, const char *fmt, ...)
> > +{
> > +       [...]
> > +       if (write_in_full(fd, buf.buf, buf.len) < 0) {
> > +               error_errno(_("could not write to '%s'"), filename);
> > +               rollback_lock_file(&lock);
> 
> strbuf_release(&buf);
> 
> > +               return -1;
> > +       }
> > +       if (commit_lock_file(&lock) < 0) {
> > +               rollback_lock_file(&lock);
> 
> strbuf_release(&buf);
> 
> > +               return error(_("failed to finalize '%s'"), filename);
> > +       }
> > +
> 
> strbuf_release(&buf);
> 
> > +       return 0;
> > +}
> > +
> > +static int do_reset(const char *name, int len, struct replay_opts *opts)
> > +{
> > +       [...]
> > +       unpack_tree_opts.reset = 1;
> > +
> > +       if (read_cache_unmerged())
> 
> rollback_lock_file(&lock);
> strbuf_release(&ref_name);

Thank you very much! I fixed these locally and force-pushed the
recreate-merges branch to https://github.com/dscho/git. These fixes will
be part of v4.

Ciao,
Dscho

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 5/8] rebase: introduce the --recreate-merges option
  2018-02-12 20:39         ` Johannes Schindelin
@ 2018-02-13  4:39           ` Jacob Keller
  2018-02-13  7:15             ` Sergey Organov
  2018-02-13  6:43           ` Sergey Organov
  1 sibling, 1 reply; 276+ messages in thread
From: Jacob Keller @ 2018-02-13  4:39 UTC (permalink / raw)
  To: Johannes Schindelin; +Cc: Sergey Organov, Git mailing list, Junio C Hamano

On Mon, Feb 12, 2018 at 12:39 PM, Johannes Schindelin
<Johannes.Schindelin@gmx.de> wrote:
> Hi Sergey,
>
> On Mon, 12 Feb 2018, Sergey Organov wrote:
>> > Have a look at https://github.com/git/git/pull/447, especially the
>> > latest commit in there which is an early version of the deprecation I
>> > intend to bring about.
>>
>> You shouldn't want a deprecation at all should you have re-used
>> --preserve-merges in the first place, and I still don't see why you
>> haven't.
>
> Keep repeating it, and it won't become truer.
>
> If you break formats, you break scripts. Git has *so* many users, there
> are very likely some who script *every* part of it.
>
> We simply cannot do that.
>
> What we can is deprecate designs which we learned on the way were not only
> incomplete from the get-go, but bad overall and hard (or impossible) to
> fix. Like --preserve-merges.
>
> Or for that matter like the design you proposed, to use --first-parent for
> --recreate-merges. Or to use --first-parent for some --recreate-merges,
> surprising users in very bad ways when it is not used (or when it is
> used). I get the impression that you still think it would be a good idea,
> even if it should be obvious that it is not.

If we consider the addition of new todo list elements as "user
breaking", then yes this change would be user-script breaking.

Since we did not originally spell out that todo-list items are subject
to enhancement by addition of operations in the future, scripts are
likely not designed to allow addition of new elements.

Thus, adding recreate-merges, and deprecating preserve-merges, seems
to me to be the correct action to take here.

One could argue that users should have expected new todo list elements
to be added in the future and thus design their scripts to cope with
such a thing. If you can convincingly argue this, then I don't
necessarily see it as a complete user breaking change to fix
preserve-merges in order to allow it to handle re-ordering properly..

I think I lean towards agreeing with Johannes, and that adding
recreate-merges and removing preserve-merges is the better solution.

Thanks,
Jake

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 5/8] rebase: introduce the --recreate-merges option
  2018-02-12 20:39         ` Johannes Schindelin
  2018-02-13  4:39           ` Jacob Keller
@ 2018-02-13  6:43           ` Sergey Organov
  2018-02-15  1:40             ` Johannes Schindelin
  1 sibling, 1 reply; 276+ messages in thread
From: Sergey Organov @ 2018-02-13  6:43 UTC (permalink / raw)
  To: Johannes Schindelin; +Cc: git, Junio C Hamano, Jacob Keller

Hi Johannes,

Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:
> Hi Sergey,
>
> On Mon, 12 Feb 2018, Sergey Organov wrote:
>
>> Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:
>> >
>> > On Fri, 9 Feb 2018, Sergey Organov wrote:
>> >
>> >> Johannes Schindelin <johannes.schindelin@gmx.de> writes:
>> >> 
>> >> [...]
>> >> 
>> >> > With this patch, the goodness of the Git garden shears comes to `git
>> >> > rebase -i` itself. Passing the `--recreate-merges` option will generate
>> >> > a todo list that can be understood readily, and where it is obvious
>> >> > how to reorder commits. New branches can be introduced by inserting
>> >> > `label` commands and calling `merge - <label> <oneline>`. And once this
>> >> > mode has become stable and universally accepted, we can deprecate the
>> >> > design mistake that was `--preserve-merges`.
>> >> 
>> >> This doesn't explain why you introduced this new --recreate-merges. Why
>> >> didn't you rather fix --preserve-merges to generate and use new todo
>> >> list format?
>> >
>> > Because that would of course break existing users of
>> > --preserve-merges.
>> 
>> How exactly?
>
> Power users of interactive rebase use scripting to augment Git's
> functionality. One particularly powerful trick is to override
> GIT_SEQUENCER_EDITOR with an invocation of such a script, to perform
> automated edits. Such a script breaks when we change the format of the
> content to edit. If we change the format of the todo list generated in
> --preserve-merges mode, that is exactly what happens. We break existing
> users.

I didn't say a word against "--preserve-merges mode", whatever it is,
only about re-using "--preserve-merges" command-line option to "git
rebase", the git user interface. I'm sure you see the difference? Unless
there are out-of-git scripts that do use "git rebase --preserve-merges"
and simultaneously do rely on the todo list format this exact command
generates, there should be no breakage of existing users caused by
changing todo list format generated by  "git rebase --preserve-merges".

Old broken "--preserve-merges mode" could be then kept in the
implementation for ages, unused by the new fixed "git rebase
--preserve-merge", for the sake of compatibility.

> BTW it seems that you did not really read my previous reply carefully
> because I referenced such a use case: the Git garden shears.

I thought I did. You confirm below that this script doesn't use "git
rebase --preserve-merges" in the first place, nor will it break if "git
rebase --preserve-merges" starts to generate new todo format, yet you
expected I'd readily see how it's relevant? No, I'm not that clever, nor
am I a mind-reader.

> They do override the sequencer editor, and while they do not exactly
> edit the todo list (they simply through the generated one away), they
> generate a new todo list and would break if that format changes. Of
> course, the shears do not use the --preserve-merges mode,
> but from just reading about the way how the Git garden shears work, it
> is quite obvious how similar users of --preserve-merges are likely to
> exist?

Maybe, I dunno. If even "garden shears" won't break, then what will? Do
you know an example?

Anyway, as it seems it's too late already for such a change, let me stop
this and assume there are indeed such scripts that will break and that
it's indeed a good idea to introduce new option. Case closed. The manual
should still be fixed though, I think.

>> Doesn't "--recreate-merges" produce the same result as
>> "--preserve-merges" if run non-interactively?
>
> The final result of a rebase where you do not edit the todo list? Should
> be identical, indeed.

That's good to hear.

> But that is the most boring, most uninteresting, and least important use
> case.

For you. Do you suddenly stop caring about compatibility?

> So we might just as well forget about it when we focus on keeping
> Git's usage stable.

Why? It's good it behaves the same, so --preserve-merges could indeed be
deprecated, as you apparently intend.

>> > So why not --preserve-merges=v2? Because that would force me to
>> > maintain --preserve-merges forever. And I don't want to.
>> >
>> >> It doesn't seem likely that todo list created by one Git version is
>> >> to be ever used by another, right?
>> >
>> > No. But by scripts based on `git rebase -p`.
>> >
>> >> Is there some hidden reason here? Some tools outside of Git that use
>> >> old todo list format, maybe?
>> >
>> > Exactly.
>> >
>> > I did mention such a tool: the Git garden shears:
>> >
>> > 	https://github.com/git-for-windows/build-extra/blob/master/shears.sh
>> >
>> > Have a look at it. It will inform the discussion.
>> 
>> I've searched for "-p" in the script, but didn't find positives for
>> either "-p" or "--preserve-merges". How it would break if it doesn't use
>> them? What am I missing?
>
> *This* particular script does not use -p.
>
> But it is not *this* particular script that I do not want to break!

I thought that was an example of a tool that would break. Well, it won't
break. Good.

> It is *all* scripts that use interactive rebase!

I'm really interested, and here I *do* ask for education. What are
those? As I now only ask this out of curiosity, and don't argue
--recreate-merges anymore, are you finally willing to reveal the
information?

> Don't you also care about not breaking existing users?

I do care. I just suspected they are very unlikely to exist, and I do
want to be educated in this matter indeed, as they could be rather
interesting.

[Please notice violation of your own standard of not using "Don't
you...", not that I care myself.]

>> >> Then, if new option indeed required, please look at the resulting manual:
>> >> 
>> >> --recreate-merges::
>> >> 	Recreate merge commits instead of flattening the history by replaying
>> >> 	merges. Merge conflict resolutions or manual amendments to merge
>> >> 	commits are not preserved.
>> >> 
>> >> -p::
>> >> --preserve-merges::
>> >> 	Recreate merge commits instead of flattening the history by replaying
>> >> 	commits a merge commit introduces. Merge conflict resolutions or manual
>> >> 	amendments to merge commits are not preserved.
>> >
>> > As I stated in the cover letter, there are more patches lined up after
>> > this patch series.
>> 
>> Good, but I thought this one should better be self-consistent anyway.
>> What if those that come later aren't included?
>
> Right, let's just rip apart the partial progress because the latter
> patches might not make it in?

No, let's fix it instead.

>
> I cannot work on that basis, and I also do not want to work on that basis.
>
> If you do not like how the documentation is worded, fine, suggest a better
> alternative.

I suggested to re-use --preserve-merges command-line option to "git
rebase", unless there are actual users that would break. But as you
believe that's wrong idea, then it could be something like this in the
manual:

--recreate-merges::
	Recreate merge commits instead of flattening the history. Merge
	conflict resolutions or manual amendments to merge commits are
	not preserved. 

-p::
--preserve-merges::
	This option is similar to --recreate-merges, but doesn't
        support interactive mode properly. This option is deprecated,
        use --recreate-merges instead.

>
>> > Have a look at https://github.com/git/git/pull/447, especially the
>> > latest commit in there which is an early version of the deprecation I
>> > intend to bring about.
>> 
>> You shouldn't want a deprecation at all should you have re-used
>> --preserve-merges in the first place, and I still don't see why you
>> haven't. 
>
> Keep repeating it, and it won't become truer.

It is just my point that I repeat, and you gave no evidence it is false,
so I assume it's true, unless proved otherwise.

[...]

> Or for that matter like the design you proposed, to use --first-parent for
> --recreate-merges. Or to use --first-parent for some --recreate-merges,
> surprising users in very bad ways when it is not used (or when it is
> used). I get the impression that you still think it would be a good idea,
> even if it should be obvious that it is not.

What you describe here is bad idea indeed, but it has little to do with
what I actually have in mind and what you apparently don't want to even
try to understand.

>> > Also, please refrain from saying things like... "Don't you think ..."
>> >
>> > If you don't like the wording, I wold much more appreciate it if a better
>> > alternative was suggested.
>> 
>> Sorry, but how can I suggest one if I don't understand what you are
>> doing here in the first place? That's why I ask you.
>
> There are ways to put the person you ask on trial. And there are ways to
> genuinely show interest and seek education.

I didn't seek education, nor did I intend any trial. I asked for
clarification of the patch to the manual page that you wrote in a way
that made resulting manual page confusing for me. Confusing manual is
often indication of some additional problem(s) elsewhere, that's what
I've learned for sure, from multiple occasions, so I did reveal my
doubts.

> I am a really poor example how to communicate properly, of course, so
> don't try to learn from me. I am trying myself to learn better ways to
> express what I mean clearly, and to express it in a direct yet kind
> manner.
>
>> >> Don't you think more explanations are needed there in the manual on
>> >> why do we have 2 separate options with almost the same yet subtly
>> >> different description? Is this subtle difference even important? How?
>> >> 
>> >> I also have trouble making sense of "Recreate merge commits instead of
>> >> flattening the history by replaying merges." Is it "<Recreate merge
>> >> commits by replaying merges> instead of <flattening the history>" or is it
>> >> rather "<Recreate merge commits> instead of <flattening the history by
>> >> replaying merges>?
>> >
>> > The documentation of the --recreate-merges option is not meant to explain
>> > the difference to --preserve-merges. It is meant to explain the difference
>> > to regular `git rebase -i`, which flattens the commit history into a
>> > single branch without merge commits (in fact, all merge commits are simply
>> > ignored).
>> 
>> Yeah, that's obvious, but the point is that resulting manual is ended
>> up being confusing.
>
> Again, just saying something is bad, is bad. Saying something leaves room
> for improvement and then suggesting how to improve it, is good.

Please see wording suggestion above.

>> > And I would rather not start to describe the difference between
>> > --recreate-merges and --preserve-merges because I want to deprecate the
>> > latter, and describing the difference as I get the sense is your wish
>> > would simply mean more work because it would have to be added and then
>> > removed again.
>> 
>> I suspect you actually didn't need those new option in the first place,
>> and that's the core reason of these troubles.
>
> Are you suspecting that I, myself, do not use --recreate-merges?

I suspect that if you've had rather changed --preserve-merges, you'd
happily use it and no --recreate-merges were ever necessary. You did
what you did, and it seems to be too late to ask for changing it back,
exactly due to heavy use of this new option.

> If so, please read the cover letter again, in particular the part where I
> describe how this entire series of patch series arose from the Git garden
> shears, which I invented myself to help with maintaining Git for Windows,
> and which I use for five years now. This should help disperse that
> suspicion rather quickly: the intent of --recreate-merges is to allow me
> to simplify the shears by quite a bit, and maybe eventually even get rid
> of the script altogether (if I ever manage to convince myself that the
> concept of a merging-rebase should be official enough to enter core Git).
>
> I am a heavy user of --recreate-merges, even if it does not really exist
> yet. I have five years of experience with it, which is the reason why I am
> so confident about its design, and why I can tell you a lot about typical
> use cases and common pitfalls, and where the original design had to be
> adjusted.

I fail to see how anything of the above would change should
--recreate-merges be still called --preserve-merges, but I do see why
you don't want that to happen now, so please only consider fixing of the
manual page.

-- Sergey

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 5/8] rebase: introduce the --recreate-merges option
  2018-02-12 20:21         ` Johannes Schindelin
@ 2018-02-13  6:44           ` Sergey Organov
  2018-02-15  1:08             ` Johannes Schindelin
  0 siblings, 1 reply; 276+ messages in thread
From: Sergey Organov @ 2018-02-13  6:44 UTC (permalink / raw)
  To: Johannes Schindelin; +Cc: git, Junio C Hamano, Jacob Keller

Hi Johannes,

Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:
> Hi Sergey,
>
> On Mon, 12 Feb 2018, Sergey Organov wrote:
>
>> Thanks for explanations, and could you please answer this one:
>> 
>> [...]
>> 
>> >> I also have trouble making sense of "Recreate merge commits instead of
>> >> flattening the history by replaying merges." Is it "<Recreate merge
>> >> commits by replaying merges> instead of <flattening the history>" or is it
>> >> rather "<Recreate merge commits> instead of <flattening the history by
>> >> replaying merges>?
>
> I thought I had answered that one.

No, not really, but now you did, please see below.

>
> Flattening the history is what happens in regular rebase (i.e. without
> --recreate-merges and without --preserve-merges).
>
> The idea to recreate merges is of course to *not* flatten the history.

Sure. Never supposed it is.

> Maybe there should have been a comma after "history" to clarify what the
> sentence means.

That's the actual answer to my question, but it in turn raises another
one: why did you change wording of --preserve-merges description for
this new option?

> The wording is poor either way, but you are also not a native speaker so
> we have to rely on, say, Eric to help us out here.

Likely, but why didn't you keep original wording from --preserve-merges?
Do you feel it's somehow poor either?

Anyway, please also refer to wording suggestion in the another (lengthy)
answer in this thread.

-- Sergey

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 5/8] rebase: introduce the --recreate-merges option
  2018-02-13  4:39           ` Jacob Keller
@ 2018-02-13  7:15             ` Sergey Organov
  2018-02-14  1:35               ` Jacob Keller
  0 siblings, 1 reply; 276+ messages in thread
From: Sergey Organov @ 2018-02-13  7:15 UTC (permalink / raw)
  To: Jacob Keller; +Cc: Johannes Schindelin, Git mailing list, Junio C Hamano

Hi Jake,

Jacob Keller <jacob.keller@gmail.com> writes:

> On Mon, Feb 12, 2018 at 12:39 PM, Johannes Schindelin
> <Johannes.Schindelin@gmx.de> wrote:
>> Hi Sergey,
>>
>> On Mon, 12 Feb 2018, Sergey Organov wrote:
>>> > Have a look at https://github.com/git/git/pull/447, especially the
>>> > latest commit in there which is an early version of the deprecation I
>>> > intend to bring about.
>>>
>>> You shouldn't want a deprecation at all should you have re-used
>>> --preserve-merges in the first place, and I still don't see why you
>>> haven't.
>>
>> Keep repeating it, and it won't become truer.
>>
>> If you break formats, you break scripts. Git has *so* many users, there
>> are very likely some who script *every* part of it.
>>
>> We simply cannot do that.
>>
>> What we can is deprecate designs which we learned on the way were not only
>> incomplete from the get-go, but bad overall and hard (or impossible) to
>> fix. Like --preserve-merges.
>>
>> Or for that matter like the design you proposed, to use --first-parent for
>> --recreate-merges. Or to use --first-parent for some --recreate-merges,
>> surprising users in very bad ways when it is not used (or when it is
>> used). I get the impression that you still think it would be a good idea,
>> even if it should be obvious that it is not.
>
> If we consider the addition of new todo list elements as "user
> breaking", then yes this change would be user-script breaking.

It _is_ user script breaking, provided such script exists. Has anybody
actually seen one? Not that it's wrong to be extra-cautious about it,
just curios. Note that to be actually affected, such a script must
invoke "git rebase -p" _command_ and then tweak its todo output to
produce outcome.

> Since we did not originally spell out that todo-list items are subject
> to enhancement by addition of operations in the future, scripts are
> likely not designed to allow addition of new elements.

Out of curiosity, are you going to spell it now, for the new todo
format?

> Thus, adding recreate-merges, and deprecating preserve-merges, seems
> to me to be the correct action to take here.

Yes, sure, provided there is actual breakage, or at least informed
suspicion there is one.

> One could argue that users should have expected new todo list elements
> to be added in the future and thus design their scripts to cope with
> such a thing. If you can convincingly argue this, then I don't
> necessarily see it as a complete user breaking change to fix
> preserve-merges in order to allow it to handle re-ordering properly..

I'd not argue this way myself. If there are out-of-git-tree non-human
users that accept and tweak todo _generated_ by current "git rebase -p"
_command_, I also vote for a new option.

> I think I lean towards agreeing with Johannes, and that adding
> recreate-merges and removing preserve-merges is the better solution.

On these grounds it is, no objections.

-- Sergey

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 5/8] rebase: introduce the --recreate-merges option
  2018-02-13  7:15             ` Sergey Organov
@ 2018-02-14  1:35               ` Jacob Keller
  2018-02-15  1:14                 ` Johannes Schindelin
  0 siblings, 1 reply; 276+ messages in thread
From: Jacob Keller @ 2018-02-14  1:35 UTC (permalink / raw)
  To: Sergey Organov; +Cc: Johannes Schindelin, Git mailing list, Junio C Hamano

On Mon, Feb 12, 2018 at 11:15 PM, Sergey Organov <sorganov@gmail.com> wrote:
> Hi Jake,
>
> Jacob Keller <jacob.keller@gmail.com> writes:
>
>> On Mon, Feb 12, 2018 at 12:39 PM, Johannes Schindelin
>> <Johannes.Schindelin@gmx.de> wrote:
>>> Hi Sergey,
>>>
>>> On Mon, 12 Feb 2018, Sergey Organov wrote:
>>>> > Have a look at https://github.com/git/git/pull/447, especially the
>>>> > latest commit in there which is an early version of the deprecation I
>>>> > intend to bring about.
>>>>
>>>> You shouldn't want a deprecation at all should you have re-used
>>>> --preserve-merges in the first place, and I still don't see why you
>>>> haven't.
>>>
>>> Keep repeating it, and it won't become truer.
>>>
>>> If you break formats, you break scripts. Git has *so* many users, there
>>> are very likely some who script *every* part of it.
>>>
>>> We simply cannot do that.
>>>
>>> What we can is deprecate designs which we learned on the way were not only
>>> incomplete from the get-go, but bad overall and hard (or impossible) to
>>> fix. Like --preserve-merges.
>>>
>>> Or for that matter like the design you proposed, to use --first-parent for
>>> --recreate-merges. Or to use --first-parent for some --recreate-merges,
>>> surprising users in very bad ways when it is not used (or when it is
>>> used). I get the impression that you still think it would be a good idea,
>>> even if it should be obvious that it is not.
>>
>> If we consider the addition of new todo list elements as "user
>> breaking", then yes this change would be user-script breaking.
>
> It _is_ user script breaking, provided such script exists. Has anybody
> actually seen one? Not that it's wrong to be extra-cautious about it,
> just curios. Note that to be actually affected, such a script must
> invoke "git rebase -p" _command_ and then tweak its todo output to
> produce outcome.
>
>> Since we did not originally spell out that todo-list items are subject
>> to enhancement by addition of operations in the future, scripts are
>> likely not designed to allow addition of new elements.
>
> Out of curiosity, are you going to spell it now, for the new todo
> format?
>
>> Thus, adding recreate-merges, and deprecating preserve-merges, seems
>> to me to be the correct action to take here.
>
> Yes, sure, provided there is actual breakage, or at least informed
> suspicion there is one.
>
>> One could argue that users should have expected new todo list elements
>> to be added in the future and thus design their scripts to cope with
>> such a thing. If you can convincingly argue this, then I don't
>> necessarily see it as a complete user breaking change to fix
>> preserve-merges in order to allow it to handle re-ordering properly..
>
> I'd not argue this way myself. If there are out-of-git-tree non-human
> users that accept and tweak todo _generated_ by current "git rebase -p"
> _command_, I also vote for a new option.
>

To be fair, I have not seen anything that actually reads the todo list
and tweaks it in such a manner. The closest example is the git garden
shears script, which simply replaces the todo list.

It's certainly *possible* that such a script would exist though,

Thanks,
Jake

>> I think I lean towards agreeing with Johannes, and that adding
>> recreate-merges and removing preserve-merges is the better solution.
>
> On these grounds it is, no objections.
>
> -- Sergey

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 5/8] rebase: introduce the --recreate-merges option
  2018-02-08 12:34           ` Johannes Schindelin
@ 2018-02-14  5:41             ` Sergey Organov
  0 siblings, 0 replies; 276+ messages in thread
From: Sergey Organov @ 2018-02-14  5:41 UTC (permalink / raw)
  To: Johannes Schindelin; +Cc: Junio C Hamano, Øyvind Rønningstad, git, Jacob Keller, Johannes Sixt

Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:
[...]
> Just to give you one concrete example: when I recently rebased some
> patches (no reording or dropping involved here!) and one of the picks
> failed with merge conflicts, I realized that that particular commit
> introduced incorrect formatting and fixed that right away (verifying that
> no other commits introduced incorrect formatting, of course).
>
> With your new cute idea to magically cherry-pick -m1, this change would
> have been magically dropped from the subsequent merge commits!

You put it as if the problem you describe is unsolvable short of getting
back to your favorite blind re-merge. Do you really believe it?

I thought it's obvious that I originally meant "cherry-pick -m1" to be
an explanation facility, a proof of concept, not the final answer to all
the problems of history editing. It's a nice base for actually
approaching these problems though, unlike blind re-merge currently being
used, the latter having no potential.

The fact that bare naked "cherry-pick -m1" doesn't do what is often[1]
required in such cases neither voids the general idea of reproducing
merge-the-result, nor does it make current re-merge approach less
broken.

[1] Please take into consideration that it's _not always_ the case that
one needs a change made to a side-branch to actually propagate to the
main-line over the merge (think "merge -x ours", or something similar
but not that simple), and then it's rather the cute idea to blindly
re-merge that will wreak havoc, as in a lot of other cases.

-- Sergey

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 5/8] rebase: introduce the --recreate-merges option
  2018-02-13  6:44           ` Sergey Organov
@ 2018-02-15  1:08             ` Johannes Schindelin
  2018-02-15  4:28               ` Sergey Organov
  0 siblings, 1 reply; 276+ messages in thread
From: Johannes Schindelin @ 2018-02-15  1:08 UTC (permalink / raw)
  To: Sergey Organov; +Cc: git, Junio C Hamano, Jacob Keller

Hi,

On Tue, 13 Feb 2018, Sergey Organov wrote:

> Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:
> 
> > The wording is poor either way, but you are also not a native speaker so
> > we have to rely on, say, Eric to help us out here.
> 
> Likely, but why didn't you keep original wording from --preserve-merges?
> Do you feel it's somehow poor either?

Yes, I felt it is poor, especially when --recreate-merges is present, that
is indeed why I changed it.

Ciao,
Johannes

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 5/8] rebase: introduce the --recreate-merges option
  2018-02-14  1:35               ` Jacob Keller
@ 2018-02-15  1:14                 ` Johannes Schindelin
  2018-02-15  4:35                   ` Sergey Organov
  0 siblings, 1 reply; 276+ messages in thread
From: Johannes Schindelin @ 2018-02-15  1:14 UTC (permalink / raw)
  To: Jacob Keller; +Cc: Sergey Organov, Git mailing list, Junio C Hamano

Hi Jake,

On Tue, 13 Feb 2018, Jacob Keller wrote:

> On Mon, Feb 12, 2018 at 11:15 PM, Sergey Organov <sorganov@gmail.com> wrote:
> >
> > Jacob Keller <jacob.keller@gmail.com> writes:
> >
> >> On Mon, Feb 12, 2018 at 12:39 PM, Johannes Schindelin
> >> <Johannes.Schindelin@gmx.de> wrote:
> >>>
> >>> On Mon, 12 Feb 2018, Sergey Organov wrote:
> >>>> > Have a look at https://github.com/git/git/pull/447, especially the
> >>>> > latest commit in there which is an early version of the deprecation I
> >>>> > intend to bring about.
> >>>>
> >>>> You shouldn't want a deprecation at all should you have re-used
> >>>> --preserve-merges in the first place, and I still don't see why you
> >>>> haven't.
> >>>
> >>> Keep repeating it, and it won't become truer.
> >>>
> >>> If you break formats, you break scripts. Git has *so* many users, there
> >>> are very likely some who script *every* part of it.
> >>>
> >>> We simply cannot do that.
> >>>
> >>> What we can is deprecate designs which we learned on the way were not only
> >>> incomplete from the get-go, but bad overall and hard (or impossible) to
> >>> fix. Like --preserve-merges.
> >>>
> >>> Or for that matter like the design you proposed, to use --first-parent for
> >>> --recreate-merges. Or to use --first-parent for some --recreate-merges,
> >>> surprising users in very bad ways when it is not used (or when it is
> >>> used). I get the impression that you still think it would be a good idea,
> >>> even if it should be obvious that it is not.
> >>
> >> If we consider the addition of new todo list elements as "user
> >> breaking", then yes this change would be user-script breaking.
> >
> > It _is_ user script breaking, provided such script exists. Has anybody
> > actually seen one? Not that it's wrong to be extra-cautious about it,
> > just curios. Note that to be actually affected, such a script must
> > invoke "git rebase -p" _command_ and then tweak its todo output to
> > produce outcome.
> >
> >> Since we did not originally spell out that todo-list items are subject
> >> to enhancement by addition of operations in the future, scripts are
> >> likely not designed to allow addition of new elements.
> >
> > Out of curiosity, are you going to spell it now, for the new todo
> > format?
> >
> >> Thus, adding recreate-merges, and deprecating preserve-merges, seems
> >> to me to be the correct action to take here.
> >
> > Yes, sure, provided there is actual breakage, or at least informed
> > suspicion there is one.
> >
> >> One could argue that users should have expected new todo list elements
> >> to be added in the future and thus design their scripts to cope with
> >> such a thing. If you can convincingly argue this, then I don't
> >> necessarily see it as a complete user breaking change to fix
> >> preserve-merges in order to allow it to handle re-ordering properly..
> >
> > I'd not argue this way myself. If there are out-of-git-tree non-human
> > users that accept and tweak todo _generated_ by current "git rebase -p"
> > _command_, I also vote for a new option.
> >
> 
> To be fair, I have not seen anything that actually reads the todo list
> and tweaks it in such a manner. The closest example is the git garden
> shears script, which simply replaces the todo list.
> 
> It's certainly *possible* that such a script would exist though,

We actually know of such scripts.

Remember how rewriting parts of rebase -i in C broke somebody's script
because the todo list was not re-read after a successful `exec`?

Guess three times why that script was broken? Precisely: it modified the
todo list!

To see the fix (and the explanation) in all its glory, just have a look at
54fd3243dae (rebase -i: reread the todo list if `exec` touched it,
2017-04-26).

And even if we did not know about any user. What does that mean? Does it
mean that there is no such user? Or does it not rather mean that our
imagination is rather limited, but we *still* should practice safe
software development and use the totally appropriate vehicle of
deprecating, rather than replacing, functionality?

Obviously, the latter option is what I favor, that's why I suggested it in
the first place.

Ciao,
Dscho

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 5/8] rebase: introduce the --recreate-merges option
  2018-02-13  6:43           ` Sergey Organov
@ 2018-02-15  1:40             ` Johannes Schindelin
  0 siblings, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-02-15  1:40 UTC (permalink / raw)
  To: Sergey Organov; +Cc: git, Junio C Hamano, Jacob Keller

Hi Sergey,

On Tue, 13 Feb 2018, Sergey Organov wrote:

> Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:
> >
> > On Mon, 12 Feb 2018, Sergey Organov wrote:
> >
> >> Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:
> >> >
> >> > On Fri, 9 Feb 2018, Sergey Organov wrote:
> >> >
> >> >> Johannes Schindelin <johannes.schindelin@gmx.de> writes:
> >> >> 
> >> >> [...]
> >> >> 
> >> >> > With this patch, the goodness of the Git garden shears comes to `git
> >> >> > rebase -i` itself. Passing the `--recreate-merges` option will generate
> >> >> > a todo list that can be understood readily, and where it is obvious
> >> >> > how to reorder commits. New branches can be introduced by inserting
> >> >> > `label` commands and calling `merge - <label> <oneline>`. And once this
> >> >> > mode has become stable and universally accepted, we can deprecate the
> >> >> > design mistake that was `--preserve-merges`.
> >> >> 
> >> >> This doesn't explain why you introduced this new --recreate-merges. Why
> >> >> didn't you rather fix --preserve-merges to generate and use new todo
> >> >> list format?
> >> >
> >> > Because that would of course break existing users of
> >> > --preserve-merges.
> >> 
> >> How exactly?
> >
> > Power users of interactive rebase use scripting to augment Git's
> > functionality. One particularly powerful trick is to override
> > GIT_SEQUENCER_EDITOR with an invocation of such a script, to perform
> > automated edits. Such a script breaks when we change the format of the
> > content to edit. If we change the format of the todo list generated in
> > --preserve-merges mode, that is exactly what happens. We break existing
> > users.
> 
> I didn't say a word against "--preserve-merges mode", whatever it is,
> only about re-using "--preserve-merges" command-line option to "git
> rebase", the git user interface.

*I* said something against --preserve-merges. You did not even need to. I
know fully well its limitations.

I also said something agains the suggestion to replace the functionality
of a previously well-defined (although misdesigned) feature.

I do not know how often I have to repeat that your suggestion would break
backwards-compatibility?

> I'm sure you see the difference?

Yes, of course I do, and you do not even have to suggest otherwise by
asking such a question.

I already demonstrated plenty of times that I do understand what you wish
for, and that I see serious problems with it.

> Unless there are out-of-git scripts that do use "git rebase
> --preserve-merges" and simultaneously do rely on the todo list format
> this exact command generates, there should be no breakage of existing
> users caused by changing todo list format generated by  "git rebase
> --preserve-merges".

So. Just because you cannot imagine that anybody uses rebase in such a
powerful way means you are willing to break their setups?

Git is used by millions of users. Many of them are power users. It would
be quite naive to assume that nobody uses rebase -p in a scripted manner
that modifies the todo list.

Changing the behavior of --preserve-merges would be simply irresponsible,
and that's why we won't do it.

Even if that was not so, there is yet another really good reason not to
reuse the name --preserve-merges: The name itself suggests that this mode
is about preserving all merges in the specified commit range. That was its
original intention, too, as I never designed it to be user with rebase -i.
If the todo list of rebase -p is not modified (preserving the entire
commit topology as well as possible), it works quite well.

The new mode is not so much about preserving, though. It is about
interactively modifying the todo list, to change the order of the commits,
even to change the branch topology. That means that we do not necessarily
preserve the merges. We recreate them. So you see, I did try to be careful
about the naming, too. I thought about this.

> Old broken "--preserve-merges mode" could be then kept in the
> implementation for ages, unused by the new fixed "git rebase
> --preserve-merge", for the sake of compatibility.

This sentence contradicts itself. Either you keep the code unused, or you
keep it used for backwards-compatibility.

> > BTW it seems that you did not really read my previous reply carefully
> > because I referenced such a use case: the Git garden shears.
> 
> I thought I did. You confirm below that this script doesn't use "git
> rebase --preserve-merges" in the first place, nor will it break if "git
> rebase --preserve-merges" starts to generate new todo format, yet you
> expected I'd readily see how it's relevant? No, I'm not that clever, nor
> am I a mind-reader.

You caught me. I am not a user of --preserve-merges. Not anymore.

Does that mean that by extension nobody is a user of that feature?

Certainly not.

And does my example of (ab-)using interactive rebase by scripting on top
of it maybe suggest that others do the same? Maybe even with
--preserve-merges? Most likely. Git is used by many, many users. It would
be foolish to make any assumption about how Git is used by others.

> > They do override the sequencer editor, and while they do not exactly
> > edit the todo list (they simply through the generated one away), they
> > generate a new todo list and would break if that format changes. Of
> > course, the shears do not use the --preserve-merges mode, but from
> > just reading about the way how the Git garden shears work, it is quite
> > obvious how similar users of --preserve-merges are likely to exist?
> 
> Maybe, I dunno. If even "garden shears" won't break, then what will? Do
> you know an example?

You are not seriously suggesting that we should assume that there is no
such Git user, just because neither you nor I personally know such a user?
Seriously?

> Anyway, as it seems it's too late already for such a change, let me stop
> this and assume there are indeed such scripts that will break and that
> it's indeed a good idea to introduce new option. Case closed. The manual
> should still be fixed though, I think.

Finally I got through. Yes, we cannot break backwards-compatibility.

> >> Doesn't "--recreate-merges" produce the same result as
> >> "--preserve-merges" if run non-interactively?
> >
> > The final result of a rebase where you do not edit the todo list? Should
> > be identical, indeed.
> 
> That's good to hear.
> 
> > But that is the most boring, most uninteresting, and least important use
> > case.
> 
> For you. Do you suddenly stop caring about compatibility?

What does "fun" and "interesting" have to do with compatibility?

Yes, to me, this case is boring. And yes, I took pains to make it work
(for compatibility).

And yes, I did not stop after that. After the boring case, I still wanted
to think things through, to come up with a design that would not be too
limited to be useful. With a design that is consistent.

If you find holes in the consistency or usability, please do call them
out.

But please stop suggesting to break backwards-compatibility, or to
introduce features that are inconsistent and/or can produce "surprising"
results (as --first-parent would, and multiple contributors had to argue
in concert, pointing out how it is not extensible to the general case, and
is hence consistent).

> > It is *all* scripts that use interactive rebase!
> 
> I'm really interested, and here I *do* ask for education. What are
> those? As I now only ask this out of curiosity, and don't argue
> --recreate-merges anymore, are you finally willing to reveal the
> information?

If you are interested, why don't you go about asking people for their
power scripts.

In the context of this patch series, I am not interested in such a
collection. What I had to do was to convince myself that they could not
exist, in which case I could just do away with backwards-compatibility. In
the alternative, the safe play is to go the deprecation route.

A mere "highly unlikely" made up from thin air does not convince me,
though. So deprecation route it is.

> > Don't you also care about not breaking existing users?
> 
> I do care. I just suspected they are very unlikely to exist, and I do
> want to be educated in this matter indeed, as they could be rather
> interesting.

Okay, "very unlikely". Not "highly unlikely". Still, it is an unconvincing
argument that suffers very seriously from lack of any robust evidence.

> [Please notice violation of your own standard of not using "Don't
> you...", not that I care myself.]

True. My apologies.

And as the rest of the mail seems to reiterate the idea that the
--recreate-merges code should override --preserve-merges (breaking
backwards-compatibility), despite my repeated efforts to educate you why
this would be a bad idea, I guess the best course of action to avoid
telling you "Don't you ..." is to just stop here.

Ciao,
Johannes

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 5/8] rebase: introduce the --recreate-merges option
  2018-02-15  1:08             ` Johannes Schindelin
@ 2018-02-15  4:28               ` Sergey Organov
  2018-02-15 16:51                 ` Johannes Schindelin
  0 siblings, 1 reply; 276+ messages in thread
From: Sergey Organov @ 2018-02-15  4:28 UTC (permalink / raw)
  To: Johannes Schindelin; +Cc: git, Junio C Hamano, Jacob Keller

Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:

> Hi,
>
> On Tue, 13 Feb 2018, Sergey Organov wrote:
>
>> Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:
>> 
>> > The wording is poor either way, but you are also not a native speaker so
>> > we have to rely on, say, Eric to help us out here.
>> 
>> Likely, but why didn't you keep original wording from --preserve-merges?
>> Do you feel it's somehow poor either?
>
> Yes, I felt it is poor, especially when --recreate-merges is present, that
> is indeed why I changed it.

So, how about this (yeah, I noticed the option now got arguments, but
please, tweak this to the new implementation yourself):

--recreate-merges::
	Recreate merge commits instead of flattening the history. Merge
	conflict resolutions or manual amendments to merge commits are
	not preserved. 

-p::
--preserve-merges::
	(deprecated) This option is similar to --recreate-merges. It has
        no proper support for interactive mode and thus is deprecated.
        Use '--recreate-merges' instead.


-- Sergey

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 5/8] rebase: introduce the --recreate-merges option
  2018-02-15  1:14                 ` Johannes Schindelin
@ 2018-02-15  4:35                   ` Sergey Organov
  2018-02-15 16:50                     ` Johannes Schindelin
  0 siblings, 1 reply; 276+ messages in thread
From: Sergey Organov @ 2018-02-15  4:35 UTC (permalink / raw)
  To: Johannes Schindelin; +Cc: Jacob Keller, Git mailing list, Junio C Hamano

Hi Johannes,
Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:

[...]

>> > I'd not argue this way myself. If there are out-of-git-tree non-human
>> > users that accept and tweak todo _generated_ by current "git rebase -p"
>> > _command_, I also vote for a new option.
>> >
>> 
>> To be fair, I have not seen anything that actually reads the todo list
>> and tweaks it in such a manner. The closest example is the git garden
>> shears script, which simply replaces the todo list.
>> 
>> It's certainly *possible* that such a script would exist though,
>
> We actually know of such scripts.

Please consider to explain this in the description of the change. I
believe readers deserve an explanation of why you decided to invent new
option instead of fixing the old one, even if it were only a suspicion,
more so if it is confidence.

-- Sergey

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 5/8] rebase: introduce the --recreate-merges option
  2018-02-15  4:35                   ` Sergey Organov
@ 2018-02-15 16:50                     ` Johannes Schindelin
  0 siblings, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-02-15 16:50 UTC (permalink / raw)
  To: Sergey Organov; +Cc: Jacob Keller, Git mailing list, Junio C Hamano

Hi,

On Thu, 15 Feb 2018, Sergey Organov wrote:

> Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:
> 
> [...]
> 
> >> > I'd not argue this way myself. If there are out-of-git-tree non-human
> >> > users that accept and tweak todo _generated_ by current "git rebase -p"
> >> > _command_, I also vote for a new option.
> >> >
> >> 
> >> To be fair, I have not seen anything that actually reads the todo list
> >> and tweaks it in such a manner. The closest example is the git garden
> >> shears script, which simply replaces the todo list.
> >> 
> >> It's certainly *possible* that such a script would exist though,
> >
> > We actually know of such scripts.
> 
> Please consider to explain this in the description of the change. I
> believe readers deserve an explanation of why you decided to invent new
> option instead of fixing the old one, even if it were only a suspicion,
> more so if it is confidence.

I considered.

And since even the absence of this use case would *still* not be a
convincing case against keeping --preserve-merges backwards-compatible, I
will not mention it.

Just saying that --preserve-merges is not changed, in order to keep
backwards-compatibility, is plenty enough.

It probably already convinced the Git maintainer, who is very careful
about backwards-compatibility, and rightfully so.

Ciao,
Johannes

^ permalink raw reply	[flat|nested] 276+ messages in thread

* Re: [PATCH 5/8] rebase: introduce the --recreate-merges option
  2018-02-15  4:28               ` Sergey Organov
@ 2018-02-15 16:51                 ` Johannes Schindelin
  0 siblings, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-02-15 16:51 UTC (permalink / raw)
  To: Sergey Organov; +Cc: git, Junio C Hamano, Jacob Keller

Hi,

On Thu, 15 Feb 2018, Sergey Organov wrote:

> Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:
> 
> > On Tue, 13 Feb 2018, Sergey Organov wrote:
> >
> >> Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:
> >> 
> >> > The wording is poor either way, but you are also not a native speaker so
> >> > we have to rely on, say, Eric to help us out here.
> >> 
> >> Likely, but why didn't you keep original wording from --preserve-merges?
> >> Do you feel it's somehow poor either?
> >
> > Yes, I felt it is poor, especially when --recreate-merges is present, that
> > is indeed why I changed it.
> 
> So, how about this (yeah, I noticed the option now got arguments, but
> please, tweak this to the new implementation yourself):
> 
> --recreate-merges::
> 	Recreate merge commits instead of flattening the history. Merge
> 	conflict resolutions or manual amendments to merge commits are
> 	not preserved. 
> 
> -p::
> --preserve-merges::
> 	(deprecated) This option is similar to --recreate-merges. It has
>         no proper support for interactive mode and thus is deprecated.
>         Use '--recreate-merges' instead.

I still don't like either.

I want something different there: descriptions that are a bit more
self-contained, and only describe the differences to -i or
--preserve-merges in a second paragraph.

Don't worry about it, though, I don't think you or me are capable of a
good explanation. I will ask some native speakers I trust.

Ciao,
Johannes

^ permalink raw reply	[flat|nested] 276+ messages in thread

* [PATCH v4 00/12] rebase -i: offer to recreate merge commits
  2018-02-11  0:09   ` [PATCH v3 00/12] " Johannes Schindelin
                       ` (11 preceding siblings ...)
  2018-02-11  0:10     ` [PATCH v3 12/12] rebase -i: introduce --recreate-merges=[no-]rebase-cousins Johannes Schindelin
@ 2018-02-23 12:35     ` Johannes Schindelin
  2018-02-23 12:35       ` [PATCH v4 01/12] sequencer: avoid using errno clobbered by rollback_lock_file() Johannes Schindelin
                         ` (12 more replies)
  2018-02-26 21:29     ` [PATCH v5 " Johannes Schindelin
  13 siblings, 13 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-02-23 12:35 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Jacob Keller, Stefan Beller, Philip Oakley, Eric Sunshine, Phillip Wood

Once upon a time, I dreamt of an interactive rebase that would not
flatten branch structure, but instead recreate the commit topology
faithfully.

My original attempt was --preserve-merges, but that design was so
limited that I did not even enable it in interactive mode.

Subsequently, it *was* enabled in interactive mode, with the predictable
consequences: as the --preserve-merges design does not allow for
specifying the parents of merge commits explicitly, all the new commits'
parents are defined *implicitly* by the previous commit history, and
hence it is *not possible to even reorder commits*.

This design flaw cannot be fixed. Not without a complete re-design, at
least. This patch series offers such a re-design.

Think of --recreate-merges as "--preserve-merges done right". It
introduces new verbs for the todo list, `label`, `reset` and `merge`.
For a commit topology like this:

            A - B - C
              \   /
                D

the generated todo list would look like this:

            # branch D
            pick 0123 A
            label branch-point
            pick 1234 D
            label D

            reset branch-point
            pick 2345 B
            merge -C 3456 D # C

There are more patches in the pipeline, based on this patch series, but
left for later in the interest of reviewable patch series: one mini
series to use the sequencer even for `git rebase -i --root`, and another
one to add support for octopus merges to --recreate-merges.

Changes since v3:

- fixed a grammar error in "introduce the `merge` command"'s commit message.

- fixed a couple of resource leaks in safe_append() and do_reset(), pointed
  out by Eric Sunshine.


Johannes Schindelin (11):
  sequencer: avoid using errno clobbered by rollback_lock_file()
  sequencer: make rearrange_squash() a bit more obvious
  sequencer: introduce new commands to reset the revision
  sequencer: introduce the `merge` command
  sequencer: fast-forward merge commits, if possible
  rebase-helper --make-script: introduce a flag to recreate merges
  rebase: introduce the --recreate-merges option
  sequencer: make refs generated by the `label` command worktree-local
  sequencer: handle post-rewrite for merge commands
  pull: accept --rebase=recreate to recreate the branch topology
  rebase -i: introduce --recreate-merges=[no-]rebase-cousins

Stefan Beller (1):
  git-rebase--interactive: clarify arguments

 Documentation/config.txt               |   8 +
 Documentation/git-pull.txt             |   5 +-
 Documentation/git-rebase.txt           |  14 +-
 builtin/pull.c                         |  14 +-
 builtin/rebase--helper.c               |  13 +-
 builtin/remote.c                       |   2 +
 contrib/completion/git-completion.bash |   4 +-
 git-rebase--interactive.sh             |  22 +-
 git-rebase.sh                          |  16 +
 refs.c                                 |   3 +-
 sequencer.c                            | 742 ++++++++++++++++++++++++++++++++-
 sequencer.h                            |   7 +
 t/t3430-rebase-recreate-merges.sh      | 208 +++++++++
 13 files changed, 1027 insertions(+), 31 deletions(-)
 create mode 100755 t/t3430-rebase-recreate-merges.sh


base-commit: e3a80781f5932f5fea12a49eb06f3ade4ed8945c
Published-As: https://github.com/dscho/git/releases/tag/recreate-merges-v4
Fetch-It-Via: git fetch https://github.com/dscho/git recreate-merges-v4

Interdiff vs v3:
 diff --git a/Documentation/config.txt b/Documentation/config.txt
 index f57e9cf10ca..8c9adea0d0c 100644
 --- a/Documentation/config.txt
 +++ b/Documentation/config.txt
 @@ -1058,6 +1058,10 @@ branch.<name>.rebase::
  	"git pull" is run. See "pull.rebase" for doing this in a non
  	branch-specific manner.
  +
 +When recreate, also pass `--recreate-merges` along to 'git rebase'
 +so that locally committed merge commits will not be flattened
 +by running 'git pull'.
 ++
  When preserve, also pass `--preserve-merges` along to 'git rebase'
  so that locally committed merge commits will not be flattened
  by running 'git pull'.
 @@ -2607,6 +2611,10 @@ pull.rebase::
  	pull" is run. See "branch.<name>.rebase" for setting this on a
  	per-branch basis.
  +
 +When recreate, also pass `--recreate-merges` along to 'git rebase'
 +so that locally committed merge commits will not be flattened
 +by running 'git pull'.
 ++
  When preserve, also pass `--preserve-merges` along to 'git rebase'
  so that locally committed merge commits will not be flattened
  by running 'git pull'.
 diff --git a/Documentation/git-pull.txt b/Documentation/git-pull.txt
 index ce05b7a5b13..b4f9f057ea9 100644
 --- a/Documentation/git-pull.txt
 +++ b/Documentation/git-pull.txt
 @@ -101,13 +101,16 @@ Options related to merging
  include::merge-options.txt[]
  
  -r::
 ---rebase[=false|true|preserve|interactive]::
 +--rebase[=false|true|recreate|preserve|interactive]::
  	When true, rebase the current branch on top of the upstream
  	branch after fetching. If there is a remote-tracking branch
  	corresponding to the upstream branch and the upstream branch
  	was rebased since last fetched, the rebase uses that information
  	to avoid rebasing non-local changes.
  +
 +When set to recreate, rebase with the `--recreate-merges` option passed
 +to `git rebase` so that locally created merge commits will not be flattened.
 ++
  When set to preserve, rebase with the `--preserve-merges` option passed
  to `git rebase` so that locally created merge commits will not be flattened.
  +
 diff --git a/Documentation/git-rebase.txt b/Documentation/git-rebase.txt
 index d713951b86a..c5a77599c47 100644
 --- a/Documentation/git-rebase.txt
 +++ b/Documentation/git-rebase.txt
 @@ -373,6 +373,17 @@ The commit list format can be changed by setting the configuration option
  rebase.instructionFormat.  A customized instruction format will automatically
  have the long commit hash prepended to the format.
  
 +--recreate-merges[=(rebase-cousins|no-rebase-cousins)]::
 +	Recreate merge commits instead of flattening the history by replaying
 +	merges. Merge conflict resolutions or manual amendments to merge
 +	commits are not recreated automatically, but have to be recreated
 +	manually.
 ++
 +By default, or when `no-rebase-cousins` was specified, commits which do not
 +have `<upstream>` as direct ancestor keep their original branch point.
 +If the `rebase-cousins` mode is turned on, such commits are rebased onto
 +`<upstream>` (or `<onto>`, if specified).
 +
  -p::
  --preserve-merges::
  	Recreate merge commits instead of flattening the history by replaying
 @@ -775,7 +786,8 @@ BUGS
  The todo list presented by `--preserve-merges --interactive` does not
  represent the topology of the revision graph.  Editing commits and
  rewording their commit messages should work fine, but attempts to
 -reorder commits tend to produce counterintuitive results.
 +reorder commits tend to produce counterintuitive results. Use
 +--recreate-merges for a more faithful representation.
  
  For example, an attempt to rearrange
  ------------
 diff --git a/builtin/pull.c b/builtin/pull.c
 index 1876271af94..9da2cfa0bd3 100644
 --- a/builtin/pull.c
 +++ b/builtin/pull.c
 @@ -27,14 +27,16 @@ enum rebase_type {
  	REBASE_FALSE = 0,
  	REBASE_TRUE,
  	REBASE_PRESERVE,
 +	REBASE_RECREATE,
  	REBASE_INTERACTIVE
  };
  
  /**
   * Parses the value of --rebase. If value is a false value, returns
   * REBASE_FALSE. If value is a true value, returns REBASE_TRUE. If value is
 - * "preserve", returns REBASE_PRESERVE. If value is a invalid value, dies with
 - * a fatal error if fatal is true, otherwise returns REBASE_INVALID.
 + * "recreate", returns REBASE_RECREATE. If value is "preserve", returns
 + * REBASE_PRESERVE. If value is a invalid value, dies with a fatal error if
 + * fatal is true, otherwise returns REBASE_INVALID.
   */
  static enum rebase_type parse_config_rebase(const char *key, const char *value,
  		int fatal)
 @@ -47,6 +49,8 @@ static enum rebase_type parse_config_rebase(const char *key, const char *value,
  		return REBASE_TRUE;
  	else if (!strcmp(value, "preserve"))
  		return REBASE_PRESERVE;
 +	else if (!strcmp(value, "recreate"))
 +		return REBASE_RECREATE;
  	else if (!strcmp(value, "interactive"))
  		return REBASE_INTERACTIVE;
  
 @@ -130,7 +134,7 @@ static struct option pull_options[] = {
  	/* Options passed to git-merge or git-rebase */
  	OPT_GROUP(N_("Options related to merging")),
  	{ OPTION_CALLBACK, 'r', "rebase", &opt_rebase,
 -	  "false|true|preserve|interactive",
 +	  "false|true|recreate|preserve|interactive",
  	  N_("incorporate changes by rebasing rather than merging"),
  	  PARSE_OPT_OPTARG, parse_opt_rebase },
  	OPT_PASSTHRU('n', NULL, &opt_diffstat, NULL,
 @@ -800,7 +804,9 @@ static int run_rebase(const struct object_id *curr_head,
  	argv_push_verbosity(&args);
  
  	/* Options passed to git-rebase */
 -	if (opt_rebase == REBASE_PRESERVE)
 +	if (opt_rebase == REBASE_RECREATE)
 +		argv_array_push(&args, "--recreate-merges");
 +	else if (opt_rebase == REBASE_PRESERVE)
  		argv_array_push(&args, "--preserve-merges");
  	else if (opt_rebase == REBASE_INTERACTIVE)
  		argv_array_push(&args, "--interactive");
 diff --git a/builtin/rebase--helper.c b/builtin/rebase--helper.c
 index ad074705bb5..5d1f12de57b 100644
 --- a/builtin/rebase--helper.c
 +++ b/builtin/rebase--helper.c
 @@ -12,8 +12,8 @@ static const char * const builtin_rebase_helper_usage[] = {
  int cmd_rebase__helper(int argc, const char **argv, const char *prefix)
  {
  	struct replay_opts opts = REPLAY_OPTS_INIT;
 -	unsigned flags = 0, keep_empty = 0;
 -	int abbreviate_commands = 0;
 +	unsigned flags = 0, keep_empty = 0, recreate_merges = 0;
 +	int abbreviate_commands = 0, rebase_cousins = -1;
  	enum {
  		CONTINUE = 1, ABORT, MAKE_SCRIPT, SHORTEN_OIDS, EXPAND_OIDS,
  		CHECK_TODO_LIST, SKIP_UNNECESSARY_PICKS, REARRANGE_SQUASH,
 @@ -24,6 +24,9 @@ int cmd_rebase__helper(int argc, const char **argv, const char *prefix)
  		OPT_BOOL(0, "keep-empty", &keep_empty, N_("keep empty commits")),
  		OPT_BOOL(0, "allow-empty-message", &opts.allow_empty_message,
  			N_("allow commits with empty messages")),
 +		OPT_BOOL(0, "recreate-merges", &recreate_merges, N_("recreate merge commits")),
 +		OPT_BOOL(0, "rebase-cousins", &rebase_cousins,
 +			 N_("keep original branch points of cousins")),
  		OPT_CMDMODE(0, "continue", &command, N_("continue rebase"),
  				CONTINUE),
  		OPT_CMDMODE(0, "abort", &command, N_("abort rebase"),
 @@ -57,8 +60,14 @@ int cmd_rebase__helper(int argc, const char **argv, const char *prefix)
  
  	flags |= keep_empty ? TODO_LIST_KEEP_EMPTY : 0;
  	flags |= abbreviate_commands ? TODO_LIST_ABBREVIATE_CMDS : 0;
 +	flags |= recreate_merges ? TODO_LIST_RECREATE_MERGES : 0;
 +	flags |= rebase_cousins > 0 ? TODO_LIST_REBASE_COUSINS : 0;
  	flags |= command == SHORTEN_OIDS ? TODO_LIST_SHORTEN_IDS : 0;
  
 +	if (rebase_cousins >= 0 && !recreate_merges)
 +		warning(_("--[no-]rebase-cousins has no effect without "
 +			  "--recreate-merges"));
 +
  	if (command == CONTINUE && argc == 1)
  		return !!sequencer_continue(&opts);
  	if (command == ABORT && argc == 1)
 diff --git a/builtin/remote.c b/builtin/remote.c
 index d95bf904c3b..b7d0f7ce596 100644
 --- a/builtin/remote.c
 +++ b/builtin/remote.c
 @@ -306,6 +306,8 @@ static int config_read_branches(const char *key, const char *value, void *cb)
  				info->rebase = v;
  			else if (!strcmp(value, "preserve"))
  				info->rebase = NORMAL_REBASE;
 +			else if (!strcmp(value, "recreate"))
 +				info->rebase = NORMAL_REBASE;
  			else if (!strcmp(value, "interactive"))
  				info->rebase = INTERACTIVE_REBASE;
  		}
 diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash
 index 88813e91244..3d44cb6890c 100644
 --- a/contrib/completion/git-completion.bash
 +++ b/contrib/completion/git-completion.bash
 @@ -2008,7 +2008,7 @@ _git_rebase ()
  	--*)
  		__gitcomp "
  			--onto --merge --strategy --interactive
 -			--preserve-merges --stat --no-stat
 +			--recreate-merges --preserve-merges --stat --no-stat
  			--committer-date-is-author-date --ignore-date
  			--ignore-whitespace --whitespace=
  			--autosquash --no-autosquash
 @@ -2182,7 +2182,7 @@ _git_config ()
  		return
  		;;
  	branch.*.rebase)
 -		__gitcomp "false true preserve interactive"
 +		__gitcomp "false true recreate preserve interactive"
  		return
  		;;
  	remote.pushdefault)
 diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh
 index a2659fea982..679d79e0d17 100644
 --- a/git-rebase--interactive.sh
 +++ b/git-rebase--interactive.sh
 @@ -162,6 +162,12 @@ s, squash <commit> = use commit, but meld into previous commit
  f, fixup <commit> = like \"squash\", but discard this commit's log message
  x, exec <commit> = run command (the rest of the line) using shell
  d, drop <commit> = remove commit
 +l, label <label> = label current HEAD with a name
 +t, reset <label> = reset HEAD to a label
 +m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
 +.       create a merge commit using the original merge commit's
 +.       message (or the oneline, if no original merge commit was
 +.       specified). Use -c <commit> to reword the commit message.
  
  These lines can be re-ordered; they are executed from top to bottom.
  " | git stripspace --comment-lines >>"$todo"
 @@ -900,6 +906,8 @@ fi
  if test t != "$preserve_merges"
  then
  	git rebase--helper --make-script ${keep_empty:+--keep-empty} \
 +		${recreate_merges:+--recreate-merges} \
 +		${rebase_cousins:+--rebase-cousins} \
  		$revisions ${restrict_revision+^$restrict_revision} >"$todo" ||
  	die "$(gettext "Could not generate todo list")"
  else
 diff --git a/git-rebase.sh b/git-rebase.sh
 index b353c33d417..9487e543bec 100755
 --- a/git-rebase.sh
 +++ b/git-rebase.sh
 @@ -17,6 +17,7 @@ q,quiet!           be quiet. implies --no-stat
  autostash          automatically stash/stash pop before and after
  fork-point         use 'merge-base --fork-point' to refine upstream
  onto=!             rebase onto given branch instead of upstream
 +recreate-merges?   try to recreate merges instead of skipping them
  p,preserve-merges! try to recreate merges instead of ignoring them
  s,strategy=!       use the given merge strategy
  no-ff!             cherry-pick all commits, even if unchanged
 @@ -87,6 +88,8 @@ type=
  state_dir=
  # One of {'', continue, skip, abort}, as parsed from command line
  action=
 +recreate_merges=
 +rebase_cousins=
  preserve_merges=
  autosquash=
  keep_empty=
 @@ -267,6 +270,19 @@ do
  	--allow-empty-message)
  		allow_empty_message=--allow-empty-message
  		;;
 +	--recreate-merges)
 +		recreate_merges=t
 +		test -z "$interactive_rebase" && interactive_rebase=implied
 +		;;
 +	--recreate-merges=*)
 +		recreate_merges=t
 +		case "${1#*=}" in
 +		rebase-cousins) rebase_cousins=t;;
 +		no-rebase-cousins) rebase_cousins=;;
 +		*) die "Unknown mode: $1";;
 +		esac
 +		test -z "$interactive_rebase" && interactive_rebase=implied
 +		;;
  	--preserve-merges)
  		preserve_merges=t
  		test -z "$interactive_rebase" && interactive_rebase=implied
 diff --git a/refs.c b/refs.c
 index 20ba82b4343..e8b84c189ff 100644
 --- a/refs.c
 +++ b/refs.c
 @@ -600,7 +600,8 @@ int dwim_log(const char *str, int len, struct object_id *oid, char **log)
  static int is_per_worktree_ref(const char *refname)
  {
  	return !strcmp(refname, "HEAD") ||
 -		starts_with(refname, "refs/bisect/");
 +		starts_with(refname, "refs/bisect/") ||
 +		starts_with(refname, "refs/rewritten/");
  }
  
  static int is_pseudoref_syntax(const char *refname)
 diff --git a/sequencer.c b/sequencer.c
 index cfa01d3bdd2..b2bf63029d4 100644
 --- a/sequencer.c
 +++ b/sequencer.c
 @@ -23,6 +23,10 @@
  #include "hashmap.h"
  #include "notes-utils.h"
  #include "sigchain.h"
 +#include "unpack-trees.h"
 +#include "worktree.h"
 +#include "oidmap.h"
 +#include "oidset.h"
  
  #define GIT_REFLOG_ACTION "GIT_REFLOG_ACTION"
  
 @@ -120,6 +124,13 @@ static GIT_PATH_FUNC(rebase_path_stopped_sha, "rebase-merge/stopped-sha")
  static GIT_PATH_FUNC(rebase_path_rewritten_list, "rebase-merge/rewritten-list")
  static GIT_PATH_FUNC(rebase_path_rewritten_pending,
  	"rebase-merge/rewritten-pending")
 +
 +/*
 + * The path of the file listing refs that need to be deleted after the rebase
 + * finishes. This is used by the `label` command to record the need for cleanup.
 + */
 +static GIT_PATH_FUNC(rebase_path_refs_to_delete, "rebase-merge/refs-to-delete")
 +
  /*
   * The following files are written by git-rebase just after parsing the
   * command-line (and are only consumed, not modified, by the sequencer).
 @@ -244,18 +255,33 @@ static const char *gpg_sign_opt_quoted(struct replay_opts *opts)
  
  int sequencer_remove_state(struct replay_opts *opts)
  {
 -	struct strbuf dir = STRBUF_INIT;
 +	struct strbuf buf = STRBUF_INIT;
  	int i;
  
 +	if (strbuf_read_file(&buf, rebase_path_refs_to_delete(), 0) > 0) {
 +		char *p = buf.buf;
 +		while (*p) {
 +			char *eol = strchr(p, '\n');
 +			if (eol)
 +				*eol = '\0';
 +			if (delete_ref("(rebase -i) cleanup", p, NULL, 0) < 0)
 +				warning(_("could not delete '%s'"), p);
 +			if (!eol)
 +				break;
 +			p = eol + 1;
 +		}
 +	}
 +
  	free(opts->gpg_sign);
  	free(opts->strategy);
  	for (i = 0; i < opts->xopts_nr; i++)
  		free(opts->xopts[i]);
  	free(opts->xopts);
  
 -	strbuf_addstr(&dir, get_dir(opts));
 -	remove_dir_recursively(&dir, 0);
 -	strbuf_release(&dir);
 +	strbuf_reset(&buf);
 +	strbuf_addstr(&buf, get_dir(opts));
 +	remove_dir_recursively(&buf, 0);
 +	strbuf_release(&buf);
  
  	return 0;
  }
 @@ -1280,6 +1306,10 @@ enum todo_command {
  	TODO_SQUASH,
  	/* commands that do something else than handling a single commit */
  	TODO_EXEC,
 +	TODO_LABEL,
 +	TODO_RESET,
 +	TODO_MERGE,
 +	TODO_MERGE_AND_EDIT,
  	/* commands that do nothing but are counted for reporting progress */
  	TODO_NOOP,
  	TODO_DROP,
 @@ -1298,6 +1328,10 @@ static struct {
  	{ 'f', "fixup" },
  	{ 's', "squash" },
  	{ 'x', "exec" },
 +	{ 'l', "label" },
 +	{ 't', "reset" },
 +	{ 'm', "merge" },
 +	{ 0, "merge" }, /* MERGE_AND_EDIT */
  	{ 0,   "noop" },
  	{ 'd', "drop" },
  	{ 0,   NULL }
 @@ -1803,13 +1837,29 @@ static int parse_insn_line(struct todo_item *item, const char *bol, char *eol)
  		return error(_("missing arguments for %s"),
  			     command_to_string(item->command));
  
 -	if (item->command == TODO_EXEC) {
 +	if (item->command == TODO_EXEC || item->command == TODO_LABEL ||
 +	    item->command == TODO_RESET) {
  		item->commit = NULL;
  		item->arg = bol;
  		item->arg_len = (int)(eol - bol);
  		return 0;
  	}
  
 +	if (item->command == TODO_MERGE) {
 +		if (skip_prefix(bol, "-C", &bol))
 +			bol += strspn(bol, " \t");
 +		else if (skip_prefix(bol, "-c", &bol)) {
 +			bol += strspn(bol, " \t");
 +			item->command = TODO_MERGE_AND_EDIT;
 +		} else {
 +			item->command = TODO_MERGE_AND_EDIT;
 +			item->commit = NULL;
 +			item->arg = bol;
 +			item->arg_len = (int)(eol - bol);
 +			return 0;
 +		}
 +	}
 +
  	end_of_object_name = (char *) bol + strcspn(bol, " \t\n");
  	saved = *end_of_object_name;
  	*end_of_object_name = '\0';
 @@ -2444,6 +2494,304 @@ static int do_exec(const char *command_line)
  	return status;
  }
  
 +static int safe_append(const char *filename, const char *fmt, ...)
 +{
 +	va_list ap;
 +	struct lock_file lock = LOCK_INIT;
 +	int fd = hold_lock_file_for_update(&lock, filename,
 +					   LOCK_REPORT_ON_ERROR);
 +	struct strbuf buf = STRBUF_INIT;
 +
 +	if (fd < 0)
 +		return -1;
 +
 +	if (strbuf_read_file(&buf, filename, 0) < 0 && errno != ENOENT)
 +		return error_errno(_("could not read '%s'"), filename);
 +	strbuf_complete(&buf, '\n');
 +	va_start(ap, fmt);
 +	strbuf_vaddf(&buf, fmt, ap);
 +	va_end(ap);
 +
 +	if (write_in_full(fd, buf.buf, buf.len) < 0) {
 +		error_errno(_("could not write to '%s'"), filename);
 +		strbuf_release(&buf);
 +		rollback_lock_file(&lock);
 +		return -1;
 +	}
 +	if (commit_lock_file(&lock) < 0) {
 +		strbuf_release(&buf);
 +		rollback_lock_file(&lock);
 +		return error(_("failed to finalize '%s'"), filename);
 +	}
 +
 +	strbuf_release(&buf);
 +	return 0;
 +}
 +
 +static int do_label(const char *name, int len)
 +{
 +	struct ref_store *refs = get_main_ref_store();
 +	struct ref_transaction *transaction;
 +	struct strbuf ref_name = STRBUF_INIT, err = STRBUF_INIT;
 +	struct strbuf msg = STRBUF_INIT;
 +	int ret = 0;
 +	struct object_id head_oid;
 +
 +	if (len == 1 && *name == '#')
 +		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);
 +
 +	transaction = ref_store_transaction_begin(refs, &err);
 +	if (!transaction) {
 +		error("%s", err.buf);
 +		ret = -1;
 +	} else if (get_oid("HEAD", &head_oid)) {
 +		error(_("could not read HEAD"));
 +		ret = -1;
 +	} else if (ref_transaction_update(transaction, ref_name.buf, &head_oid,
 +					  NULL, 0, msg.buf, &err) < 0 ||
 +		   ref_transaction_commit(transaction, &err)) {
 +		error("%s", err.buf);
 +		ret = -1;
 +	}
 +	ref_transaction_free(transaction);
 +	strbuf_release(&err);
 +	strbuf_release(&msg);
 +
 +	if (!ret)
 +		ret = safe_append(rebase_path_refs_to_delete(),
 +				  "%s\n", ref_name.buf);
 +	strbuf_release(&ref_name);
 +
 +	return ret;
 +}
 +
 +static int do_reset(const char *name, int len, struct replay_opts *opts)
 +{
 +	struct strbuf ref_name = STRBUF_INIT;
 +	struct object_id oid;
 +	struct lock_file lock = LOCK_INIT;
 +	struct tree_desc desc;
 +	struct tree *tree;
 +	struct unpack_trees_options unpack_tree_opts;
 +	int ret = 0, i;
 +
 +	if (hold_locked_index(&lock, LOCK_REPORT_ON_ERROR) < 0)
 +		return -1;
 +
 +	/* Determine the length of the label */
 +	for (i = 0; i < len; i++)
 +		if (isspace(name[i]))
 +			len = i;
 +
 +	strbuf_addf(&ref_name, "refs/rewritten/%.*s", len, name);
 +	if (get_oid(ref_name.buf, &oid) &&
 +	    get_oid(ref_name.buf + strlen("refs/rewritten/"), &oid)) {
 +		error(_("could not read '%s'"), ref_name.buf);
 +		rollback_lock_file(&lock);
 +		strbuf_release(&ref_name);
 +		return -1;
 +	}
 +
 +	memset(&unpack_tree_opts, 0, sizeof(unpack_tree_opts));
 +	unpack_tree_opts.head_idx = 1;
 +	unpack_tree_opts.src_index = &the_index;
 +	unpack_tree_opts.dst_index = &the_index;
 +	unpack_tree_opts.fn = oneway_merge;
 +	unpack_tree_opts.merge = 1;
 +	unpack_tree_opts.update = 1;
 +	unpack_tree_opts.reset = 1;
 +
 +	if (read_cache_unmerged()) {
 +		rollback_lock_file(&lock);
 +		strbuf_release(&ref_name);
 +		return error_resolve_conflict(_(action_name(opts)));
 +	}
 +
 +	if (!fill_tree_descriptor(&desc, &oid)) {
 +		error(_("failed to find tree of %s"), oid_to_hex(&oid));
 +		rollback_lock_file(&lock);
 +		free((void *)desc.buffer);
 +		strbuf_release(&ref_name);
 +		return -1;
 +	}
 +
 +	if (unpack_trees(1, &desc, &unpack_tree_opts)) {
 +		rollback_lock_file(&lock);
 +		free((void *)desc.buffer);
 +		strbuf_release(&ref_name);
 +		return -1;
 +	}
 +
 +	tree = parse_tree_indirect(&oid);
 +	prime_cache_tree(&the_index, tree);
 +
 +	if (write_locked_index(&the_index, &lock, COMMIT_LOCK) < 0)
 +		ret = error(_("could not write index"));
 +	free((void *)desc.buffer);
 +
 +	if (!ret) {
 +		struct strbuf msg = STRBUF_INIT;
 +
 +		strbuf_addf(&msg, "(rebase -i) reset '%.*s'", len, name);
 +		ret = update_ref(msg.buf, "HEAD", &oid, NULL, 0,
 +				 UPDATE_REFS_MSG_ON_ERR);
 +		strbuf_release(&msg);
 +	}
 +
 +	strbuf_release(&ref_name);
 +	return ret;
 +}
 +
 +static int do_merge(struct commit *commit, const char *arg, int arg_len,
 +		    int run_commit_flags, struct replay_opts *opts)
 +{
 +	int merge_arg_len;
 +	struct strbuf ref_name = STRBUF_INIT;
 +	struct commit *head_commit, *merge_commit, *i;
 +	struct commit_list *common, *j, *reversed = NULL;
 +	struct merge_options o;
 +	int can_fast_forward, ret;
 +	static struct lock_file lock;
 +
 +	for (merge_arg_len = 0; merge_arg_len < arg_len; merge_arg_len++)
 +		if (isspace(arg[merge_arg_len]))
 +			break;
 +
 +	if (hold_locked_index(&lock, LOCK_REPORT_ON_ERROR) < 0)
 +		return -1;
 +
 +	head_commit = lookup_commit_reference_by_name("HEAD");
 +	if (!head_commit) {
 +		rollback_lock_file(&lock);
 +		return error(_("cannot merge without a current revision"));
 +	}
 +
 +	if (commit) {
 +		const char *message = get_commit_buffer(commit, NULL);
 +		const char *body;
 +		int len;
 +
 +		if (!message) {
 +			rollback_lock_file(&lock);
 +			return error(_("could not get commit message of '%s'"),
 +				     oid_to_hex(&commit->object.oid));
 +		}
 +		write_author_script(message);
 +		find_commit_subject(message, &body);
 +		len = strlen(body);
 +		if (write_message(body, len, git_path_merge_msg(), 0) < 0) {
 +			error_errno(_("could not write '%s'"),
 +				    git_path_merge_msg());
 +			unuse_commit_buffer(commit, message);
 +			rollback_lock_file(&lock);
 +			return -1;
 +		}
 +		unuse_commit_buffer(commit, message);
 +	} else {
 +		const char *p = arg + merge_arg_len;
 +		struct strbuf buf = STRBUF_INIT;
 +		int len;
 +
 +		strbuf_addf(&buf, "author %s", git_author_info(0));
 +		write_author_script(buf.buf);
 +		strbuf_reset(&buf);
 +
 +		p += strspn(p, " \t");
 +		if (*p == '#' && isspace(p[1]))
 +			p += 1 + strspn(p + 1, " \t");
 +		if (*p)
 +			len = strlen(p);
 +		else {
 +			strbuf_addf(&buf, "Merge branch '%.*s'",
 +				    merge_arg_len, arg);
 +			p = buf.buf;
 +			len = buf.len;
 +		}
 +
 +		if (write_message(p, len, git_path_merge_msg(), 0) < 0) {
 +			error_errno(_("could not write '%s'"),
 +				    git_path_merge_msg());
 +			strbuf_release(&buf);
 +			rollback_lock_file(&lock);
 +			return -1;
 +		}
 +		strbuf_release(&buf);
 +	}
 +
 +	/*
 +	 * If HEAD is not identical to the parent of the original merge commit,
 +	 * we cannot fast-forward.
 +	 */
 +	can_fast_forward = opts->allow_ff && commit && commit->parents &&
 +		!oidcmp(&commit->parents->item->object.oid,
 +			&head_commit->object.oid);
 +
 +	strbuf_addf(&ref_name, "refs/rewritten/%.*s", merge_arg_len, arg);
 +	merge_commit = lookup_commit_reference_by_name(ref_name.buf);
 +	if (!merge_commit) {
 +		/* fall back to non-rewritten ref or commit */
 +		strbuf_splice(&ref_name, 0, strlen("refs/rewritten/"), "", 0);
 +		merge_commit = lookup_commit_reference_by_name(ref_name.buf);
 +	}
 +	if (!merge_commit) {
 +		error(_("could not resolve '%s'"), ref_name.buf);
 +		strbuf_release(&ref_name);
 +		rollback_lock_file(&lock);
 +		return -1;
 +	}
 +
 +	if (can_fast_forward && commit->parents->next &&
 +	    !commit->parents->next->next &&
 +	    !oidcmp(&commit->parents->next->item->object.oid,
 +		    &merge_commit->object.oid)) {
 +		strbuf_release(&ref_name);
 +		rollback_lock_file(&lock);
 +		return fast_forward_to(&commit->object.oid,
 +				       &head_commit->object.oid, 0, opts);
 +	}
 +
 +	write_message(oid_to_hex(&merge_commit->object.oid), GIT_SHA1_HEXSZ,
 +		      git_path_merge_head(), 0);
 +	write_message("no-ff", 5, git_path_merge_mode(), 0);
 +
 +	common = get_merge_bases(head_commit, merge_commit);
 +	for (j = common; j; j = j->next)
 +		commit_list_insert(j->item, &reversed);
 +	free_commit_list(common);
 +
 +	read_cache();
 +	init_merge_options(&o);
 +	o.branch1 = "HEAD";
 +	o.branch2 = ref_name.buf;
 +	o.buffer_output = 2;
 +
 +	ret = merge_recursive(&o, head_commit, merge_commit, reversed, &i);
 +	if (ret <= 0)
 +		fputs(o.obuf.buf, stdout);
 +	strbuf_release(&o.obuf);
 +	if (ret < 0) {
 +		strbuf_release(&ref_name);
 +		rollback_lock_file(&lock);
 +		return error(_("conflicts while merging '%.*s'"),
 +			     merge_arg_len, arg);
 +	}
 +
 +	if (active_cache_changed &&
 +	    write_locked_index(&the_index, &lock, COMMIT_LOCK)) {
 +		strbuf_release(&ref_name);
 +		return error(_("merge: Unable to write new index file"));
 +	}
 +	rollback_lock_file(&lock);
 +
 +	ret = run_git_commit(git_path_merge_msg(), opts, run_commit_flags);
 +	strbuf_release(&ref_name);
 +
 +	return ret;
 +}
 +
  static int is_final_fixup(struct todo_list *todo_list)
  {
  	int i = todo_list->current;
 @@ -2627,6 +2975,18 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts)
  				/* `current` will be incremented below */
  				todo_list->current = -1;
  			}
 +		} else if (item->command == TODO_LABEL)
 +			res = do_label(item->arg, item->arg_len);
 +		else if (item->command == TODO_RESET)
 +			res = do_reset(item->arg, item->arg_len, opts);
 +		else if (item->command == TODO_MERGE ||
 +			 item->command == TODO_MERGE_AND_EDIT) {
 +			res = do_merge(item->commit, item->arg, item->arg_len,
 +				       item->command == TODO_MERGE_AND_EDIT ?
 +				       EDIT_MSG | VERIFY_MSG : 0, opts);
 +			if (item->commit)
 +				record_in_rewritten(&item->commit->object.oid,
 +						    peek_command(todo_list, 1));
  		} else if (!is_noop(item->command))
  			return error(_("unknown command %d"), item->command);
  
 @@ -2981,6 +3341,345 @@ void append_signoff(struct strbuf *msgbuf, int ignore_footer, unsigned flag)
  	strbuf_release(&sob);
  }
  
 +struct labels_entry {
 +	struct hashmap_entry entry;
 +	char label[FLEX_ARRAY];
 +};
 +
 +static int labels_cmp(const void *fndata, const struct labels_entry *a,
 +		      const struct labels_entry *b, const void *key)
 +{
 +	return key ? strcmp(a->label, key) : strcmp(a->label, b->label);
 +}
 +
 +struct string_entry {
 +	struct oidmap_entry entry;
 +	char string[FLEX_ARRAY];
 +};
 +
 +struct label_state {
 +	struct oidmap commit2label;
 +	struct hashmap labels;
 +	struct strbuf buf;
 +};
 +
 +static const char *label_oid(struct object_id *oid, const char *label,
 +			     struct label_state *state)
 +{
 +	struct labels_entry *labels_entry;
 +	struct string_entry *string_entry;
 +	struct object_id dummy;
 +	size_t len;
 +	int i;
 +
 +	string_entry = oidmap_get(&state->commit2label, oid);
 +	if (string_entry)
 +		return string_entry->string;
 +
 +	/*
 +	 * For "uninteresting" commits, i.e. commits that are not to be
 +	 * rebased, and which can therefore not be labeled, we use a unique
 +	 * abbreviation of the commit name. This is slightly more complicated
 +	 * than calling find_unique_abbrev() because we also need to make
 +	 * sure that the abbreviation does not conflict with any other
 +	 * label.
 +	 *
 +	 * We disallow "interesting" commits to be labeled by a string that
 +	 * is a valid full-length hash, to ensure that we always can find an
 +	 * abbreviation for any uninteresting commit's names that does not
 +	 * clash with any other label.
 +	 */
 +	if (!label) {
 +		char *p;
 +
 +		strbuf_reset(&state->buf);
 +		strbuf_grow(&state->buf, GIT_SHA1_HEXSZ);
 +		label = p = state->buf.buf;
 +
 +		find_unique_abbrev_r(p, oid->hash, default_abbrev);
 +
 +		/*
 +		 * We may need to extend the abbreviated hash so that there is
 +		 * no conflicting label.
 +		 */
 +		if (hashmap_get_from_hash(&state->labels, strihash(p), p)) {
 +			size_t i = strlen(p) + 1;
 +
 +			oid_to_hex_r(p, oid);
 +			for (; i < GIT_SHA1_HEXSZ; i++) {
 +				char save = p[i];
 +				p[i] = '\0';
 +				if (!hashmap_get_from_hash(&state->labels,
 +							   strihash(p), p))
 +					break;
 +				p[i] = save;
 +			}
 +		}
 +	} else if (((len = strlen(label)) == GIT_SHA1_RAWSZ &&
 +		    !get_oid_hex(label, &dummy)) ||
 +		   (len == 1 && *label == '#') ||
 +		   hashmap_get_from_hash(&state->labels,
 +					 strihash(label), label)) {
 +		/*
 +		 * If the label already exists, or if the label is a valid full
 +		 * OID, or the label is a '#' (which we use as a separator
 +		 * between merge heads and oneline), we append a dash and a
 +		 * number to make it unique.
 +		 */
 +		struct strbuf *buf = &state->buf;
 +
 +		strbuf_reset(buf);
 +		strbuf_add(buf, label, len);
 +
 +		for (i = 2; ; i++) {
 +			strbuf_setlen(buf, len);
 +			strbuf_addf(buf, "-%d", i);
 +			if (!hashmap_get_from_hash(&state->labels,
 +						   strihash(buf->buf),
 +						   buf->buf))
 +				break;
 +		}
 +
 +		label = buf->buf;
 +	}
 +
 +	FLEX_ALLOC_STR(labels_entry, label, label);
 +	hashmap_entry_init(labels_entry, strihash(label));
 +	hashmap_add(&state->labels, labels_entry);
 +
 +	FLEX_ALLOC_STR(string_entry, string, label);
 +	oidcpy(&string_entry->entry.oid, oid);
 +	oidmap_put(&state->commit2label, string_entry);
 +
 +	return string_entry->string;
 +}
 +
 +static int make_script_with_merges(struct pretty_print_context *pp,
 +				   struct rev_info *revs, FILE *out,
 +				   unsigned flags)
 +{
 +	int keep_empty = flags & TODO_LIST_KEEP_EMPTY;
 +	int rebase_cousins = flags & TODO_LIST_REBASE_COUSINS;
 +	struct strbuf buf = STRBUF_INIT, oneline = STRBUF_INIT;
 +	struct strbuf label = STRBUF_INIT;
 +	struct commit_list *commits = NULL, **tail = &commits, *iter;
 +	struct commit_list *tips = NULL, **tips_tail = &tips;
 +	struct commit *commit;
 +	struct oidmap commit2todo = OIDMAP_INIT;
 +	struct string_entry *entry;
 +	struct oidset interesting = OIDSET_INIT, child_seen = OIDSET_INIT,
 +		shown = OIDSET_INIT;
 +	struct label_state state = { OIDMAP_INIT, { NULL }, STRBUF_INIT };
 +
 +	int abbr = flags & TODO_LIST_ABBREVIATE_CMDS;
 +	const char *cmd_pick = abbr ? "p" : "pick",
 +		*cmd_label = abbr ? "l" : "label",
 +		*cmd_reset = abbr ? "t" : "reset",
 +		*cmd_merge = abbr ? "m" : "merge";
 +
 +	oidmap_init(&commit2todo, 0);
 +	oidmap_init(&state.commit2label, 0);
 +	hashmap_init(&state.labels, (hashmap_cmp_fn) labels_cmp, NULL, 0);
 +	strbuf_init(&state.buf, 32);
 +
 +	if (revs->cmdline.nr && (revs->cmdline.rev[0].flags & BOTTOM)) {
 +		struct object_id *oid = &revs->cmdline.rev[0].item->oid;
 +		FLEX_ALLOC_STR(entry, string, "onto");
 +		oidcpy(&entry->entry.oid, oid);
 +		oidmap_put(&state.commit2label, entry);
 +	}
 +
 +	/*
 +	 * First phase:
 +	 * - get onelines for all commits
 +	 * - gather all branch tips (i.e. 2nd or later parents of merges)
 +	 * - label all branch tips
 +	 */
 +	while ((commit = get_revision(revs))) {
 +		struct commit_list *to_merge;
 +		int is_octopus;
 +		const char *p1, *p2;
 +		struct object_id *oid;
 +
 +		tail = &commit_list_insert(commit, tail)->next;
 +		oidset_insert(&interesting, &commit->object.oid);
 +
 +		if ((commit->object.flags & PATCHSAME))
 +			continue;
 +
 +		strbuf_reset(&oneline);
 +		pretty_print_commit(pp, commit, &oneline);
 +
 +		to_merge = commit->parents ? commit->parents->next : NULL;
 +		if (!to_merge) {
 +			/* non-merge commit: easy case */
 +			strbuf_reset(&buf);
 +			if (!keep_empty && is_original_commit_empty(commit))
 +				strbuf_addf(&buf, "%c ", comment_line_char);
 +			strbuf_addf(&buf, "%s %s %s", cmd_pick,
 +				    oid_to_hex(&commit->object.oid),
 +				    oneline.buf);
 +
 +			FLEX_ALLOC_STR(entry, string, buf.buf);
 +			oidcpy(&entry->entry.oid, &commit->object.oid);
 +			oidmap_put(&commit2todo, entry);
 +
 +			continue;
 +		}
 +
 +		is_octopus = to_merge && to_merge->next;
 +
 +		if (is_octopus)
 +			BUG("Octopus merges not yet supported");
 +
 +		/* Create a label */
 +		strbuf_reset(&label);
 +		if (skip_prefix(oneline.buf, "Merge ", &p1) &&
 +		    (p1 = strchr(p1, '\'')) &&
 +		    (p2 = strchr(++p1, '\'')))
 +			strbuf_add(&label, p1, p2 - p1);
 +		else if (skip_prefix(oneline.buf, "Merge pull request ",
 +				     &p1) &&
 +			 (p1 = strstr(p1, " from ")))
 +			strbuf_addstr(&label, p1 + strlen(" from "));
 +		else
 +			strbuf_addbuf(&label, &oneline);
 +
 +		for (p1 = label.buf; *p1; p1++)
 +			if (isspace(*p1))
 +				*(char *)p1 = '-';
 +
 +		strbuf_reset(&buf);
 +		strbuf_addf(&buf, "%s -C %s",
 +			    cmd_merge, oid_to_hex(&commit->object.oid));
 +
 +		/* label the tip of merged branch */
 +		oid = &to_merge->item->object.oid;
 +		strbuf_addch(&buf, ' ');
 +
 +		if (!oidset_contains(&interesting, oid))
 +			strbuf_addstr(&buf, label_oid(oid, NULL, &state));
 +		else {
 +			tips_tail = &commit_list_insert(to_merge->item,
 +							tips_tail)->next;
 +
 +			strbuf_addstr(&buf, label_oid(oid, label.buf, &state));
 +		}
 +		strbuf_addf(&buf, " # %s", oneline.buf);
 +
 +		FLEX_ALLOC_STR(entry, string, buf.buf);
 +		oidcpy(&entry->entry.oid, &commit->object.oid);
 +		oidmap_put(&commit2todo, entry);
 +	}
 +
 +	/*
 +	 * Second phase:
 +	 * - label branch points
 +	 * - add HEAD to the branch tips
 +	 */
 +	for (iter = commits; iter; iter = iter->next) {
 +		struct commit_list *parent = iter->item->parents;
 +		for (; parent; parent = parent->next) {
 +			struct object_id *oid = &parent->item->object.oid;
 +			if (!oidset_contains(&interesting, oid))
 +				continue;
 +			if (!oidset_contains(&child_seen, oid))
 +				oidset_insert(&child_seen, oid);
 +			else
 +				label_oid(oid, "branch-point", &state);
 +		}
 +
 +		/* Add HEAD as implict "tip of branch" */
 +		if (!iter->next)
 +			tips_tail = &commit_list_insert(iter->item,
 +							tips_tail)->next;
 +	}
 +
 +	/*
 +	 * Third phase: output the todo list. This is a bit tricky, as we
 +	 * want to avoid jumping back and forth between revisions. To
 +	 * accomplish that goal, we walk backwards from the branch tips,
 +	 * gathering commits not yet shown, reversing the list on the fly,
 +	 * then outputting that list (labeling revisions as needed).
 +	 */
 +	fprintf(out, "%s onto\n", cmd_label);
 +	for (iter = tips; iter; iter = iter->next) {
 +		struct commit_list *list = NULL, *iter2;
 +
 +		commit = iter->item;
 +		if (oidset_contains(&shown, &commit->object.oid))
 +			continue;
 +		entry = oidmap_get(&state.commit2label, &commit->object.oid);
 +
 +		if (entry)
 +			fprintf(out, "\n# Branch %s\n", entry->string);
 +		else
 +			fprintf(out, "\n");
 +
 +		while (oidset_contains(&interesting, &commit->object.oid) &&
 +		       !oidset_contains(&shown, &commit->object.oid)) {
 +			commit_list_insert(commit, &list);
 +			if (!commit->parents) {
 +				commit = NULL;
 +				break;
 +			}
 +			commit = commit->parents->item;
 +		}
 +
 +		if (!commit)
 +			fprintf(out, "%s onto\n", cmd_reset);
 +		else {
 +			const char *to = NULL;
 +
 +			entry = oidmap_get(&state.commit2label,
 +					   &commit->object.oid);
 +			if (entry)
 +				to = entry->string;
 +			else if (!rebase_cousins)
 +				to = label_oid(&commit->object.oid, NULL,
 +					       &state);
 +
 +			if (!to || !strcmp(to, "onto"))
 +				fprintf(out, "%s onto\n", cmd_reset);
 +			else {
 +				strbuf_reset(&oneline);
 +				pretty_print_commit(pp, commit, &oneline);
 +				fprintf(out, "%s %s # %s\n",
 +					cmd_reset, to, oneline.buf);
 +			}
 +		}
 +
 +		for (iter2 = list; iter2; iter2 = iter2->next) {
 +			struct object_id *oid = &iter2->item->object.oid;
 +			entry = oidmap_get(&commit2todo, oid);
 +			/* only show if not already upstream */
 +			if (entry)
 +				fprintf(out, "%s\n", entry->string);
 +			entry = oidmap_get(&state.commit2label, oid);
 +			if (entry)
 +				fprintf(out, "%s %s\n",
 +					cmd_label, entry->string);
 +			oidset_insert(&shown, oid);
 +		}
 +
 +		free_commit_list(list);
 +	}
 +
 +	free_commit_list(commits);
 +	free_commit_list(tips);
 +
 +	strbuf_release(&label);
 +	strbuf_release(&oneline);
 +	strbuf_release(&buf);
 +
 +	oidmap_free(&commit2todo, 1);
 +	oidmap_free(&state.commit2label, 1);
 +	hashmap_free(&state.labels, 1);
 +	strbuf_release(&state.buf);
 +
 +	return 0;
 +}
 +
  int sequencer_make_script(FILE *out, int argc, const char **argv,
  			  unsigned flags)
  {
 @@ -2991,11 +3690,16 @@ int sequencer_make_script(FILE *out, int argc, const char **argv,
  	struct commit *commit;
  	int keep_empty = flags & TODO_LIST_KEEP_EMPTY;
  	const char *insn = flags & TODO_LIST_ABBREVIATE_CMDS ? "p" : "pick";
 +	int recreate_merges = flags & TODO_LIST_RECREATE_MERGES;
  
  	init_revisions(&revs, NULL);
  	revs.verbose_header = 1;
 -	revs.max_parents = 1;
 -	revs.cherry_pick = 1;
 +	if (recreate_merges)
 +		revs.cherry_mark = 1;
 +	else {
 +		revs.max_parents = 1;
 +		revs.cherry_pick = 1;
 +	}
  	revs.limited = 1;
  	revs.reverse = 1;
  	revs.right_only = 1;
 @@ -3019,6 +3723,9 @@ int sequencer_make_script(FILE *out, int argc, const char **argv,
  	if (prepare_revision_walk(&revs) < 0)
  		return error(_("make_script: error preparing revisions"));
  
 +	if (recreate_merges)
 +		return make_script_with_merges(&pp, &revs, out, flags);
 +
  	while ((commit = get_revision(&revs))) {
  		strbuf_reset(&buf);
  		if (!keep_empty && is_original_commit_empty(commit))
 @@ -3108,8 +3815,14 @@ int transform_todos(unsigned flags)
  					  short_commit_name(item->commit) :
  					  oid_to_hex(&item->commit->object.oid);
  
 +			if (item->command == TODO_MERGE)
 +				strbuf_addstr(&buf, " -C");
 +			else if (item->command == TODO_MERGE_AND_EDIT)
 +				strbuf_addstr(&buf, " -c");
 +
  			strbuf_addf(&buf, " %s", oid);
  		}
 +
  		/* add all the rest */
  		if (!item->arg_len)
  			strbuf_addch(&buf, '\n');
 diff --git a/sequencer.h b/sequencer.h
 index e45b178dfc4..739dd0fa92b 100644
 --- a/sequencer.h
 +++ b/sequencer.h
 @@ -59,6 +59,13 @@ int sequencer_remove_state(struct replay_opts *opts);
  #define TODO_LIST_KEEP_EMPTY (1U << 0)
  #define TODO_LIST_SHORTEN_IDS (1U << 1)
  #define TODO_LIST_ABBREVIATE_CMDS (1U << 2)
 +#define TODO_LIST_RECREATE_MERGES (1U << 3)
 +/*
 + * When recreating merges, commits that do have the base commit as ancestor
 + * ("cousins") are *not* rebased onto the new base by default. If those
 + * commits should be rebased onto the new base, this flag needs to be passed.
 + */
 +#define TODO_LIST_REBASE_COUSINS (1U << 4)
  int sequencer_make_script(FILE *out, int argc, const char **argv,
  			  unsigned flags);
  
 diff --git a/t/t3430-rebase-recreate-merges.sh b/t/t3430-rebase-recreate-merges.sh
 new file mode 100755
 index 00000000000..9a59f12b670
 --- /dev/null
 +++ b/t/t3430-rebase-recreate-merges.sh
 @@ -0,0 +1,208 @@
 +#!/bin/sh
 +#
 +# Copyright (c) 2017 Johannes E. Schindelin
 +#
 +
 +test_description='git rebase -i --recreate-merges
 +
 +This test runs git rebase "interactively", retaining the branch structure by
 +recreating merge commits.
 +
 +Initial setup:
 +
 +    -- B --                   (first)
 +   /       \
 + A - C - D - E - H            (master)
 +       \       /
 +         F - G                (second)
 +'
 +. ./test-lib.sh
 +. "$TEST_DIRECTORY"/lib-rebase.sh
 +
 +test_expect_success 'setup' '
 +	write_script replace-editor.sh <<-\EOF &&
 +	mv "$1" "$(git rev-parse --git-path ORIGINAL-TODO)"
 +	cp script-from-scratch "$1"
 +	EOF
 +
 +	test_commit A &&
 +	git checkout -b first &&
 +	test_commit B &&
 +	git checkout master &&
 +	test_commit C &&
 +	test_commit D &&
 +	git merge --no-commit B &&
 +	test_tick &&
 +	git commit -m E &&
 +	git tag -m E E &&
 +	git checkout -b second C &&
 +	test_commit F &&
 +	test_commit G &&
 +	git checkout master &&
 +	git merge --no-commit G &&
 +	test_tick &&
 +	git commit -m H &&
 +	git tag -m H H
 +'
 +
 +cat >script-from-scratch <<\EOF
 +label onto
 +
 +# onebranch
 +pick G
 +pick D
 +label onebranch
 +
 +# second
 +reset onto
 +pick B
 +label second
 +
 +reset onto
 +merge -C H second
 +merge onebranch # Merge the topic branch 'onebranch'
 +EOF
 +
 +test_cmp_graph () {
 +	cat >expect &&
 +	git log --graph --boundary --format=%s "$@" >output &&
 +	sed "s/ *$//" <output >output.trimmed &&
 +	test_cmp expect output.trimmed
 +}
 +
 +test_expect_success 'create completely different structure' '
 +	test_config sequence.editor \""$PWD"/replace-editor.sh\" &&
 +	test_tick &&
 +	git rebase -i --recreate-merges A &&
 +	test_cmp_graph <<-\EOF
 +	*   Merge the topic branch '\''onebranch'\''
 +	|\
 +	| * D
 +	| * G
 +	* |   H
 +	|\ \
 +	| |/
 +	|/|
 +	| * B
 +	|/
 +	* A
 +	EOF
 +'
 +
 +test_expect_success 'generate correct todo list' '
 +	cat >expect <<-\EOF &&
 +	label onto
 +
 +	reset onto
 +	pick d9df450 B
 +	label E
 +
 +	reset onto
 +	pick 5dee784 C
 +	label branch-point
 +	pick ca2c861 F
 +	pick 088b00a G
 +	label H
 +
 +	reset branch-point # C
 +	pick 12bd07b D
 +	merge -C 2051b56 E # E
 +	merge -C 233d48a H # H
 +
 +	EOF
 +
 +	grep -v "^#" <.git/ORIGINAL-TODO >output &&
 +	test_cmp expect output
 +'
 +
 +test_expect_success 'with a branch tip that was cherry-picked already' '
 +	git checkout -b already-upstream master &&
 +	base="$(git rev-parse --verify HEAD)" &&
 +
 +	test_commit A1 &&
 +	test_commit A2 &&
 +	git reset --hard $base &&
 +	test_commit B1 &&
 +	test_tick &&
 +	git merge -m "Merge branch A" A2 &&
 +
 +	git checkout -b upstream-with-a2 $base &&
 +	test_tick &&
 +	git cherry-pick A2 &&
 +
 +	git checkout already-upstream &&
 +	test_tick &&
 +	git rebase -i --recreate-merges upstream-with-a2 &&
 +	test_cmp_graph upstream-with-a2.. <<-\EOF
 +	*   Merge branch A
 +	|\
 +	| * A1
 +	* | B1
 +	|/
 +	o A2
 +	EOF
 +'
 +
 +test_expect_success 'do not rebase cousins unless asked for' '
 +	write_script copy-editor.sh <<-\EOF &&
 +	cp "$1" "$(git rev-parse --git-path ORIGINAL-TODO)"
 +	EOF
 +
 +	test_config sequence.editor \""$PWD"/copy-editor.sh\" &&
 +	git checkout -b cousins master &&
 +	before="$(git rev-parse --verify HEAD)" &&
 +	test_tick &&
 +	git rebase -i --recreate-merges HEAD^ &&
 +	test_cmp_rev HEAD $before &&
 +	test_tick &&
 +	git rebase -i --recreate-merges=rebase-cousins HEAD^ &&
 +	test_cmp_graph HEAD^.. <<-\EOF
 +	*   Merge the topic branch '\''onebranch'\''
 +	|\
 +	| * D
 +	| * G
 +	|/
 +	o H
 +	EOF
 +'
 +
 +test_expect_success 'refs/rewritten/* is worktree-local' '
 +	git worktree add wt &&
 +	cat >wt/script-from-scratch <<-\EOF &&
 +	label xyz
 +	exec GIT_DIR=../.git git rev-parse --verify refs/rewritten/xyz >a || :
 +	exec git rev-parse --verify refs/rewritten/xyz >b
 +	EOF
 +
 +	test_config -C wt sequence.editor \""$PWD"/replace-editor.sh\" &&
 +	git -C wt rebase -i HEAD &&
 +	test_must_be_empty wt/a &&
 +	test_cmp_rev HEAD "$(cat wt/b)"
 +'
 +
 +test_expect_success 'post-rewrite hook and fixups work for merges' '
 +	git checkout -b post-rewrite &&
 +	test_commit same1 &&
 +	git reset --hard HEAD^ &&
 +	test_commit same2 &&
 +	git merge -m "to fix up" same1 &&
 +	echo same old same old >same2.t &&
 +	test_tick &&
 +	git commit --fixup HEAD same2.t &&
 +	fixup="$(git rev-parse HEAD)" &&
 +
 +	mkdir -p .git/hooks &&
 +	test_when_finished "rm .git/hooks/post-rewrite" &&
 +	echo "cat >actual" | write_script .git/hooks/post-rewrite &&
 +
 +	test_tick &&
 +	git rebase -i --autosquash --recreate-merges HEAD^^^ &&
 +	printf "%s %s\n%s %s\n%s %s\n%s %s\n" >expect $(git rev-parse \
 +		$fixup^^2 HEAD^2 \
 +		$fixup^^ HEAD^ \
 +		$fixup^ HEAD \
 +		$fixup HEAD) &&
 +	test_cmp expect actual
 +'
 +
 +test_done
-- 
2.16.1.windows.4


^ permalink raw reply	[flat|nested] 276+ messages in thread

* [PATCH v4 01/12] sequencer: avoid using errno clobbered by rollback_lock_file()
  2018-02-23 12:35     ` [PATCH v4 00/12] rebase -i: offer to recreate merge commits Johannes Schindelin
@ 2018-02-23 12:35       ` Johannes Schindelin
  2018-02-23 12:36       ` [PATCH v4 02/12] sequencer: make rearrange_squash() a bit more obvious Johannes Schindelin
                         ` (11 subsequent siblings)
  12 siblings, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-02-23 12:35 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Jacob Keller, Stefan Beller, Philip Oakley, Eric Sunshine, Phillip Wood

As pointed out in a review of the `--recreate-merges` patch series,
`rollback_lock_file()` clobbers errno. Therefore, we have to report the
error message that uses errno before calling said function.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 sequencer.c | 13 ++++++++-----
 1 file changed, 8 insertions(+), 5 deletions(-)

diff --git a/sequencer.c b/sequencer.c
index e9baaf59bd9..5aa3dc3c95c 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -345,12 +345,14 @@ static int write_message(const void *buf, size_t len, const char *filename,
 	if (msg_fd < 0)
 		return error_errno(_("could not lock '%s'"), filename);
 	if (write_in_full(msg_fd, buf, len) < 0) {
+		error_errno(_("could not write to '%s'"), filename);
 		rollback_lock_file(&msg_file);
-		return error_errno(_("could not write to '%s'"), filename);
+		return -1;
 	}
 	if (append_eol && write(msg_fd, "\n", 1) < 0) {
+		error_errno(_("could not write eol to '%s'"), filename);
 		rollback_lock_file(&msg_file);
-		return error_errno(_("could not write eol to '%s'"), filename);
+		return -1;
 	}
 	if (commit_lock_file(&msg_file) < 0) {
 		rollback_lock_file(&msg_file);
@@ -2106,16 +2108,17 @@ static int save_head(const char *head)
 
 	fd = hold_lock_file_for_update(&head_lock, git_path_head_file(), 0);
 	if (fd < 0) {
+		error_errno(_("could not lock HEAD"));
 		rollback_lock_file(&head_lock);
-		return error_errno(_("could not lock HEAD"));
+		return -1;
 	}
 	strbuf_addf(&buf, "%s\n", head);
 	written = write_in_full(fd, buf.buf, buf.len);
 	strbuf_release(&buf);
 	if (written < 0) {
+		error_errno(_("could not write to '%s'"), git_path_head_file());
 		rollback_lock_file(&head_lock);
-		return error_errno(_("could not write to '%s'"),
-				   git_path_head_file());
+		return -1;
 	}
 	if (commit_lock_file(&head_lock) < 0) {
 		rollback_lock_file(&head_lock);
-- 
2.16.1.windows.4



^ permalink raw reply	[flat|nested] 276+ messages in thread

* [PATCH v4 02/12] sequencer: make rearrange_squash() a bit more obvious
  2018-02-23 12:35     ` [PATCH v4 00/12] rebase -i: offer to recreate merge commits Johannes Schindelin
  2018-02-23 12:35       ` [PATCH v4 01/12] sequencer: avoid using errno clobbered by rollback_lock_file() Johannes Schindelin
@ 2018-02-23 12:36       ` Johannes Schindelin
  2018-02-23 12:36       ` [PATCH v4 03/12] git-rebase--interactive: clarify arguments Johannes Schindelin
                         ` (10 subsequent siblings)
  12 siblings, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-02-23 12:36 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Jacob Keller, Stefan Beller, Philip Oakley, Eric Sunshine, Phillip Wood

There are some commands that have to be skipped from rearranging by virtue
of not handling any commits.

However, the logic was not quite obvious: it skipped commands based on
their position in the enum todo_command.

Instead, let's make it explicit that we skip all commands that do not
handle any commit. With one exception: the `drop` command, because it,
well, drops the commit and is therefore not eligible to rearranging.

Note: this is a bit academic at the moment because the only time we call
`rearrange_squash()` is directly after generating the todo list, when we
have nothing but `pick` commands anyway.

However, the upcoming `merge` command *will* want to be handled by that
function, and it *can* handle commits.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 sequencer.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/sequencer.c b/sequencer.c
index 5aa3dc3c95c..cfa01d3bdd2 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -3412,7 +3412,7 @@ int rearrange_squash(void)
 		struct subject2item_entry *entry;
 
 		next[i] = tail[i] = -1;
-		if (item->command >= TODO_EXEC) {
+		if (!item->commit || item->command == TODO_DROP) {
 			subjects[i] = NULL;
 			continue;
 		}
-- 
2.16.1.windows.4



^ permalink raw reply	[flat|nested] 276+ messages in thread

* [PATCH v4 03/12] git-rebase--interactive: clarify arguments
  2018-02-23 12:35     ` [PATCH v4 00/12] rebase -i: offer to recreate merge commits Johannes Schindelin
  2018-02-23 12:35       ` [PATCH v4 01/12] sequencer: avoid using errno clobbered by rollback_lock_file() Johannes Schindelin
  2018-02-23 12:36       ` [PATCH v4 02/12] sequencer: make rearrange_squash() a bit more obvious Johannes Schindelin
@ 2018-02-23 12:36       ` Johannes Schindelin
  2018-02-23 12:37       ` [PATCH v4 04/12] sequencer: introduce new commands to reset the revision Johannes Schindelin
                         ` (9 subsequent siblings)
  12 siblings, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-02-23 12:36 UTC (permalink / raw)
  To: git; +Cc: Stefan Beller, Junio C Hamano, Jacob Keller, Stefan Beller, Philip Oakley, Eric Sunshine, Phillip Wood

From: Stefan Beller <stefanbeller@gmail.com>

Up to now each command took a commit as its first argument and ignored
the rest of the line (usually the subject of the commit)

Now that we are about to introduce commands that take different
arguments, clarify each command by giving the argument list.

Signed-off-by: Stefan Beller <sbeller@google.com>
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 git-rebase--interactive.sh | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh
index 81c5b428757..a2659fea982 100644
--- a/git-rebase--interactive.sh
+++ b/git-rebase--interactive.sh
@@ -155,13 +155,13 @@ reschedule_last_action () {
 append_todo_help () {
 	gettext "
 Commands:
-p, pick = use commit
-r, reword = use commit, but edit the commit message
-e, edit = use commit, but stop for amending
-s, squash = use commit, but meld into previous commit
-f, fixup = like \"squash\", but discard this commit's log message
-x, exec = run command (the rest of the line) using shell
-d, drop = remove commit
+p, pick <commit> = use commit
+r, reword <commit> = use commit, but edit the commit message
+e, edit <commit> = use commit, but stop for amending
+s, squash <commit> = use commit, but meld into previous commit
+f, fixup <commit> = like \"squash\", but discard this commit's log message
+x, exec <commit> = run command (the rest of the line) using shell
+d, drop <commit> = remove commit
 
 These lines can be re-ordered; they are executed from top to bottom.
 " | git stripspace --comment-lines >>"$todo"
-- 
2.16.1.windows.4



^ permalink raw reply	[flat|nested] 276+ messages in thread

* [PATCH v4 04/12] sequencer: introduce new commands to reset the revision
  2018-02-23 12:35     ` [PATCH v4 00/12] rebase -i: offer to recreate merge commits Johannes Schindelin
                         ` (2 preceding siblings ...)
  2018-02-23 12:36       ` [PATCH v4 03/12] git-rebase--interactive: clarify arguments Johannes Schindelin
@ 2018-02-23 12:37       ` Johannes Schindelin
  2018-02-23 12:37       ` [PATCH v4 05/12] sequencer: introduce the `merge` command Johannes Schindelin
                         ` (8 subsequent siblings)
  12 siblings, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-02-23 12:37 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Jacob Keller, Stefan Beller, Philip Oakley, Eric Sunshine, Phillip Wood

In the upcoming commits, we will teach the sequencer to recreate merges.
This will be done in a very different way from the unfortunate design of
`git rebase --preserve-merges` (which does not allow for reordering
commits, or changing the branch topology).

The main idea is to introduce new todo list commands, to support
labeling the current revision with a given name, resetting the current
revision to a previous state, and  merging labeled revisions.

This idea was developed in Git for Windows' Git garden shears (that are
used to maintain the "thicket of branches" on top of upstream Git), and
this patch is part of the effort to make it available to a wider
audience, as well as to make the entire process more robust (by
implementing it in a safe and portable language rather than a Unix shell
script).

This commit implements the commands to label, and to reset to, given
revisions. The syntax is:

	label <name>
	reset <name>

Internally, the `label <name>` command creates the ref
`refs/rewritten/<name>`. This makes it possible to work with the labeled
revisions interactively, or in a scripted fashion (e.g. via the todo
list command `exec`).

These temporary refs are removed upon sequencer_remove_state(), so that
even a `git rebase --abort` cleans them up.

We disallow '#' as label because that character will be used as separator
in the upcoming `merge` command.

Later in this patch series, we will mark the `refs/rewritten/` refs as
worktree-local, to allow for interactive rebases to be run in parallel in
worktrees linked to the same repository.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 git-rebase--interactive.sh |   2 +
 sequencer.c                | 196 +++++++++++++++++++++++++++++++++++++++++++--
 2 files changed, 192 insertions(+), 6 deletions(-)

diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh
index a2659fea982..501f09b28c4 100644
--- a/git-rebase--interactive.sh
+++ b/git-rebase--interactive.sh
@@ -162,6 +162,8 @@ s, squash <commit> = use commit, but meld into previous commit
 f, fixup <commit> = like \"squash\", but discard this commit's log message
 x, exec <commit> = run command (the rest of the line) using shell
 d, drop <commit> = remove commit
+l, label <label> = label current HEAD with a name
+t, reset <label> = reset HEAD to a label
 
 These lines can be re-ordered; they are executed from top to bottom.
 " | git stripspace --comment-lines >>"$todo"
diff --git a/sequencer.c b/sequencer.c
index cfa01d3bdd2..e25522ecdf1 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -23,6 +23,8 @@
 #include "hashmap.h"
 #include "notes-utils.h"
 #include "sigchain.h"
+#include "unpack-trees.h"
+#include "worktree.h"
 
 #define GIT_REFLOG_ACTION "GIT_REFLOG_ACTION"
 
@@ -120,6 +122,13 @@ static GIT_PATH_FUNC(rebase_path_stopped_sha, "rebase-merge/stopped-sha")
 static GIT_PATH_FUNC(rebase_path_rewritten_list, "rebase-merge/rewritten-list")
 static GIT_PATH_FUNC(rebase_path_rewritten_pending,
 	"rebase-merge/rewritten-pending")
+
+/*
+ * The path of the file listing refs that need to be deleted after the rebase
+ * finishes. This is used by the `label` command to record the need for cleanup.
+ */
+static GIT_PATH_FUNC(rebase_path_refs_to_delete, "rebase-merge/refs-to-delete")
+
 /*
  * The following files are written by git-rebase just after parsing the
  * command-line (and are only consumed, not modified, by the sequencer).
@@ -244,18 +253,33 @@ static const char *gpg_sign_opt_quoted(struct replay_opts *opts)
 
 int sequencer_remove_state(struct replay_opts *opts)
 {
-	struct strbuf dir = STRBUF_INIT;
+	struct strbuf buf = STRBUF_INIT;
 	int i;
 
+	if (strbuf_read_file(&buf, rebase_path_refs_to_delete(), 0) > 0) {
+		char *p = buf.buf;
+		while (*p) {
+			char *eol = strchr(p, '\n');
+			if (eol)
+				*eol = '\0';
+			if (delete_ref("(rebase -i) cleanup", p, NULL, 0) < 0)
+				warning(_("could not delete '%s'"), p);
+			if (!eol)
+				break;
+			p = eol + 1;
+		}
+	}
+
 	free(opts->gpg_sign);
 	free(opts->strategy);
 	for (i = 0; i < opts->xopts_nr; i++)
 		free(opts->xopts[i]);
 	free(opts->xopts);
 
-	strbuf_addstr(&dir, get_dir(opts));
-	remove_dir_recursively(&dir, 0);
-	strbuf_release(&dir);
+	strbuf_reset(&buf);
+	strbuf_addstr(&buf, get_dir(opts));
+	remove_dir_recursively(&buf, 0);
+	strbuf_release(&buf);
 
 	return 0;
 }
@@ -1280,6 +1304,8 @@ enum todo_command {
 	TODO_SQUASH,
 	/* commands that do something else than handling a single commit */
 	TODO_EXEC,
+	TODO_LABEL,
+	TODO_RESET,
 	/* commands that do nothing but are counted for reporting progress */
 	TODO_NOOP,
 	TODO_DROP,
@@ -1298,6 +1324,8 @@ static struct {
 	{ 'f', "fixup" },
 	{ 's', "squash" },
 	{ 'x', "exec" },
+	{ 'l', "label" },
+	{ 't', "reset" },
 	{ 0,   "noop" },
 	{ 'd', "drop" },
 	{ 0,   NULL }
@@ -1803,7 +1831,8 @@ static int parse_insn_line(struct todo_item *item, const char *bol, char *eol)
 		return error(_("missing arguments for %s"),
 			     command_to_string(item->command));
 
-	if (item->command == TODO_EXEC) {
+	if (item->command == TODO_EXEC || item->command == TODO_LABEL ||
+	    item->command == TODO_RESET) {
 		item->commit = NULL;
 		item->arg = bol;
 		item->arg_len = (int)(eol - bol);
@@ -2444,6 +2473,157 @@ static int do_exec(const char *command_line)
 	return status;
 }
 
+static int safe_append(const char *filename, const char *fmt, ...)
+{
+	va_list ap;
+	struct lock_file lock = LOCK_INIT;
+	int fd = hold_lock_file_for_update(&lock, filename,
+					   LOCK_REPORT_ON_ERROR);
+	struct strbuf buf = STRBUF_INIT;
+
+	if (fd < 0)
+		return -1;
+
+	if (strbuf_read_file(&buf, filename, 0) < 0 && errno != ENOENT)
+		return error_errno(_("could not read '%s'"), filename);
+	strbuf_complete(&buf, '\n');
+	va_start(ap, fmt);
+	strbuf_vaddf(&buf, fmt, ap);
+	va_end(ap);
+
+	if (write_in_full(fd, buf.buf, buf.len) < 0) {
+		error_errno(_("could not write to '%s'"), filename);
+		strbuf_release(&buf);
+		rollback_lock_file(&lock);
+		return -1;
+	}
+	if (commit_lock_file(&lock) < 0) {
+		strbuf_release(&buf);
+		rollback_lock_file(&lock);
+		return error(_("failed to finalize '%s'"), filename);
+	}
+
+	strbuf_release(&buf);
+	return 0;
+}
+
+static int do_label(const char *name, int len)
+{
+	struct ref_store *refs = get_main_ref_store();
+	struct ref_transaction *transaction;
+	struct strbuf ref_name = STRBUF_INIT, err = STRBUF_INIT;
+	struct strbuf msg = STRBUF_INIT;
+	int ret = 0;
+	struct object_id head_oid;
+
+	if (len == 1 && *name == '#')
+		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);
+
+	transaction = ref_store_transaction_begin(refs, &err);
+	if (!transaction) {
+		error("%s", err.buf);
+		ret = -1;
+	} else if (get_oid("HEAD", &head_oid)) {
+		error(_("could not read HEAD"));
+		ret = -1;
+	} else if (ref_transaction_update(transaction, ref_name.buf, &head_oid,
+					  NULL, 0, msg.buf, &err) < 0 ||
+		   ref_transaction_commit(transaction, &err)) {
+		error("%s", err.buf);
+		ret = -1;
+	}
+	ref_transaction_free(transaction);
+	strbuf_release(&err);
+	strbuf_release(&msg);
+
+	if (!ret)
+		ret = safe_append(rebase_path_refs_to_delete(),
+				  "%s\n", ref_name.buf);
+	strbuf_release(&ref_name);
+
+	return ret;
+}
+
+static int do_reset(const char *name, int len, struct replay_opts *opts)
+{
+	struct strbuf ref_name = STRBUF_INIT;
+	struct object_id oid;
+	struct lock_file lock = LOCK_INIT;
+	struct tree_desc desc;
+	struct tree *tree;
+	struct unpack_trees_options unpack_tree_opts;
+	int ret = 0, i;
+
+	if (hold_locked_index(&lock, LOCK_REPORT_ON_ERROR) < 0)
+		return -1;
+
+	/* Determine the length of the label */
+	for (i = 0; i < len; i++)
+		if (isspace(name[i]))
+			len = i;
+
+	strbuf_addf(&ref_name, "refs/rewritten/%.*s", len, name);
+	if (get_oid(ref_name.buf, &oid) &&
+	    get_oid(ref_name.buf + strlen("refs/rewritten/"), &oid)) {
+		error(_("could not read '%s'"), ref_name.buf);
+		rollback_lock_file(&lock);
+		strbuf_release(&ref_name);
+		return -1;
+	}
+
+	memset(&unpack_tree_opts, 0, sizeof(unpack_tree_opts));
+	unpack_tree_opts.head_idx = 1;
+	unpack_tree_opts.src_index = &the_index;
+	unpack_tree_opts.dst_index = &the_index;
+	unpack_tree_opts.fn = oneway_merge;
+	unpack_tree_opts.merge = 1;
+	unpack_tree_opts.update = 1;
+	unpack_tree_opts.reset = 1;
+
+	if (read_cache_unmerged()) {
+		rollback_lock_file(&lock);
+		strbuf_release(&ref_name);
+		return error_resolve_conflict(_(action_name(opts)));
+	}
+
+	if (!fill_tree_descriptor(&desc, &oid)) {
+		error(_("failed to find tree of %s"), oid_to_hex(&oid));
+		rollback_lock_file(&lock);
+		free((void *)desc.buffer);
+		strbuf_release(&ref_name);
+		return -1;
+	}
+
+	if (unpack_trees(1, &desc, &unpack_tree_opts)) {
+		rollback_lock_file(&lock);
+		free((void *)desc.buffer);
+		strbuf_release(&ref_name);
+		return -1;
+	}
+
+	tree = parse_tree_indirect(&oid);
+	prime_cache_tree(&the_index, tree);
+
+	if (write_locked_index(&the_index, &lock, COMMIT_LOCK) < 0)
+		ret = error(_("could not write index"));
+	free((void *)desc.buffer);
+
+	if (!ret) {
+		struct strbuf msg = STRBUF_INIT;
+
+		strbuf_addf(&msg, "(rebase -i) reset '%.*s'", len, name);
+		ret = update_ref(msg.buf, "HEAD", &oid, NULL, 0,
+				 UPDATE_REFS_MSG_ON_ERR);
+		strbuf_release(&msg);
+	}
+
+	strbuf_release(&ref_name);
+	return ret;
+}
+
 static int is_final_fixup(struct todo_list *todo_list)
 {
 	int i = todo_list->current;
@@ -2627,7 +2807,11 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts)
 				/* `current` will be incremented below */
 				todo_list->current = -1;
 			}
-		} else if (!is_noop(item->command))
+		} else if (item->command == TODO_LABEL)
+			res = do_label(item->arg, item->arg_len);
+		else if (item->command == TODO_RESET)
+			res = do_reset(item->arg, item->arg_len, opts);
+		else if (!is_noop(item->command))
 			return error(_("unknown command %d"), item->command);
 
 		todo_list->current++;
-- 
2.16.1.windows.4



^ permalink raw reply	[flat|nested] 276+ messages in thread

* [PATCH v4 05/12] sequencer: introduce the `merge` command
  2018-02-23 12:35     ` [PATCH v4 00/12] rebase -i: offer to recreate merge commits Johannes Schindelin
                         ` (3 preceding siblings ...)
  2018-02-23 12:37       ` [PATCH v4 04/12] sequencer: introduce new commands to reset the revision Johannes Schindelin
@ 2018-02-23 12:37       ` Johannes Schindelin
  2018-02-23 12:37       ` [PATCH v4 06/12] sequencer: fast-forward merge commits, if possible Johannes Schindelin
                         ` (7 subsequent siblings)
  12 siblings, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-02-23 12:37 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Jacob Keller, Stefan Beller, Philip Oakley, Eric Sunshine, Phillip Wood

This patch is part of the effort to reimplement `--preserve-merges` with
a substantially improved design, a design that has been developed in the
Git for Windows project to maintain the dozens of Windows-specific patch
series on top of upstream Git.

The previous patch implemented the `label` and `reset` commands to label
commits and to reset to labeled commits. This patch adds the `merge`
command, with the following syntax:

	merge [-C <commit>] <rev> # <oneline>

The <commit> parameter in this instance is the *original* merge commit,
whose author and message will be used for the merge commit that is about
to be created.

The <rev> parameter refers to the (possibly rewritten) revision to
merge. Let's see an example of a todo list:

	label onto

	# Branch abc
	reset onto
	pick deadbeef Hello, world!
	label abc

	reset onto
	pick cafecafe And now for something completely different
	merge -C baaabaaa abc # Merge the branch 'abc' into master

To edit the merge commit's message (a "reword" for merges, if you will),
use `-c` (lower-case) instead of `-C`; this convention was borrowed from
`git commit` that also supports `-c` and `-C` with similar meanings.

To create *new* merges, i.e. without copying the commit message from an
existing commit, simply omit the `-C <commit>` parameter (which will
open an editor for the merge message):

	merge abc

This comes in handy when splitting a branch into two or more branches.

Note: this patch only adds support for recursive merges, to keep things
simple. Support for octopus merges will be added later in a separate
patch series, support for merges using strategies other than the
recursive merge is left for the future.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 git-rebase--interactive.sh |   4 ++
 sequencer.c                | 158 +++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 162 insertions(+)

diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh
index 501f09b28c4..2d8bbe20b74 100644
--- a/git-rebase--interactive.sh
+++ b/git-rebase--interactive.sh
@@ -164,6 +164,10 @@ x, exec <commit> = run command (the rest of the line) using shell
 d, drop <commit> = remove commit
 l, label <label> = label current HEAD with a name
 t, reset <label> = reset HEAD to a label
+m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
+.       create a merge commit using the original merge commit's
+.       message (or the oneline, if no original merge commit was
+.       specified). Use -c <commit> to reword the commit message.
 
 These lines can be re-ordered; they are executed from top to bottom.
 " | git stripspace --comment-lines >>"$todo"
diff --git a/sequencer.c b/sequencer.c
index e25522ecdf1..64dbd1d3e2e 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -1306,6 +1306,8 @@ enum todo_command {
 	TODO_EXEC,
 	TODO_LABEL,
 	TODO_RESET,
+	TODO_MERGE,
+	TODO_MERGE_AND_EDIT,
 	/* commands that do nothing but are counted for reporting progress */
 	TODO_NOOP,
 	TODO_DROP,
@@ -1326,6 +1328,8 @@ static struct {
 	{ 'x', "exec" },
 	{ 'l', "label" },
 	{ 't', "reset" },
+	{ 'm', "merge" },
+	{ 0, "merge" }, /* MERGE_AND_EDIT */
 	{ 0,   "noop" },
 	{ 'd', "drop" },
 	{ 0,   NULL }
@@ -1839,6 +1843,21 @@ static int parse_insn_line(struct todo_item *item, const char *bol, char *eol)
 		return 0;
 	}
 
+	if (item->command == TODO_MERGE) {
+		if (skip_prefix(bol, "-C", &bol))
+			bol += strspn(bol, " \t");
+		else if (skip_prefix(bol, "-c", &bol)) {
+			bol += strspn(bol, " \t");
+			item->command = TODO_MERGE_AND_EDIT;
+		} else {
+			item->command = TODO_MERGE_AND_EDIT;
+			item->commit = NULL;
+			item->arg = bol;
+			item->arg_len = (int)(eol - bol);
+			return 0;
+		}
+	}
+
 	end_of_object_name = (char *) bol + strcspn(bol, " \t\n");
 	saved = *end_of_object_name;
 	*end_of_object_name = '\0';
@@ -2624,6 +2643,134 @@ static int do_reset(const char *name, int len, struct replay_opts *opts)
 	return ret;
 }
 
+static int do_merge(struct commit *commit, const char *arg, int arg_len,
+		    int run_commit_flags, struct replay_opts *opts)
+{
+	int merge_arg_len;
+	struct strbuf ref_name = STRBUF_INIT;
+	struct commit *head_commit, *merge_commit, *i;
+	struct commit_list *common, *j, *reversed = NULL;
+	struct merge_options o;
+	int ret;
+	static struct lock_file lock;
+
+	for (merge_arg_len = 0; merge_arg_len < arg_len; merge_arg_len++)
+		if (isspace(arg[merge_arg_len]))
+			break;
+
+	if (hold_locked_index(&lock, LOCK_REPORT_ON_ERROR) < 0)
+		return -1;
+
+	head_commit = lookup_commit_reference_by_name("HEAD");
+	if (!head_commit) {
+		rollback_lock_file(&lock);
+		return error(_("cannot merge without a current revision"));
+	}
+
+	if (commit) {
+		const char *message = get_commit_buffer(commit, NULL);
+		const char *body;
+		int len;
+
+		if (!message) {
+			rollback_lock_file(&lock);
+			return error(_("could not get commit message of '%s'"),
+				     oid_to_hex(&commit->object.oid));
+		}
+		write_author_script(message);
+		find_commit_subject(message, &body);
+		len = strlen(body);
+		if (write_message(body, len, git_path_merge_msg(), 0) < 0) {
+			error_errno(_("could not write '%s'"),
+				    git_path_merge_msg());
+			unuse_commit_buffer(commit, message);
+			rollback_lock_file(&lock);
+			return -1;
+		}
+		unuse_commit_buffer(commit, message);
+	} else {
+		const char *p = arg + merge_arg_len;
+		struct strbuf buf = STRBUF_INIT;
+		int len;
+
+		strbuf_addf(&buf, "author %s", git_author_info(0));
+		write_author_script(buf.buf);
+		strbuf_reset(&buf);
+
+		p += strspn(p, " \t");
+		if (*p == '#' && isspace(p[1]))
+			p += 1 + strspn(p + 1, " \t");
+		if (*p)
+			len = strlen(p);
+		else {
+			strbuf_addf(&buf, "Merge branch '%.*s'",
+				    merge_arg_len, arg);
+			p = buf.buf;
+			len = buf.len;
+		}
+
+		if (write_message(p, len, git_path_merge_msg(), 0) < 0) {
+			error_errno(_("could not write '%s'"),
+				    git_path_merge_msg());
+			strbuf_release(&buf);
+			rollback_lock_file(&lock);
+			return -1;
+		}
+		strbuf_release(&buf);
+	}
+
+	strbuf_addf(&ref_name, "refs/rewritten/%.*s", merge_arg_len, arg);
+	merge_commit = lookup_commit_reference_by_name(ref_name.buf);
+	if (!merge_commit) {
+		/* fall back to non-rewritten ref or commit */
+		strbuf_splice(&ref_name, 0, strlen("refs/rewritten/"), "", 0);
+		merge_commit = lookup_commit_reference_by_name(ref_name.buf);
+	}
+	if (!merge_commit) {
+		error(_("could not resolve '%s'"), ref_name.buf);
+		strbuf_release(&ref_name);
+		rollback_lock_file(&lock);
+		return -1;
+	}
+	write_message(oid_to_hex(&merge_commit->object.oid), GIT_SHA1_HEXSZ,
+		      git_path_merge_head(), 0);
+	write_message("no-ff", 5, git_path_merge_mode(), 0);
+
+	common = get_merge_bases(head_commit, merge_commit);
+	for (j = common; j; j = j->next)
+		commit_list_insert(j->item, &reversed);
+	free_commit_list(common);
+
+	read_cache();
+	init_merge_options(&o);
+	o.branch1 = "HEAD";
+	o.branch2 = ref_name.buf;
+	o.buffer_output = 2;
+
+	ret = merge_recursive(&o, head_commit, merge_commit, reversed, &i);
+	if (ret <= 0)
+		fputs(o.obuf.buf, stdout);
+	strbuf_release(&o.obuf);
+	if (ret < 0) {
+		strbuf_release(&ref_name);
+		rollback_lock_file(&lock);
+		return error(_("conflicts while merging '%.*s'"),
+			     merge_arg_len, arg);
+	}
+
+	if (active_cache_changed &&
+	    write_locked_index(&the_index, &lock, COMMIT_LOCK)) {
+		strbuf_release(&ref_name);
+		return error(_("merge: Unable to write new index file"));
+	}
+	rollback_lock_file(&lock);
+
+	ret = run_git_commit(git_path_merge_msg(), opts, run_commit_flags);
+	strbuf_release(&ref_name);
+
+	return ret;
+}
+
 static int is_final_fixup(struct todo_list *todo_list)
 {
 	int i = todo_list->current;
@@ -2811,6 +2958,11 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts)
 			res = do_label(item->arg, item->arg_len);
 		else if (item->command == TODO_RESET)
 			res = do_reset(item->arg, item->arg_len, opts);
+		else if (item->command == TODO_MERGE ||
+			 item->command == TODO_MERGE_AND_EDIT)
+			res = do_merge(item->commit, item->arg, item->arg_len,
+				       item->command == TODO_MERGE_AND_EDIT ?
+				       EDIT_MSG | VERIFY_MSG : 0, opts);
 		else if (!is_noop(item->command))
 			return error(_("unknown command %d"), item->command);
 
@@ -3292,8 +3444,14 @@ int transform_todos(unsigned flags)
 					  short_commit_name(item->commit) :
 					  oid_to_hex(&item->commit->object.oid);
 
+			if (item->command == TODO_MERGE)
+				strbuf_addstr(&buf, " -C");
+			else if (item->command == TODO_MERGE_AND_EDIT)
+				strbuf_addstr(&buf, " -c");
+
 			strbuf_addf(&buf, " %s", oid);
 		}
+
 		/* add all the rest */
 		if (!item->arg_len)
 			strbuf_addch(&buf, '\n');
-- 
2.16.1.windows.4



^ permalink raw reply	[flat|nested] 276+ messages in thread

* [PATCH v4 06/12] sequencer: fast-forward merge commits, if possible
  2018-02-23 12:35     ` [PATCH v4 00/12] rebase -i: offer to recreate merge commits Johannes Schindelin
                         ` (4 preceding siblings ...)
  2018-02-23 12:37       ` [PATCH v4 05/12] sequencer: introduce the `merge` command Johannes Schindelin
@ 2018-02-23 12:37       ` Johannes Schindelin
  2018-02-23 12:38       ` [PATCH v4 07/12] rebase-helper --make-script: introduce a flag to recreate merges Johannes Schindelin
                         ` (6 subsequent siblings)
  12 siblings, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-02-23 12:37 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Jacob Keller, Stefan Beller, Philip Oakley, Eric Sunshine, Phillip Wood

Just like with regular `pick` commands, if we are trying to recreate a
merge commit, we now test whether the parents of said commit match HEAD
and the commits to be merged, and fast-forward if possible.

This is not only faster, but also avoids unnecessary proliferation of
new objects.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 sequencer.c | 21 ++++++++++++++++++++-
 1 file changed, 20 insertions(+), 1 deletion(-)

diff --git a/sequencer.c b/sequencer.c
index 64dbd1d3e2e..361ec98f764 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -2651,7 +2651,7 @@ static int do_merge(struct commit *commit, const char *arg, int arg_len,
 	struct commit *head_commit, *merge_commit, *i;
 	struct commit_list *common, *j, *reversed = NULL;
 	struct merge_options o;
-	int ret;
+	int can_fast_forward, ret;
 	static struct lock_file lock;
 
 	for (merge_arg_len = 0; merge_arg_len < arg_len; merge_arg_len++)
@@ -2719,6 +2719,14 @@ static int do_merge(struct commit *commit, const char *arg, int arg_len,
 		strbuf_release(&buf);
 	}
 
+	/*
+	 * If HEAD is not identical to the parent of the original merge commit,
+	 * we cannot fast-forward.
+	 */
+	can_fast_forward = opts->allow_ff && commit && commit->parents &&
+		!oidcmp(&commit->parents->item->object.oid,
+			&head_commit->object.oid);
+
 	strbuf_addf(&ref_name, "refs/rewritten/%.*s", merge_arg_len, arg);
 	merge_commit = lookup_commit_reference_by_name(ref_name.buf);
 	if (!merge_commit) {
@@ -2732,6 +2740,17 @@ static int do_merge(struct commit *commit, const char *arg, int arg_len,
 		rollback_lock_file(&lock);
 		return -1;
 	}
+
+	if (can_fast_forward && commit->parents->next &&
+	    !commit->parents->next->next &&
+	    !oidcmp(&commit->parents->next->item->object.oid,
+		    &merge_commit->object.oid)) {
+		strbuf_release(&ref_name);
+		rollback_lock_file(&lock);
+		return fast_forward_to(&commit->object.oid,
+				       &head_commit->object.oid, 0, opts);
+	}
+
 	write_message(oid_to_hex(&merge_commit->object.oid), GIT_SHA1_HEXSZ,
 		      git_path_merge_head(), 0);
 	write_message("no-ff", 5, git_path_merge_mode(), 0);
-- 
2.16.1.windows.4



^ permalink raw reply	[flat|nested] 276+ messages in thread

* [PATCH v4 07/12] rebase-helper --make-script: introduce a flag to recreate merges
  2018-02-23 12:35     ` [PATCH v4 00/12] rebase -i: offer to recreate merge commits Johannes Schindelin
                         ` (5 preceding siblings ...)
  2018-02-23 12:37       ` [PATCH v4 06/12] sequencer: fast-forward merge commits, if possible Johannes Schindelin
@ 2018-02-23 12:38       ` Johannes Schindelin
  2018-02-23 12:38       ` [PATCH v4 08/12] rebase: introduce the --recreate-merges option Johannes Schindelin
                         ` (5 subsequent siblings)
  12 siblings, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-02-23 12:38 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Jacob Keller, Stefan Beller, Philip Oakley, Eric Sunshine, Phillip Wood

The sequencer just learned new commands intended to recreate branch
structure (similar in spirit to --preserve-merges, but with a
substantially less-broken design).

Let's allow the rebase--helper to generate todo lists making use of
these commands, triggered by the new --recreate-merges option. For a
commit topology like this (where the HEAD points to C):

	- A - B - C
	    \   /
	      D

the generated todo list would look like this:

	# branch D
	pick 0123 A
	label branch-point
	pick 1234 D
	label D

	reset branch-point
	pick 2345 B
	merge -C 3456 D # C

To keep things simple, we first only implement support for merge commits
with exactly two parents, leaving support for octopus merges to a later
patch in this patch series.

As a special, hard-coded label, all merge-recreating todo lists start with
the command `label onto` so that we can later always refer to the revision
onto which everything is rebased.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 builtin/rebase--helper.c |   4 +-
 sequencer.c              | 349 ++++++++++++++++++++++++++++++++++++++++++++++-
 sequencer.h              |   1 +
 3 files changed, 351 insertions(+), 3 deletions(-)

diff --git a/builtin/rebase--helper.c b/builtin/rebase--helper.c
index ad074705bb5..a5b07c43c96 100644
--- a/builtin/rebase--helper.c
+++ b/builtin/rebase--helper.c
@@ -12,7 +12,7 @@ static const char * const builtin_rebase_helper_usage[] = {
 int cmd_rebase__helper(int argc, const char **argv, const char *prefix)
 {
 	struct replay_opts opts = REPLAY_OPTS_INIT;
-	unsigned flags = 0, keep_empty = 0;
+	unsigned flags = 0, keep_empty = 0, recreate_merges = 0;
 	int abbreviate_commands = 0;
 	enum {
 		CONTINUE = 1, ABORT, MAKE_SCRIPT, SHORTEN_OIDS, EXPAND_OIDS,
@@ -24,6 +24,7 @@ int cmd_rebase__helper(int argc, const char **argv, const char *prefix)
 		OPT_BOOL(0, "keep-empty", &keep_empty, N_("keep empty commits")),
 		OPT_BOOL(0, "allow-empty-message", &opts.allow_empty_message,
 			N_("allow commits with empty messages")),
+		OPT_BOOL(0, "recreate-merges", &recreate_merges, N_("recreate merge commits")),
 		OPT_CMDMODE(0, "continue", &command, N_("continue rebase"),
 				CONTINUE),
 		OPT_CMDMODE(0, "abort", &command, N_("abort rebase"),
@@ -57,6 +58,7 @@ int cmd_rebase__helper(int argc, const char **argv, const char *prefix)
 
 	flags |= keep_empty ? TODO_LIST_KEEP_EMPTY : 0;
 	flags |= abbreviate_commands ? TODO_LIST_ABBREVIATE_CMDS : 0;
+	flags |= recreate_merges ? TODO_LIST_RECREATE_MERGES : 0;
 	flags |= command == SHORTEN_OIDS ? TODO_LIST_SHORTEN_IDS : 0;
 
 	if (command == CONTINUE && argc == 1)
diff --git a/sequencer.c b/sequencer.c
index 361ec98f764..01bafe2fe47 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -25,6 +25,8 @@
 #include "sigchain.h"
 #include "unpack-trees.h"
 #include "worktree.h"
+#include "oidmap.h"
+#include "oidset.h"
 
 #define GIT_REFLOG_ACTION "GIT_REFLOG_ACTION"
 
@@ -3336,6 +3338,341 @@ void append_signoff(struct strbuf *msgbuf, int ignore_footer, unsigned flag)
 	strbuf_release(&sob);
 }
 
+struct labels_entry {
+	struct hashmap_entry entry;
+	char label[FLEX_ARRAY];
+};
+
+static int labels_cmp(const void *fndata, const struct labels_entry *a,
+		      const struct labels_entry *b, const void *key)
+{
+	return key ? strcmp(a->label, key) : strcmp(a->label, b->label);
+}
+
+struct string_entry {
+	struct oidmap_entry entry;
+	char string[FLEX_ARRAY];
+};
+
+struct label_state {
+	struct oidmap commit2label;
+	struct hashmap labels;
+	struct strbuf buf;
+};
+
+static const char *label_oid(struct object_id *oid, const char *label,
+			     struct label_state *state)
+{
+	struct labels_entry *labels_entry;
+	struct string_entry *string_entry;
+	struct object_id dummy;
+	size_t len;
+	int i;
+
+	string_entry = oidmap_get(&state->commit2label, oid);
+	if (string_entry)
+		return string_entry->string;
+
+	/*
+	 * For "uninteresting" commits, i.e. commits that are not to be
+	 * rebased, and which can therefore not be labeled, we use a unique
+	 * abbreviation of the commit name. This is slightly more complicated
+	 * than calling find_unique_abbrev() because we also need to make
+	 * sure that the abbreviation does not conflict with any other
+	 * label.
+	 *
+	 * We disallow "interesting" commits to be labeled by a string that
+	 * is a valid full-length hash, to ensure that we always can find an
+	 * abbreviation for any uninteresting commit's names that does not
+	 * clash with any other label.
+	 */
+	if (!label) {
+		char *p;
+
+		strbuf_reset(&state->buf);
+		strbuf_grow(&state->buf, GIT_SHA1_HEXSZ);
+		label = p = state->buf.buf;
+
+		find_unique_abbrev_r(p, oid->hash, default_abbrev);
+
+		/*
+		 * We may need to extend the abbreviated hash so that there is
+		 * no conflicting label.
+		 */
+		if (hashmap_get_from_hash(&state->labels, strihash(p), p)) {
+			size_t i = strlen(p) + 1;
+
+			oid_to_hex_r(p, oid);
+			for (; i < GIT_SHA1_HEXSZ; i++) {
+				char save = p[i];
+				p[i] = '\0';
+				if (!hashmap_get_from_hash(&state->labels,
+							   strihash(p), p))
+					break;
+				p[i] = save;
+			}
+		}
+	} else if (((len = strlen(label)) == GIT_SHA1_RAWSZ &&
+		    !get_oid_hex(label, &dummy)) ||
+		   (len == 1 && *label == '#') ||
+		   hashmap_get_from_hash(&state->labels,
+					 strihash(label), label)) {
+		/*
+		 * If the label already exists, or if the label is a valid full
+		 * OID, or the label is a '#' (which we use as a separator
+		 * between merge heads and oneline), we append a dash and a
+		 * number to make it unique.
+		 */
+		struct strbuf *buf = &state->buf;
+
+		strbuf_reset(buf);
+		strbuf_add(buf, label, len);
+
+		for (i = 2; ; i++) {
+			strbuf_setlen(buf, len);
+			strbuf_addf(buf, "-%d", i);
+			if (!hashmap_get_from_hash(&state->labels,
+						   strihash(buf->buf),
+						   buf->buf))
+				break;
+		}
+
+		label = buf->buf;
+	}
+
+	FLEX_ALLOC_STR(labels_entry, label, label);
+	hashmap_entry_init(labels_entry, strihash(label));
+	hashmap_add(&state->labels, labels_entry);
+
+	FLEX_ALLOC_STR(string_entry, string, label);
+	oidcpy(&string_entry->entry.oid, oid);
+	oidmap_put(&state->commit2label, string_entry);
+
+	return string_entry->string;
+}
+
+static int make_script_with_merges(struct pretty_print_context *pp,
+				   struct rev_info *revs, FILE *out,
+				   unsigned flags)
+{
+	int keep_empty = flags & TODO_LIST_KEEP_EMPTY;
+	struct strbuf buf = STRBUF_INIT, oneline = STRBUF_INIT;
+	struct strbuf label = STRBUF_INIT;
+	struct commit_list *commits = NULL, **tail = &commits, *iter;
+	struct commit_list *tips = NULL, **tips_tail = &tips;
+	struct commit *commit;
+	struct oidmap commit2todo = OIDMAP_INIT;
+	struct string_entry *entry;
+	struct oidset interesting = OIDSET_INIT, child_seen = OIDSET_INIT,
+		shown = OIDSET_INIT;
+	struct label_state state = { OIDMAP_INIT, { NULL }, STRBUF_INIT };
+
+	int abbr = flags & TODO_LIST_ABBREVIATE_CMDS;
+	const char *cmd_pick = abbr ? "p" : "pick",
+		*cmd_label = abbr ? "l" : "label",
+		*cmd_reset = abbr ? "t" : "reset",
+		*cmd_merge = abbr ? "m" : "merge";
+
+	oidmap_init(&commit2todo, 0);
+	oidmap_init(&state.commit2label, 0);
+	hashmap_init(&state.labels, (hashmap_cmp_fn) labels_cmp, NULL, 0);
+	strbuf_init(&state.buf, 32);
+
+	if (revs->cmdline.nr && (revs->cmdline.rev[0].flags & BOTTOM)) {
+		struct object_id *oid = &revs->cmdline.rev[0].item->oid;
+		FLEX_ALLOC_STR(entry, string, "onto");
+		oidcpy(&entry->entry.oid, oid);
+		oidmap_put(&state.commit2label, entry);
+	}
+
+	/*
+	 * First phase:
+	 * - get onelines for all commits
+	 * - gather all branch tips (i.e. 2nd or later parents of merges)
+	 * - label all branch tips
+	 */
+	while ((commit = get_revision(revs))) {
+		struct commit_list *to_merge;
+		int is_octopus;
+		const char *p1, *p2;
+		struct object_id *oid;
+
+		tail = &commit_list_insert(commit, tail)->next;
+		oidset_insert(&interesting, &commit->object.oid);
+
+		if ((commit->object.flags & PATCHSAME))
+			continue;
+
+		strbuf_reset(&oneline);
+		pretty_print_commit(pp, commit, &oneline);
+
+		to_merge = commit->parents ? commit->parents->next : NULL;
+		if (!to_merge) {
+			/* non-merge commit: easy case */
+			strbuf_reset(&buf);
+			if (!keep_empty && is_original_commit_empty(commit))
+				strbuf_addf(&buf, "%c ", comment_line_char);
+			strbuf_addf(&buf, "%s %s %s", cmd_pick,
+				    oid_to_hex(&commit->object.oid),
+				    oneline.buf);
+
+			FLEX_ALLOC_STR(entry, string, buf.buf);
+			oidcpy(&entry->entry.oid, &commit->object.oid);
+			oidmap_put(&commit2todo, entry);
+
+			continue;
+		}
+
+		is_octopus = to_merge && to_merge->next;
+
+		if (is_octopus)
+			BUG("Octopus merges not yet supported");
+
+		/* Create a label */
+		strbuf_reset(&label);
+		if (skip_prefix(oneline.buf, "Merge ", &p1) &&
+		    (p1 = strchr(p1, '\'')) &&
+		    (p2 = strchr(++p1, '\'')))
+			strbuf_add(&label, p1, p2 - p1);
+		else if (skip_prefix(oneline.buf, "Merge pull request ",
+				     &p1) &&
+			 (p1 = strstr(p1, " from ")))
+			strbuf_addstr(&label, p1 + strlen(" from "));
+		else
+			strbuf_addbuf(&label, &oneline);
+
+		for (p1 = label.buf; *p1; p1++)
+			if (isspace(*p1))
+				*(char *)p1 = '-';
+
+		strbuf_reset(&buf);
+		strbuf_addf(&buf, "%s -C %s",
+			    cmd_merge, oid_to_hex(&commit->object.oid));
+
+		/* label the tip of merged branch */
+		oid = &to_merge->item->object.oid;
+		strbuf_addch(&buf, ' ');
+
+		if (!oidset_contains(&interesting, oid))
+			strbuf_addstr(&buf, label_oid(oid, NULL, &state));
+		else {
+			tips_tail = &commit_list_insert(to_merge->item,
+							tips_tail)->next;
+
+			strbuf_addstr(&buf, label_oid(oid, label.buf, &state));
+		}
+		strbuf_addf(&buf, " # %s", oneline.buf);
+
+		FLEX_ALLOC_STR(entry, string, buf.buf);
+		oidcpy(&entry->entry.oid, &commit->object.oid);
+		oidmap_put(&commit2todo, entry);
+	}
+
+	/*
+	 * Second phase:
+	 * - label branch points
+	 * - add HEAD to the branch tips
+	 */
+	for (iter = commits; iter; iter = iter->next) {
+		struct commit_list *parent = iter->item->parents;
+		for (; parent; parent = parent->next) {
+			struct object_id *oid = &parent->item->object.oid;
+			if (!oidset_contains(&interesting, oid))
+				continue;
+			if (!oidset_contains(&child_seen, oid))
+				oidset_insert(&child_seen, oid);
+			else
+				label_oid(oid, "branch-point", &state);
+		}
+
+		/* Add HEAD as implict "tip of branch" */
+		if (!iter->next)
+			tips_tail = &commit_list_insert(iter->item,
+							tips_tail)->next;
+	}
+
+	/*
+	 * Third phase: output the todo list. This is a bit tricky, as we
+	 * want to avoid jumping back and forth between revisions. To
+	 * accomplish that goal, we walk backwards from the branch tips,
+	 * gathering commits not yet shown, reversing the list on the fly,
+	 * then outputting that list (labeling revisions as needed).
+	 */
+	fprintf(out, "%s onto\n", cmd_label);
+	for (iter = tips; iter; iter = iter->next) {
+		struct commit_list *list = NULL, *iter2;
+
+		commit = iter->item;
+		if (oidset_contains(&shown, &commit->object.oid))
+			continue;
+		entry = oidmap_get(&state.commit2label, &commit->object.oid);
+
+		if (entry)
+			fprintf(out, "\n# Branch %s\n", entry->string);
+		else
+			fprintf(out, "\n");
+
+		while (oidset_contains(&interesting, &commit->object.oid) &&
+		       !oidset_contains(&shown, &commit->object.oid)) {
+			commit_list_insert(commit, &list);
+			if (!commit->parents) {
+				commit = NULL;
+				break;
+			}
+			commit = commit->parents->item;
+		}
+
+		if (!commit)
+			fprintf(out, "%s onto\n", cmd_reset);
+		else {
+			const char *to = NULL;
+
+			entry = oidmap_get(&state.commit2label,
+					   &commit->object.oid);
+			if (entry)
+				to = entry->string;
+
+			if (!to || !strcmp(to, "onto"))
+				fprintf(out, "%s onto\n", cmd_reset);
+			else {
+				strbuf_reset(&oneline);
+				pretty_print_commit(pp, commit, &oneline);
+				fprintf(out, "%s %s # %s\n",
+					cmd_reset, to, oneline.buf);
+			}
+		}
+
+		for (iter2 = list; iter2; iter2 = iter2->next) {
+			struct object_id *oid = &iter2->item->object.oid;
+			entry = oidmap_get(&commit2todo, oid);
+			/* only show if not already upstream */
+			if (entry)
+				fprintf(out, "%s\n", entry->string);
+			entry = oidmap_get(&state.commit2label, oid);
+			if (entry)
+				fprintf(out, "%s %s\n",
+					cmd_label, entry->string);
+			oidset_insert(&shown, oid);
+		}
+
+		free_commit_list(list);
+	}
+
+	free_commit_list(commits);
+	free_commit_list(tips);
+
+	strbuf_release(&label);
+	strbuf_release(&oneline);
+	strbuf_release(&buf);
+
+	oidmap_free(&commit2todo, 1);
+	oidmap_free(&state.commit2label, 1);
+	hashmap_free(&state.labels, 1);
+	strbuf_release(&state.buf);
+
+	return 0;
+}
+
 int sequencer_make_script(FILE *out, int argc, const char **argv,
 			  unsigned flags)
 {
@@ -3346,11 +3683,16 @@ int sequencer_make_script(FILE *out, int argc, const char **argv,
 	struct commit *commit;
 	int keep_empty = flags & TODO_LIST_KEEP_EMPTY;
 	const char *insn = flags & TODO_LIST_ABBREVIATE_CMDS ? "p" : "pick";
+	int recreate_merges = flags & TODO_LIST_RECREATE_MERGES;
 
 	init_revisions(&revs, NULL);
 	revs.verbose_header = 1;
-	revs.max_parents = 1;
-	revs.cherry_pick = 1;
+	if (recreate_merges)
+		revs.cherry_mark = 1;
+	else {
+		revs.max_parents = 1;
+		revs.cherry_pick = 1;
+	}
 	revs.limited = 1;
 	revs.reverse = 1;
 	revs.right_only = 1;
@@ -3374,6 +3716,9 @@ int sequencer_make_script(FILE *out, int argc, const char **argv,
 	if (prepare_revision_walk(&revs) < 0)
 		return error(_("make_script: error preparing revisions"));
 
+	if (recreate_merges)
+		return make_script_with_merges(&pp, &revs, out, flags);
+
 	while ((commit = get_revision(&revs))) {
 		strbuf_reset(&buf);
 		if (!keep_empty && is_original_commit_empty(commit))
diff --git a/sequencer.h b/sequencer.h
index e45b178dfc4..7c7c67d623c 100644
--- a/sequencer.h
+++ b/sequencer.h
@@ -59,6 +59,7 @@ int sequencer_remove_state(struct replay_opts *opts);
 #define TODO_LIST_KEEP_EMPTY (1U << 0)
 #define TODO_LIST_SHORTEN_IDS (1U << 1)
 #define TODO_LIST_ABBREVIATE_CMDS (1U << 2)
+#define TODO_LIST_RECREATE_MERGES (1U << 3)
 int sequencer_make_script(FILE *out, int argc, const char **argv,
 			  unsigned flags);
 
-- 
2.16.1.windows.4



^ permalink raw reply	[flat|nested] 276+ messages in thread

* [PATCH v4 08/12] rebase: introduce the --recreate-merges option
  2018-02-23 12:35     ` [PATCH v4 00/12] rebase -i: offer to recreate merge commits Johannes Schindelin
                         ` (6 preceding siblings ...)
  2018-02-23 12:38       ` [PATCH v4 07/12] rebase-helper --make-script: introduce a flag to recreate merges Johannes Schindelin
@ 2018-02-23 12:38       ` Johannes Schindelin
  2018-02-23 12:38       ` [PATCH v4 09/12] sequencer: make refs generated by the `label` command worktree-local Johannes Schindelin
                         ` (4 subsequent siblings)
  12 siblings, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-02-23 12:38 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Jacob Keller, Stefan Beller, Philip Oakley, Eric Sunshine, Phillip Wood

Once upon a time, this here developer thought: wouldn't it be nice if,
say, Git for Windows' patches on top of core Git could be represented as
a thicket of branches, and be rebased on top of core Git in order to
maintain a cherry-pick'able set of patch series?

The original attempt to answer this was: git rebase --preserve-merges.

However, that experiment was never intended as an interactive option,
and it only piggy-backed on git rebase --interactive because that
command's implementation looked already very, very familiar: it was
designed by the same person who designed --preserve-merges: yours truly.

Some time later, some other developer (I am looking at you, Andreas!
;-)) decided that it would be a good idea to allow --preserve-merges to
be combined with --interactive (with caveats!) and the Git maintainer
(well, the interim Git maintainer during Junio's absence, that is)
agreed, and that is when the glamor of the --preserve-merges design
started to fall apart rather quickly and unglamorously.

The reason? In --preserve-merges mode, the parents of a merge commit (or
for that matter, of *any* commit) were not stated explicitly, but were
*implied* by the commit name passed to the `pick` command.

This made it impossible, for example, to reorder commits. Not to mention
to flatten the branch topology or, deity forbid, to split topic branches
into two.

Alas, these shortcomings also prevented that mode (whose original
purpose was to serve Git for Windows' needs, with the additional hope
that it may be useful to others, too) from serving Git for Windows'
needs.

Five years later, when it became really untenable to have one unwieldy,
big hodge-podge patch series of partly related, partly unrelated patches
in Git for Windows that was rebased onto core Git's tags from time to
time (earning the undeserved wrath of the developer of the ill-fated
git-remote-hg series that first obsoleted Git for Windows' competing
approach, only to be abandoned without maintainer later) was really
untenable, the "Git garden shears" were born [*1*/*2*]: a script,
piggy-backing on top of the interactive rebase, that would first
determine the branch topology of the patches to be rebased, create a
pseudo todo list for further editing, transform the result into a real
todo list (making heavy use of the `exec` command to "implement" the
missing todo list commands) and finally recreate the patch series on
top of the new base commit.

That was in 2013. And it took about three weeks to come up with the
design and implement it as an out-of-tree script. Needless to say, the
implementation needed quite a few years to stabilize, all the while the
design itself proved itself sound.

With this patch, the goodness of the Git garden shears comes to `git
rebase -i` itself. Passing the `--recreate-merges` option will generate
a todo list that can be understood readily, and where it is obvious
how to reorder commits. New branches can be introduced by inserting
`label` commands and calling `merge <label>`. And once this mode will
have become stable and universally accepted, we can deprecate the design
mistake that was `--preserve-merges`.

Link *1*:
https://github.com/msysgit/msysgit/blob/master/share/msysGit/shears.sh
Link *2*:
https://github.com/git-for-windows/build-extra/blob/master/shears.sh

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 Documentation/git-rebase.txt           |   9 +-
 contrib/completion/git-completion.bash |   2 +-
 git-rebase--interactive.sh             |   1 +
 git-rebase.sh                          |   6 ++
 t/t3430-rebase-recreate-merges.sh      | 146 +++++++++++++++++++++++++++++++++
 5 files changed, 162 insertions(+), 2 deletions(-)
 create mode 100755 t/t3430-rebase-recreate-merges.sh

diff --git a/Documentation/git-rebase.txt b/Documentation/git-rebase.txt
index d713951b86a..5e056c8ab6b 100644
--- a/Documentation/git-rebase.txt
+++ b/Documentation/git-rebase.txt
@@ -373,6 +373,12 @@ The commit list format can be changed by setting the configuration option
 rebase.instructionFormat.  A customized instruction format will automatically
 have the long commit hash prepended to the format.
 
+--recreate-merges::
+	Recreate merge commits instead of flattening the history by replaying
+	merges. Merge conflict resolutions or manual amendments to merge
+	commits are not recreated automatically, but have to be recreated
+	manually.
+
 -p::
 --preserve-merges::
 	Recreate merge commits instead of flattening the history by replaying
@@ -775,7 +781,8 @@ BUGS
 The todo list presented by `--preserve-merges --interactive` does not
 represent the topology of the revision graph.  Editing commits and
 rewording their commit messages should work fine, but attempts to
-reorder commits tend to produce counterintuitive results.
+reorder commits tend to produce counterintuitive results. Use
+--recreate-merges for a more faithful representation.
 
 For example, an attempt to rearrange
 ------------
diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash
index 88813e91244..38bba3835c6 100644
--- a/contrib/completion/git-completion.bash
+++ b/contrib/completion/git-completion.bash
@@ -2008,7 +2008,7 @@ _git_rebase ()
 	--*)
 		__gitcomp "
 			--onto --merge --strategy --interactive
-			--preserve-merges --stat --no-stat
+			--recreate-merges --preserve-merges --stat --no-stat
 			--committer-date-is-author-date --ignore-date
 			--ignore-whitespace --whitespace=
 			--autosquash --no-autosquash
diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh
index 2d8bbe20b74..f5c8db2fdf8 100644
--- a/git-rebase--interactive.sh
+++ b/git-rebase--interactive.sh
@@ -906,6 +906,7 @@ fi
 if test t != "$preserve_merges"
 then
 	git rebase--helper --make-script ${keep_empty:+--keep-empty} \
+		${recreate_merges:+--recreate-merges} \
 		$revisions ${restrict_revision+^$restrict_revision} >"$todo" ||
 	die "$(gettext "Could not generate todo list")"
 else
diff --git a/git-rebase.sh b/git-rebase.sh
index b353c33d417..528fa0073ac 100755
--- a/git-rebase.sh
+++ b/git-rebase.sh
@@ -17,6 +17,7 @@ q,quiet!           be quiet. implies --no-stat
 autostash          automatically stash/stash pop before and after
 fork-point         use 'merge-base --fork-point' to refine upstream
 onto=!             rebase onto given branch instead of upstream
+recreate-merges!   try to recreate merges instead of skipping them
 p,preserve-merges! try to recreate merges instead of ignoring them
 s,strategy=!       use the given merge strategy
 no-ff!             cherry-pick all commits, even if unchanged
@@ -87,6 +88,7 @@ type=
 state_dir=
 # One of {'', continue, skip, abort}, as parsed from command line
 action=
+recreate_merges=
 preserve_merges=
 autosquash=
 keep_empty=
@@ -267,6 +269,10 @@ do
 	--allow-empty-message)
 		allow_empty_message=--allow-empty-message
 		;;
+	--recreate-merges)
+		recreate_merges=t
+		test -z "$interactive_rebase" && interactive_rebase=implied
+		;;
 	--preserve-merges)
 		preserve_merges=t
 		test -z "$interactive_rebase" && interactive_rebase=implied
diff --git a/t/t3430-rebase-recreate-merges.sh b/t/t3430-rebase-recreate-merges.sh
new file mode 100755
index 00000000000..0073601a206
--- /dev/null
+++ b/t/t3430-rebase-recreate-merges.sh
@@ -0,0 +1,146 @@
+#!/bin/sh
+#
+# Copyright (c) 2017 Johannes E. Schindelin
+#
+
+test_description='git rebase -i --recreate-merges
+
+This test runs git rebase "interactively", retaining the branch structure by
+recreating merge commits.
+
+Initial setup:
+
+    -- B --                   (first)
+   /       \
+ A - C - D - E - H            (master)
+       \       /
+         F - G                (second)
+'
+. ./test-lib.sh
+. "$TEST_DIRECTORY"/lib-rebase.sh
+
+test_expect_success 'setup' '
+	write_script replace-editor.sh <<-\EOF &&
+	mv "$1" "$(git rev-parse --git-path ORIGINAL-TODO)"
+	cp script-from-scratch "$1"
+	EOF
+
+	test_commit A &&
+	git checkout -b first &&
+	test_commit B &&
+	git checkout master &&
+	test_commit C &&
+	test_commit D &&
+	git merge --no-commit B &&
+	test_tick &&
+	git commit -m E &&
+	git tag -m E E &&
+	git checkout -b second C &&
+	test_commit F &&
+	test_commit G &&
+	git checkout master &&
+	git merge --no-commit G &&
+	test_tick &&
+	git commit -m H &&
+	git tag -m H H
+'
+
+cat >script-from-scratch <<\EOF
+label onto
+
+# onebranch
+pick G
+pick D
+label onebranch
+
+# second
+reset onto
+pick B
+label second
+
+reset onto
+merge -C H second
+merge onebranch # Merge the topic branch 'onebranch'
+EOF
+
+test_cmp_graph () {
+	cat >expect &&
+	git log --graph --boundary --format=%s "$@" >output &&
+	sed "s/ *$//" <output >output.trimmed &&
+	test_cmp expect output.trimmed
+}
+
+test_expect_success 'create completely different structure' '
+	test_config sequence.editor \""$PWD"/replace-editor.sh\" &&
+	test_tick &&
+	git rebase -i --recreate-merges A &&
+	test_cmp_graph <<-\EOF
+	*   Merge the topic branch '\''onebranch'\''
+	|\
+	| * D
+	| * G
+	* |   H
+	|\ \
+	| |/
+	|/|
+	| * B
+	|/
+	* A
+	EOF
+'
+
+test_expect_success 'generate correct todo list' '
+	cat >expect <<-\EOF &&
+	label onto
+
+	reset onto
+	pick d9df450 B
+	label E
+
+	reset onto
+	pick 5dee784 C
+	label branch-point
+	pick ca2c861 F
+	pick 088b00a G
+	label H
+
+	reset branch-point # C
+	pick 12bd07b D
+	merge -C 2051b56 E # E
+	merge -C 233d48a H # H
+
+	EOF
+
+	grep -v "^#" <.git/ORIGINAL-TODO >output &&
+	test_cmp expect output
+'
+
+test_expect_success 'with a branch tip that was cherry-picked already' '
+	git checkout -b already-upstream master &&
+	base="$(git rev-parse --verify HEAD)" &&
+
+	test_commit A1 &&
+	test_commit A2 &&
+	git reset --hard $base &&
+	test_commit B1 &&
+	test_tick &&
+	git merge -m "Merge branch A" A2 &&
+
+	git checkout -b upstream-with-a2 $base &&
+	test_tick &&
+	git cherry-pick A2 &&
+
+	git checkout already-upstream &&
+	test_tick &&
+	git rebase -i --recreate-merges upstream-with-a2 &&
+	test_cmp_graph upstream-with-a2.. <<-\EOF
+	*   Merge branch A
+	|\
+	| * A1
+	* | B1
+	|/
+	o A2
+	EOF
+'
+
+test_done
-- 
2.16.1.windows.4



^ permalink raw reply	[flat|nested] 276+ messages in thread

* [PATCH v4 09/12] sequencer: make refs generated by the `label` command worktree-local
  2018-02-23 12:35     ` [PATCH v4 00/12] rebase -i: offer to recreate merge commits Johannes Schindelin
                         ` (7 preceding siblings ...)
  2018-02-23 12:38       ` [PATCH v4 08/12] rebase: introduce the --recreate-merges option Johannes Schindelin
@ 2018-02-23 12:38       ` Johannes Schindelin
  2018-02-23 12:39       ` [PATCH v4 10/12] sequencer: handle post-rewrite for merge commands Johannes Schindelin
                         ` (3 subsequent siblings)
  12 siblings, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-02-23 12:38 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Jacob Keller, Stefan Beller, Philip Oakley, Eric Sunshine, Phillip Wood

This allows for rebases to be run in parallel in separate worktrees
(think: interrupted in the middle of one rebase, being asked to perform
a different rebase, adding a separate worktree just for that job).

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 refs.c                            |  3 ++-
 t/t3430-rebase-recreate-merges.sh | 14 ++++++++++++++
 2 files changed, 16 insertions(+), 1 deletion(-)

diff --git a/refs.c b/refs.c
index 20ba82b4343..e8b84c189ff 100644
--- a/refs.c
+++ b/refs.c
@@ -600,7 +600,8 @@ int dwim_log(const char *str, int len, struct object_id *oid, char **log)
 static int is_per_worktree_ref(const char *refname)
 {
 	return !strcmp(refname, "HEAD") ||
-		starts_with(refname, "refs/bisect/");
+		starts_with(refname, "refs/bisect/") ||
+		starts_with(refname, "refs/rewritten/");
 }
 
 static int is_pseudoref_syntax(const char *refname)
diff --git a/t/t3430-rebase-recreate-merges.sh b/t/t3430-rebase-recreate-merges.sh
index 0073601a206..1a3e43d66ff 100755
--- a/t/t3430-rebase-recreate-merges.sh
+++ b/t/t3430-rebase-recreate-merges.sh
@@ -143,4 +143,18 @@ test_expect_success 'with a branch tip that was cherry-picked already' '
 	EOF
 '
 
+test_expect_success 'refs/rewritten/* is worktree-local' '
+	git worktree add wt &&
+	cat >wt/script-from-scratch <<-\EOF &&
+	label xyz
+	exec GIT_DIR=../.git git rev-parse --verify refs/rewritten/xyz >a || :
+	exec git rev-parse --verify refs/rewritten/xyz >b
+	EOF
+
+	test_config -C wt sequence.editor \""$PWD"/replace-editor.sh\" &&
+	git -C wt rebase -i HEAD &&
+	test_must_be_empty wt/a &&
+	test_cmp_rev HEAD "$(cat wt/b)"
+'
+
 test_done
-- 
2.16.1.windows.4



^ permalink raw reply	[flat|nested] 276+ messages in thread

* [PATCH v4 10/12] sequencer: handle post-rewrite for merge commands
  2018-02-23 12:35     ` [PATCH v4 00/12] rebase -i: offer to recreate merge commits Johannes Schindelin
                         ` (8 preceding siblings ...)
  2018-02-23 12:38       ` [PATCH v4 09/12] sequencer: make refs generated by the `label` command worktree-local Johannes Schindelin
@ 2018-02-23 12:39       ` Johannes Schindelin
  2018-02-23 12:39       ` [PATCH v4 11/12] pull: accept --rebase=recreate to recreate the branch topology Johannes Schindelin
                         ` (2 subsequent siblings)
  12 siblings, 0 replies; 276+ messages in thread
From: Johannes Schindelin @ 2018-02-23 12:39 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Jacob Keller, Stefan Beller, Philip Oakley, Eric Sunshine, Phillip Wood

In the previous patches, we implemented the basic functionality of the
`git rebase -i --recreate-merges` command, in particular the `merge`
command to create merge commits in the sequencer.

The interactive rebase is a lot more these days, though, than a simple
cherry-pick in a loop. For example, it calls the post-rewrite hook (if
any) after rebasing with a mapping of the old->new commits.

This patch implements the post-rewrite handling for the `merge` command
we just introduced. The other commands that were added recently (`label`
and `reset`) do not create new commits, therefore post-rewrite do not
need to handle them.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 sequencer.c                       |  7 +++++--
 t/t3430-rebase-recreate-merges.sh | 25 +++++++++++++++++++++++++
 2 files changed, 30 insertions(+), 2 deletions(-)

diff --git a/sequencer.c b/sequencer.c
index 01bafe2fe47..85ce37cb99f 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -2980,11 +2980,14 @@ static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts)
 		else if (item->command == TODO_RESET)
 			res = do_reset(item->arg, item->arg_len, opts);
 		else if (item->command == TODO_MERGE ||
-			 item->command == TODO_MERGE_AND_EDIT)
+			 item->command == TODO_MERGE_AND_EDIT) {
 			res = do_merge(item->commit, item->arg, item->arg_len,
 				       item->command == TODO_MERGE_AND_EDIT ?
 				       EDIT_MSG | VERIFY_MSG : 0, opts);
-		else if (!is_noop(item->command))
+			if (item->commit)
+				record_in_rewritten(&item->commit->object.oid,
+						    peek_command(todo_list, 1));
+		} else if (!is_noop(item->command))
 			return error(_("unknown command %d"), item->command);
 
 		todo_list->current++;
diff --git a/t/t3430-rebase-recreate-merges.sh b/t/t3430-rebase-recreate-merges.sh
index 1a3e43d66ff..35a61ce90bb 100755
--- a/t/t3430-rebase-recreate-merges.sh
+++ b/t/t3430-rebase-recreate-merges.sh
@@ -157,4 +157,29 @@ test_expect_success 'refs/rewritten/* is worktree-local' '
 	test_cmp_rev HEAD "$(cat wt/b)"
 '
 
+test_expect_success 'post-rewrite hook and fixups work for merges' '
+	git checkout -b post-rewrite &&
+	test_commit same1 &&
+	git reset --hard HEAD^ &&
+	test_commit same2 &&
+	git merge -m "to fix up" same1 &&
+	echo same old same old >same2.t &&
+	test_tick &&
+	git commit --fixup HEAD same2.t &&
+	fixup="$(git rev-parse HEAD)" &&
+
+	mkdir -p .git/hooks &&
+	test_when_finished "rm .git/hooks/post-rewrite" &&
+	echo "cat >actual" | write_script .git/hooks/post-rewrite &&
+
+	test_tick &&
+	git rebase -i --autosquash --recreate-merges HEAD^^^ &&
+	printf "%s %s\n%s %s\n%s %s\n%s %s\n" >expect $(git rev-parse \
+		$fixup^^2 HEAD^2 \
+		$fixup^^ HEAD^ \
+		$fixup^ HEAD \
+		$fixup HEAD) &&
+	test_cmp expect actual
+'
+
 test_done
-- 
2.16.1.windows.4



^ permalink raw reply	[flat|nested] 276+ messages in thread

* [PATCH v4 11/12] pull: accept --rebase=recreate to recreate the branch topology
  2018-02-23 12:35     ` [PATCH v4 00/12] rebase -i: offer to recreate merge commits Johannes Schindelin
                         `