Commit Diff


commit - f6794adc6b56432ea2c960ccef0b199d2441d395
commit + 0ebf8283cd162a594e725ca2a1fd9f16e6ece6e4
blob - 99596d6ea0e30d3b6d1eeaa28f571e10d4768f9a
blob + 59507d1863a1ad983bf5f3e0811d54e3c511f8f5
--- got/got.1
+++ got/got.1
@@ -679,7 +679,101 @@ If this option is used, no further command-line argume
 .It Cm rb
 Short alias for
 .Cm rebase .
+.It Cm histedit [ Fl a ] [ Fl c] [ Fl F Ar histedit-script ]
+Edit commit history between the work tree's current base commit and
+the tip commit of the work tree's current branch.
+.Pp
+Editing of commit history is controlled via a
+.Ar histedit script
+which can be edited interactively or passed on the command line.
+The format of the histedit script is line-based.
+Each line in the script begins with a command name, followed by
+whitespace and an argument.
+For most commands, the expected argument is a commit ID SHA1 hash.
+Any remaining text on the line is ignored.
+Lines which begin with the
+.Sq #
+character are ignored entirely.
+.Pp
+The available commands are as follows:
+.Bl -column YXZ pick-commit
+.It pick Ar commit Ta Use the specified commit as it is.
+.It edit Ar commit Ta Use the specfified commit but once changes have been
+merged into the work tree interrupt the histedit operation for amending.
+.It fold Ar commit Ta Combine the specified commit with the next commit
+listed further below that will be used.
+.It drop Ar commit Ta Remove this commit from the edited history.
+.It mesg Ar log-message Ta Use the specified single-line log message for
+the commit on the previous line.
+If the log message argument is left empty, open an editor where a new
+log message can be written.
+.El
+.Pp
+Every commit in the history being edited must be mentioned in histedit script.
+Lines may be re-ordered to change the order of commits in the edited history.
+.Pp
+Edited commits are accumulated on a temporary branch.
+Once history editing has completed successfully, the temporary branch becomes
+the new version of the work tree's base branch and the work tree is
+automatically switched to it.
+.Pp
+While merging commits, show the status of each affected file,
+using the following status codes:
+.Bl -column YXZ description
+.It G Ta file was merged
+.It C Ta file was merged and conflicts occurred during merge
+.It ! Ta changes destined for a missing file were not merged
+.It D Ta file was deleted
+.It d Ta file's deletion was obstructed by local modifications
+.It A Ta new file was added
+.It ~ Ta changes destined for a non-regular file were not merged
+.El
+.Pp
+If merge conflicts occur the histedit operation is interrupted and may
+be continued once conflicts have been resolved.
+Alternatively, the histedit operation may be aborted which will leave
+the work tree switched back to its original branch.
+.Pp
+If a merge conflict is resolved in a way which renders the merged
+change into a no-op change, the corresponding commit will be elided
+when the histedit operation continues.
+.Pp
+.Cm got histedit
+will refuse to run if certain preconditions are not met.
+If the work tree contains multiple base commits it must first be updated
+to a single base commit with
+.Cm got update .
+If the work tree contains local changes, these changes must first be
+committed with
+.Cm got commit
+or reverted with
+.Cm got revert .
+If the edited history contains changes to files outside of the work tree's
+path prefix, the work tree cannot be used to edit the history of this branch.
+.Pp
+The
+.Cm got update
+and
+.Cm got commit
+commands will refuse to run while a histedit operation is in progress.
+Other commands which manipulate the work tree may be used for
+conflict resolution purposes.
+.Pp
+The options for
+.Cm got histedit
+are as follows:
+.Bl -tag -width Ds
+.It Fl a
+Abort an interrupted histedit operation.
+If this option is used, no further command-line arguments are allowed.
+.It Fl c
+Continue an interrupted histedit operation.
+If this option is used, no further command-line arguments are allowed.
 .El
+.It Cm he
+Short alias for
+.Cm histedit .
+.El
 .Sh ENVIRONMENT
 .Bl -tag -width GOT_AUTHOR
 .It Ev GOT_AUTHOR
@@ -797,6 +891,14 @@ The patch can be mailed out for review and applied to 
 .Pp
 .Dl $ got diff master unified-buffer-cache > /tmp/ubc.diff
 .Pp
+Edit the entire commit history of the
+.Dq unified-buffer-cache
+branch:
+.Pp
+.Dl $ got update -b unified-buffer-cache
+.Dl $ got update -c master
+.Dl $ got histedit
+.Pp
 .Sh SEE ALSO
 .Xr tog 1 ,
 .Xr git-repository 5 ,
blob - 7d190467b8b7bb62264d404054183db585d304c5
blob + 3dd118c77e4b297b156ed437c7928b5f83cc302b
--- got/got.c
+++ got/got.c
@@ -93,6 +93,7 @@ __dead static void	usage_commit(void);
 __dead static void	usage_cherrypick(void);
 __dead static void	usage_backout(void);
 __dead static void	usage_rebase(void);
+__dead static void	usage_histedit(void);
 
 static const struct got_error*		cmd_init(int, char *[]);
 static const struct got_error*		cmd_import(int, char *[]);
@@ -112,6 +113,7 @@ static const struct got_error*		cmd_commit(int, char *
 static const struct got_error*		cmd_cherrypick(int, char *[]);
 static const struct got_error*		cmd_backout(int, char *[]);
 static const struct got_error*		cmd_rebase(int, char *[]);
+static const struct got_error*		cmd_histedit(int, char *[]);
 
 static struct got_cmd got_commands[] = {
 	{ "init",	cmd_init,	usage_init,	"" },
@@ -132,6 +134,7 @@ static struct got_cmd got_commands[] = {
 	{ "cherrypick",	cmd_cherrypick,	usage_cherrypick, "cy" },
 	{ "backout",	cmd_backout,	usage_backout,	"bo" },
 	{ "rebase",	cmd_rebase,	usage_rebase,	"rb" },
+	{ "histedit",	cmd_histedit,	usage_histedit,	"he" },
 };
 
 static void
@@ -997,6 +1000,27 @@ switch_head_ref(struct got_reference *head_ref,
 	    got_worktree_get_head_ref_name(worktree));
 	printf("Switching work tree from %s to %s\n", base_id_str,
 	    got_worktree_get_head_ref_name(worktree));
+	return NULL;
+}
+
+static const struct got_error *
+check_rebase_or_histedit_in_progress(struct got_worktree *worktree)
+{
+	const struct got_error *err;
+	int in_progress;
+
+	err = got_worktree_rebase_in_progress(&in_progress, worktree);
+	if (err)
+		return err;
+	if (in_progress)
+		return got_error(GOT_ERR_REBASING);
+
+	err = got_worktree_histedit_in_progress(&in_progress, worktree);
+	if (err)
+		return err;
+	if (in_progress)
+		return got_error(GOT_ERR_HISTEDIT_BUSY);
+
 	return NULL;
 }
 
@@ -1011,7 +1035,7 @@ cmd_update(int argc, char *argv[])
 	char *commit_id_str = NULL;
 	const char *branch_name = NULL;
 	struct got_reference *head_ref = NULL;
-	int ch, did_something = 0, rebase_in_progress;
+	int ch, did_something = 0;
 
 	while ((ch = getopt(argc, argv, "b:c:")) != -1) {
 		switch (ch) {
@@ -1046,13 +1070,9 @@ cmd_update(int argc, char *argv[])
 	if (error)
 		goto done;
 
-	error = got_worktree_rebase_in_progress(&rebase_in_progress, worktree);
+	error = check_rebase_or_histedit_in_progress(worktree);
 	if (error)
-		goto done;
-	if (rebase_in_progress) {
-		error = got_error(GOT_ERR_REBASING);
 		goto done;
-	}
 
 	if (argc == 0) {
 		path = strdup("");
@@ -3139,7 +3159,7 @@ cmd_commit(int argc, char *argv[])
 	const char *got_author = getenv("GOT_AUTHOR");
 	struct collect_commit_logmsg_arg cl_arg;
 	char *editor = NULL;
-	int ch, rebase_in_progress;
+	int ch;
 
 	while ((ch = getopt(argc, argv, "m:")) != -1) {
 		switch (ch) {
@@ -3185,13 +3205,9 @@ cmd_commit(int argc, char *argv[])
 	if (error)
 		goto done;
 
-	error = got_worktree_rebase_in_progress(&rebase_in_progress, worktree);
+	error = check_rebase_or_histedit_in_progress(worktree);
 	if (error)
-		goto done;
-	if (rebase_in_progress) {
-		error = got_error(GOT_ERR_REBASING);
 		goto done;
-	}
 
 	error = got_repo_open(&repo, got_worktree_get_repo_path(worktree));
 	if (error != NULL)
@@ -3481,6 +3497,39 @@ usage_rebase(void)
 	fprintf(stderr, "usage: %s rebase [-a] | [-c] | branch\n",
 	    getprogname());
 	exit(1);
+}
+
+void
+trim_logmsg(char *logmsg, int limit)
+{
+	char *nl;
+	size_t len;
+
+	len = strlen(logmsg);
+	if (len > limit)
+		len = limit;
+	logmsg[len] = '\0';
+	nl = strchr(logmsg, '\n');
+	if (nl)
+		*nl = '\0';
+}
+
+static const struct got_error *
+get_short_logmsg(char **logmsg, int limit, struct got_commit_object *commit)
+{
+	const char *logmsg0 = NULL;
+
+	logmsg0 = got_object_commit_get_logmsg(commit);
+
+	while (isspace((unsigned char)logmsg0[0]))
+		logmsg0++;
+
+	*logmsg = strdup(logmsg0);
+	if (*logmsg == NULL)
+		return got_error_from_errno("strdup");
+
+	trim_logmsg(*logmsg, limit);
+	return NULL;
 }
 
 static const struct got_error *
@@ -3488,9 +3537,7 @@ show_rebase_progress(struct got_commit_object *commit,
     struct got_object_id *old_id, struct got_object_id *new_id)
 {
 	const struct got_error *err;
-	char *old_id_str = NULL, *new_id_str = NULL;
-	char *logmsg0 = NULL, *logmsg, *nl;
-	size_t len;
+	char *old_id_str = NULL, *new_id_str = NULL, *logmsg = NULL;
 
 	err = got_object_id_str(&old_id_str, old_id);
 	if (err)
@@ -3502,32 +3549,19 @@ show_rebase_progress(struct got_commit_object *commit,
 			goto done;
 	}
 
-	logmsg0 = strdup(got_object_commit_get_logmsg(commit));
-	if (logmsg0 == NULL) {
-		err = got_error_from_errno("strdup");
-		goto done;
-	}
-	logmsg = logmsg0;
-
-	while (isspace((unsigned char)logmsg[0]))
-		logmsg++;
-
 	old_id_str[12] = '\0';
 	if (new_id_str)
 		new_id_str[12] = '\0';
-	len = strlen(logmsg);
-	if (len > 42)
-		len = 42;
-	logmsg[len] = '\0';
-	nl = strchr(logmsg, '\n');
-	if (nl)
-		*nl = '\0';
+
+	err = get_short_logmsg(&logmsg, 42, commit);
+	if (err)
+		goto done;
+
 	printf("%s -> %s: %s\n", old_id_str,
 	    new_id_str ? new_id_str : "no-op change", logmsg);
 done:
 	free(old_id_str);
 	free(new_id_str);
-	free(logmsg0);
 	return err;
 }
 
@@ -3560,7 +3594,7 @@ rebase_complete(struct got_worktree *worktree, struct 
 static const struct got_error *
 rebase_commit(struct got_pathlist_head *merged_paths,
     struct got_worktree *worktree, struct got_reference *tmp_branch,
-   struct got_object_id *commit_id, struct got_repository *repo)
+    struct got_object_id *commit_id, struct got_repository *repo)
 {
 	const struct got_error *error;
 	struct got_commit_object *commit;
@@ -3934,4 +3968,991 @@ done:
 	if (repo)
 		got_repo_close(repo);
 	return error;
+}
+
+__dead static void
+usage_histedit(void)
+{
+	fprintf(stderr, "usage: %s histedit [-a] [-c] [-F path]\n",
+	    getprogname());
+	exit(1);
+}
+
+#define GOT_HISTEDIT_PICK 'p'
+#define GOT_HISTEDIT_EDIT 'e'
+#define GOT_HISTEDIT_FOLD 'f'
+#define GOT_HISTEDIT_DROP 'd'
+#define GOT_HISTEDIT_MESG 'm'
+
+static struct got_histedit_cmd {
+	unsigned char code;
+	const char *name;
+	const char *desc;
+} got_histedit_cmds[] = {
+	{ GOT_HISTEDIT_PICK, "pick", "use commit" },
+	{ GOT_HISTEDIT_EDIT, "edit", "use commit but stop for amending" },
+	{ GOT_HISTEDIT_FOLD, "fold", "combine with commit below" },
+	{ GOT_HISTEDIT_DROP, "drop", "remove commit from history" },
+	{ GOT_HISTEDIT_MESG, "mesg",
+	    "single-line log message for commit above (open editor if empty)" },
+};
+
+struct got_histedit_list_entry {
+	TAILQ_ENTRY(got_histedit_list_entry) entry;
+	struct got_object_id *commit_id;
+	const struct got_histedit_cmd *cmd;
+	char *logmsg;
+};
+TAILQ_HEAD(got_histedit_list, got_histedit_list_entry);
+
+static const struct got_error *
+histedit_write_commit(struct got_object_id *commit_id, const char *cmdname,
+    FILE *f, struct got_repository *repo)
+{
+	const struct got_error *err = NULL;
+	char *logmsg = NULL, *id_str = NULL;
+	struct got_commit_object *commit = NULL;
+	size_t n;
+
+	err = got_object_open_as_commit(&commit, repo, commit_id);
+	if (err)
+		goto done;
+
+	err = get_short_logmsg(&logmsg, 34, commit);
+	if (err)
+		goto done;
+
+	err = got_object_id_str(&id_str, commit_id);
+	if (err)
+		goto done;
+
+	n = fprintf(f, "%s %s %s\n", cmdname, id_str, logmsg);
+	if (n < 0)
+		err = got_ferror(f, GOT_ERR_IO);
+done:
+	if (commit)
+		got_object_commit_close(commit);
+	free(id_str);
+	free(logmsg);
+	return err;
+}
+
+static const struct got_error *
+histedit_write_commit_list(struct got_object_id_queue *commits, FILE *f,
+    struct got_repository *repo)
+{
+	const struct got_error *err = NULL;
+	struct got_object_qid *qid;
+
+	if (SIMPLEQ_EMPTY(commits))
+		return got_error(GOT_ERR_EMPTY_HISTEDIT);
+
+	SIMPLEQ_FOREACH(qid, commits, entry) {
+		err = histedit_write_commit(qid->id, got_histedit_cmds[0].name,
+		    f, repo);
+		if (err)
+			break;
+	}
+
+	return err;
+}
+
+static const struct got_error *
+write_cmd_list(FILE *f)
+{
+	const struct got_error *err = NULL;
+	int n, i;
+
+	n = fprintf(f, "# Available histedit commands:\n");
+	if (n < 0)
+		return got_ferror(f, GOT_ERR_IO);
+
+	for (i = 0; i < nitems(got_histedit_cmds); i++) {
+		struct got_histedit_cmd *cmd = &got_histedit_cmds[i];
+		n = fprintf(f, "#   %s (%c): %s\n", cmd->name, cmd->code,
+		    cmd->desc);
+		if (n < 0) {
+			err = got_ferror(f, GOT_ERR_IO);
+			break;
+		}
+	}
+	n = fprintf(f, "# Commits will be processed in order from top to "
+	    "bottom of this file.\n");
+	if (n < 0)
+		return got_ferror(f, GOT_ERR_IO);
+	return err;
+}
+
+static const struct got_error *
+histedit_syntax_error(int lineno)
+{
+	static char msg[42];
+	int ret;
+
+	ret = snprintf(msg, sizeof(msg), "histedit syntax error on line %d",
+	    lineno);
+	if (ret == -1 || ret >= sizeof(msg))
+		return got_error(GOT_ERR_HISTEDIT_SYNTAX);
+
+	return got_error_msg(GOT_ERR_HISTEDIT_SYNTAX, msg);
+}
+
+static const struct got_error *
+append_folded_commit_msg(char **new_msg, struct got_histedit_list_entry *hle,
+    char *logmsg, struct got_repository *repo)
+{
+	const struct got_error *err;
+	struct got_commit_object *folded_commit = NULL;
+	char *id_str;
+
+	err = got_object_id_str(&id_str, hle->commit_id);
+	if (err)
+		return err;
+
+	err = got_object_open_as_commit(&folded_commit, repo, hle->commit_id);
+	if (err)
+		goto done;
+
+	if (asprintf(new_msg, "%s%s# log message of folded commit %s: %s",
+	    logmsg ? logmsg : "", logmsg ? "\n" : "", id_str,
+	    got_object_commit_get_logmsg(folded_commit)) == -1) {
+		err = got_error_from_errno("asprintf");
+		goto done;
+	}
+done:
+	if (folded_commit)
+		got_object_commit_close(folded_commit);
+	free(id_str);
+	return err;
+}
+
+static struct got_histedit_list_entry *
+get_folded_commits(struct got_histedit_list_entry *hle)
+{
+	struct got_histedit_list_entry *prev, *folded = NULL;
+
+	prev = TAILQ_PREV(hle, got_histedit_list, entry);
+	while (prev && prev->cmd->code == GOT_HISTEDIT_FOLD) {
+		folded = prev;
+		prev = TAILQ_PREV(prev, got_histedit_list, entry);
+	}
+
+	return folded;
+}
+
+static const struct got_error *
+histedit_edit_logmsg(struct got_histedit_list_entry *hle,
+    struct got_repository *repo)
+{
+	char *logmsg_path = NULL, *id_str = NULL;
+	char *logmsg = NULL, *new_msg = NULL, *editor = NULL;
+	const struct got_error *err = NULL;
+	struct got_commit_object *commit = NULL;
+	int fd;
+	struct got_histedit_list_entry *folded = NULL;
+
+	err = got_object_open_as_commit(&commit, repo, hle->commit_id);
+	if (err)
+		return err;
+
+	folded = get_folded_commits(hle);
+	if (folded) {
+		while (folded != hle) {
+			err = append_folded_commit_msg(&new_msg, folded,
+			    logmsg, repo);
+			if (err)
+				goto done;
+			free(logmsg);
+			logmsg = new_msg;
+			folded = TAILQ_NEXT(folded, entry);
+		}
+	}
+
+	err = got_object_id_str(&id_str, hle->commit_id);
+	if (err)
+		goto done;
+	if (asprintf(&new_msg,
+	    "%s\n# original log message of commit %s: %s",
+	    logmsg ? logmsg : "", id_str,
+	    got_object_commit_get_logmsg(commit)) == -1) {
+		err = got_error_from_errno("asprintf");
+		goto done;
+	}
+	free(logmsg);
+	logmsg = new_msg;
+
+	err = got_object_id_str(&id_str, hle->commit_id);
+	if (err)
+		goto done;
+
+	err = got_opentemp_named_fd(&logmsg_path, &fd, "/tmp/got-logmsg");
+	if (err)
+		goto done;
+
+	dprintf(fd, logmsg);
+	close(fd);
+
+	err = get_editor(&editor);
+	if (err)
+		goto done;
+
+	err = edit_logmsg(&hle->logmsg, editor, logmsg_path, logmsg);
+	if (err) {
+		if (err->code != GOT_ERR_COMMIT_MSG_EMPTY)
+			goto done;
+		err = NULL;
+		hle->logmsg = strdup(got_object_commit_get_logmsg(commit));
+		if (hle->logmsg == NULL)
+			err = got_error_from_errno("strdup");
+	}
+done:
+	if (logmsg_path && unlink(logmsg_path) != 0 && err == NULL)
+		err = got_error_from_errno2("unlink", logmsg_path);
+	free(logmsg_path);
+	free(logmsg);
+	free(editor);
+	if (commit)
+		got_object_commit_close(commit);
+	return err;
 }
+
+static const struct got_error *
+histedit_parse_list(struct got_histedit_list *histedit_cmds,
+    FILE *f, struct got_repository *repo)
+{
+	const struct got_error *err = NULL;
+	char *line = NULL, *p, *end;
+	size_t size;
+	ssize_t len;
+	int lineno = 0, i;
+	const struct got_histedit_cmd *cmd;
+	struct got_object_id *commit_id = NULL;
+	struct got_histedit_list_entry *hle = NULL;
+
+	for (;;) {
+		len = getline(&line, &size, f);
+		if (len == -1) {
+			const struct got_error *getline_err;
+			if (feof(f))
+				break;
+			getline_err = got_error_from_errno("getline");
+			err = got_ferror(f, getline_err->code);
+			break;
+		}
+		lineno++;
+		p = line;
+		while (isspace((unsigned char)p[0]))
+			p++;
+		if (p[0] == '#' || p[0] == '\0') {
+			free(line);
+			line = NULL;
+			continue;
+		}
+		cmd = NULL;
+		for (i = 0; i < nitems(got_histedit_cmds); i++) {
+			cmd = &got_histedit_cmds[i];
+			if (strncmp(cmd->name, p, strlen(cmd->name)) == 0 &&
+			    isspace((unsigned char)p[strlen(cmd->name)])) {
+				p += strlen(cmd->name);
+				break;
+			}
+			if (p[0] == cmd->code && isspace((unsigned char)p[1])) {
+				p++;
+				break;
+			}
+		}
+		if (cmd == NULL) {
+			err = histedit_syntax_error(lineno);
+			break;
+		}
+		while (isspace((unsigned char)p[0]))
+			p++;
+		if (cmd->code == GOT_HISTEDIT_MESG) {
+			if (hle == NULL || hle->logmsg != NULL) {
+				err = got_error(GOT_ERR_HISTEDIT_CMD);
+				break;
+			}
+			if (p[0] == '\0') {
+				err = histedit_edit_logmsg(hle, repo);
+				if (err)
+					break;
+			} else {
+				hle->logmsg = strdup(p);
+				if (hle->logmsg == NULL) {
+					err = got_error_from_errno("strdup");
+					break;
+				}
+			}
+			free(line);
+			line = NULL;
+			continue;
+		} else {
+			end = p;
+			while (end[0] && !isspace((unsigned char)end[0]))
+				end++;
+			*end = '\0';
+
+			err = got_object_resolve_id_str(&commit_id, repo, p);
+			if (err) {
+				/* override error code */
+				err = histedit_syntax_error(lineno);
+				break;
+			}
+		}
+		hle = malloc(sizeof(*hle));
+		if (hle == NULL) {
+			err = got_error_from_errno("malloc");
+			break;
+		}
+		hle->cmd = cmd;
+		hle->commit_id = commit_id;
+		hle->logmsg = NULL;
+		commit_id = NULL;
+		free(line);
+		line = NULL;
+		TAILQ_INSERT_TAIL(histedit_cmds, hle, entry);
+	}
+
+	free(line);
+	free(commit_id);
+	return err;
+}
+
+static const struct got_error *
+histedit_run_editor(struct got_histedit_list *histedit_cmds,
+    const char *path, struct got_repository *repo)
+{
+	const struct got_error *err = NULL;
+	char *editor;
+	FILE *f = NULL;
+
+	err = get_editor(&editor);
+	if (err)
+		return err;
+
+	if (spawn_editor(editor, path) == -1) {
+		err = got_error_from_errno("failed spawning editor");
+		goto done;
+	}
+
+	f = fopen(path, "r");
+	if (f == NULL) {
+		err = got_error_from_errno("fopen");
+		goto done;
+	}
+	err = histedit_parse_list(histedit_cmds, f, repo);
+done:
+	if (f && fclose(f) != 0 && err == NULL)
+		err = got_error_from_errno("fclose");
+	free(editor);
+	return err;
+}
+
+static const struct got_error *
+histedit_edit_list_retry(struct got_histedit_list *, const char *,
+    struct got_object_id_queue *, const char *, struct got_repository *);
+
+static const struct got_error *
+histedit_edit_script(struct got_histedit_list *histedit_cmds,
+    struct got_object_id_queue *commits, struct got_repository *repo)
+{
+	const struct got_error *err;
+	FILE *f = NULL;
+	char *path = NULL;
+
+	err = got_opentemp_named(&path, &f, "got-histedit");
+	if (err)
+		return err;
+
+	err = write_cmd_list(f);
+	if (err)
+		goto done;
+
+	err = histedit_write_commit_list(commits, f, repo);
+	if (err)
+		goto done;
+
+	if (fclose(f) != 0) {
+		err = got_error_from_errno("fclose");
+		goto done;
+	}
+	f = NULL;
+
+	err = histedit_run_editor(histedit_cmds, path, repo);
+	if (err) {
+		const char *errmsg = err->msg;
+		if (err->code != GOT_ERR_HISTEDIT_SYNTAX)
+			goto done;
+		err = histedit_edit_list_retry(histedit_cmds, errmsg,
+		    commits, path, repo);
+	}
+done:
+	if (f && fclose(f) != 0 && err == NULL)
+		err = got_error_from_errno("fclose");
+	if (path && unlink(path) != 0 && err == NULL)
+		err = got_error_from_errno2("unlink", path);
+	free(path);
+	return err;
+}
+
+static const struct got_error *
+histedit_save_list(struct got_histedit_list *histedit_cmds,
+    struct got_worktree *worktree, struct got_repository *repo)
+{
+	const struct got_error *err = NULL;
+	char *path = NULL;
+	FILE *f = NULL;
+	struct got_histedit_list_entry *hle;
+	struct got_commit_object *commit = NULL;
+
+	err = got_worktree_get_histedit_list_path(&path, worktree);
+	if (err)
+		return err;
+
+	f = fopen(path, "w");
+	if (f == NULL) {
+		err = got_error_from_errno2("fopen", path);
+		goto done;
+	}
+	TAILQ_FOREACH(hle, histedit_cmds, entry) {
+		err = histedit_write_commit(hle->commit_id, hle->cmd->name, f,
+		    repo);
+		if (err)
+			break;
+
+		if (hle->logmsg) {
+			int n = fprintf(f, "%c %s\n",
+			    GOT_HISTEDIT_MESG, hle->logmsg);
+			if (n < 0) {
+				err = got_ferror(f, GOT_ERR_IO);
+				break;
+			}
+		}
+	}
+done:
+	if (f && fclose(f) != 0 && err == NULL)
+		err = got_error_from_errno("fclose");
+	free(path);
+	if (commit)
+		got_object_commit_close(commit);
+	return err;
+}
+
+static const struct got_error *
+histedit_load_list(struct got_histedit_list *histedit_cmds,
+    const char *path, struct got_repository *repo)
+{
+	const struct got_error *err = NULL;
+	FILE *f = NULL;
+
+	f = fopen(path, "r");
+	if (f == NULL) {
+		err = got_error_from_errno2("fopen", path);
+		goto done;
+	}
+
+	err = histedit_parse_list(histedit_cmds, f, repo);
+done:
+	if (f && fclose(f) != 0 && err == NULL)
+		err = got_error_from_errno("fclose");
+	return err;
+}
+
+static const struct got_error *
+histedit_edit_list_retry(struct got_histedit_list *histedit_cmds,
+    const char *errmsg, struct got_object_id_queue *commits,
+    const char *path, struct got_repository *repo)
+{
+	const struct got_error *err = NULL;
+	int resp = ' ';
+
+	while (resp != 'c' && resp != 'r' && resp != 'q') {
+		printf("%s: %s\n(c)ontinue editing, (r)estart editing, "
+		    "or (a)bort: ", getprogname(), errmsg);
+		resp = getchar();
+		switch (resp) {
+		case 'c':
+			err = histedit_run_editor(histedit_cmds, path, repo);
+			break;
+		case 'r':
+			err = histedit_edit_script(histedit_cmds,
+			    commits, repo);
+			break;
+		case 'a':
+			err = got_error(GOT_ERR_HISTEDIT_CANCEL);
+			break;
+		default:
+			printf("invalid response '%c'\n", resp);
+			break;
+		}
+	}
+
+	return err;
+}
+
+static const struct got_error *
+histedit_complete(struct got_worktree *worktree,
+    struct got_reference *tmp_branch, struct got_reference *branch,
+    struct got_repository *repo)
+{
+	printf("Switching work tree to %s\n",
+	    got_ref_get_symref_target(branch));
+	return got_worktree_histedit_complete(worktree, tmp_branch, branch,
+	    repo);
+}
+
+static const struct got_error *
+show_histedit_progress(struct got_commit_object *commit,
+    struct got_histedit_list_entry *hle, struct got_object_id *new_id)
+{
+	const struct got_error *err;
+	char *old_id_str = NULL, *new_id_str = NULL, *logmsg = NULL;
+
+	err = got_object_id_str(&old_id_str, hle->commit_id);
+	if (err)
+		goto done;
+
+	if (new_id) {
+		err = got_object_id_str(&new_id_str, new_id);
+		if (err)
+			goto done;
+	}
+
+	old_id_str[12] = '\0';
+	if (new_id_str)
+		new_id_str[12] = '\0';
+
+	if (hle->logmsg) {
+		logmsg = strdup(hle->logmsg);
+		if (logmsg == NULL) {
+			err = got_error_from_errno("strdup");
+			goto done;
+		}
+		trim_logmsg(logmsg, 42);
+	} else {
+		err = get_short_logmsg(&logmsg, 42, commit);
+		if (err)
+			goto done;
+	}
+
+	switch (hle->cmd->code) {
+	case GOT_HISTEDIT_PICK:
+	case GOT_HISTEDIT_EDIT:
+		printf("%s -> %s: %s\n", old_id_str,
+		    new_id_str ? new_id_str : "no-op change", logmsg);
+		break;
+	case GOT_HISTEDIT_DROP:
+	case GOT_HISTEDIT_FOLD:
+		printf("%s ->  %s commit: %s\n", old_id_str, hle->cmd->name,
+		    logmsg);
+		break;
+	default:
+		break;
+	}
+
+done:
+	free(old_id_str);
+	free(new_id_str);
+	return err;
+}
+
+static const struct got_error *
+histedit_commit(struct got_pathlist_head *merged_paths,
+    struct got_worktree *worktree, struct got_reference *tmp_branch,
+    struct got_histedit_list_entry *hle, struct got_repository *repo)
+{
+	const struct got_error *err;
+	struct got_commit_object *commit;
+	struct got_object_id *new_commit_id;
+
+	if ((hle->cmd->code == GOT_HISTEDIT_EDIT || get_folded_commits(hle))
+	    && hle->logmsg == NULL) {
+		err = histedit_edit_logmsg(hle, repo);
+		if (err)
+			return err;
+	}
+
+	err = got_object_open_as_commit(&commit, repo, hle->commit_id);
+	if (err)
+		return err;
+
+	err = got_worktree_histedit_commit(&new_commit_id, merged_paths,
+	    worktree, tmp_branch, commit, hle->commit_id, hle->logmsg, repo);
+	if (err) {
+		if (err->code != GOT_ERR_COMMIT_NO_CHANGES)
+			goto done;
+		err = show_histedit_progress(commit, hle, NULL);
+	} else {
+		err = show_histedit_progress(commit, hle, new_commit_id);
+		free(new_commit_id);
+	}
+done:
+	got_object_commit_close(commit);
+	return err;
+}
+
+static const struct got_error *
+histedit_skip_commit(struct got_histedit_list_entry *hle,
+    struct got_worktree *worktree, struct got_repository *repo)
+{
+	const struct got_error *error;
+	struct got_commit_object *commit;
+
+	error = got_worktree_histedit_skip_commit(worktree, hle->commit_id,
+	    repo);
+	if (error)
+		return error;
+
+	error = got_object_open_as_commit(&commit, repo, hle->commit_id);
+	if (error)
+		return error;
+
+	error = show_histedit_progress(commit, hle, NULL);
+	got_object_commit_close(commit);
+	return error;
+}
+
+static const struct got_error *
+histedit_check_script(struct got_histedit_list *histedit_cmds,
+    struct got_object_id_queue *commits, struct got_repository *repo)
+{
+	const struct got_error *err = NULL;
+	struct got_object_qid *qid;
+	struct got_histedit_list_entry *hle;
+	static char msg[80];
+	char *id_str;
+
+	if (TAILQ_EMPTY(histedit_cmds))
+		return got_error(GOT_ERR_EMPTY_HISTEDIT);
+
+	SIMPLEQ_FOREACH(qid, commits, entry) {
+		TAILQ_FOREACH(hle, histedit_cmds, entry) {
+			if (got_object_id_cmp(qid->id, hle->commit_id) == 0)
+				break;
+		}
+		if (hle == NULL) {
+			err = got_object_id_str(&id_str, qid->id);
+			if (err)
+				return err;
+			snprintf(msg, sizeof(msg),
+			    "commit %s missing from histedit script", id_str);
+			free(id_str);
+			return got_error_msg(GOT_ERR_HISTEDIT_CMD, msg);
+		}
+	}
+
+	if (hle->cmd->code == GOT_HISTEDIT_FOLD)
+		return got_error_msg(GOT_ERR_HISTEDIT_CMD,
+		    "last commit in histedit script cannot be folded");
+
+	return NULL;
+}
+
+static const struct got_error *
+cmd_histedit(int argc, char *argv[])
+{
+	const struct got_error *error = NULL;
+	struct got_worktree *worktree = NULL;
+	struct got_repository *repo = NULL;
+	char *cwd = NULL;
+	struct got_reference *branch = NULL;
+	struct got_reference *tmp_branch = NULL;
+	struct got_object_id *resume_commit_id = NULL;
+	struct got_object_id *base_commit_id = NULL;
+	struct got_object_id *head_commit_id = NULL;
+	struct got_commit_object *commit = NULL;
+	int ch, rebase_in_progress = 0;
+	int edit_in_progress = 0, abort_edit = 0, continue_edit = 0;
+	const char *edit_script_path = NULL;
+	unsigned char rebase_status = GOT_STATUS_NO_CHANGE;
+	struct got_object_id_queue commits;
+	struct got_pathlist_head merged_paths;
+	const struct got_object_id_queue *parent_ids;
+	struct got_object_qid *pid;
+	struct got_histedit_list histedit_cmds;
+	struct got_histedit_list_entry *hle;
+
+	SIMPLEQ_INIT(&commits);
+	TAILQ_INIT(&histedit_cmds);
+	TAILQ_INIT(&merged_paths);
+
+	while ((ch = getopt(argc, argv, "acF:")) != -1) {
+		switch (ch) {
+		case 'a':
+			abort_edit = 1;
+			break;
+		case 'c':
+			continue_edit = 1;
+			break;
+		case 'F':
+			edit_script_path = optarg;
+			break;
+		default:
+			usage_histedit();
+			/* NOTREACHED */
+		}
+	}
+
+	argc -= optind;
+	argv += optind;
+
+#ifndef PROFILE
+	if (pledge("stdio rpath wpath cpath fattr flock proc exec sendfd "
+	    "unveil", NULL) == -1)
+		err(1, "pledge");
+#endif
+	if (abort_edit && continue_edit)
+		usage_histedit();
+	if (argc != 0)
+		usage_histedit();
+
+	/*
+	 * This command cannot apply unveil(2) in all cases because the
+	 * user may choose to run an editor to edit the histedit script
+	 * and to edit individual commit log messages.
+	 * unveil(2) traverses exec(2); if an editor is used we have to
+	 * apply unveil after edit script and log messages have been written.
+	 * XXX TODO: Make use of unveil(2) where possible.
+	 */
+
+	cwd = getcwd(NULL, 0);
+	if (cwd == NULL) {
+		error = got_error_from_errno("getcwd");
+		goto done;
+	}
+	error = got_worktree_open(&worktree, cwd);
+	if (error)
+		goto done;
+
+	error = got_repo_open(&repo, got_worktree_get_repo_path(worktree));
+	if (error != NULL)
+		goto done;
+
+	error = got_worktree_rebase_in_progress(&rebase_in_progress, worktree);
+	if (error)
+		goto done;
+	if (rebase_in_progress) {
+		error = got_error(GOT_ERR_REBASING);
+		goto done;
+	}
+
+	error = got_worktree_histedit_in_progress(&edit_in_progress, worktree);
+	if (error)
+		goto done;
+
+	if (edit_in_progress && abort_edit) {
+		int did_something;
+		error = got_worktree_histedit_continue(&resume_commit_id,
+		    &tmp_branch, &branch, &base_commit_id, worktree, repo);
+		if (error)
+			goto done;
+		printf("Switching work tree to %s\n",
+		    got_ref_get_symref_target(branch));
+		error = got_worktree_histedit_abort(worktree, repo,
+		    branch, base_commit_id, update_progress, &did_something);
+		if (error)
+			goto done;
+		printf("Histedit of %s aborted\n",
+		    got_ref_get_symref_target(branch));
+		goto done; /* nothing else to do */
+	} else if (abort_edit) {
+		error = got_error(GOT_ERR_NOT_HISTEDIT);
+		goto done;
+	}
+
+	if (continue_edit) {
+		char *path;
+
+		if (!edit_in_progress) {
+			error = got_error(GOT_ERR_NOT_HISTEDIT);
+			goto done;
+		}
+
+		error = got_worktree_get_histedit_list_path(&path, worktree);
+		if (error)
+			goto done;
+
+		error = histedit_load_list(&histedit_cmds, path, repo);
+		free(path);
+		if (error)
+			goto done;
+
+		error = got_worktree_histedit_continue(&resume_commit_id,
+		    &tmp_branch, &branch, &base_commit_id, worktree, repo);
+		if (error)
+			goto done;
+
+		error = got_ref_resolve(&head_commit_id, repo, branch);
+		if (error)
+			goto done;
+
+		error = got_object_open_as_commit(&commit, repo,
+		    head_commit_id);
+		if (error)
+			goto done;
+		parent_ids = got_object_commit_get_parent_ids(commit);
+		pid = SIMPLEQ_FIRST(parent_ids);
+		error = collect_commits_to_rebase(&commits, head_commit_id,
+		    pid->id, base_commit_id,
+		    got_worktree_get_path_prefix(worktree), repo);
+		got_object_commit_close(commit);
+		commit = NULL;
+		if (error)
+			goto done;
+	} else {
+		if (edit_in_progress) {
+			error = got_error(GOT_ERR_HISTEDIT_BUSY);
+			goto done;
+		}
+
+		error = got_ref_open(&branch, repo,
+		    got_worktree_get_head_ref_name(worktree), 0);
+		if (error != NULL)
+			goto done;
+
+		error = got_ref_resolve(&head_commit_id, repo, branch);
+		if (error)
+			goto done;
+
+		error = got_object_open_as_commit(&commit, repo,
+		    head_commit_id);
+		if (error)
+			goto done;
+		parent_ids = got_object_commit_get_parent_ids(commit);
+		pid = SIMPLEQ_FIRST(parent_ids);
+		error = collect_commits_to_rebase(&commits, head_commit_id,
+		    pid->id, got_worktree_get_base_commit_id(worktree),
+		    got_worktree_get_path_prefix(worktree), repo);
+		got_object_commit_close(commit);
+		commit = NULL;
+		if (error)
+			goto done;
+
+		if (edit_script_path) {
+			error = histedit_load_list(&histedit_cmds,
+			    edit_script_path, repo);
+			if (error)
+				goto done;
+		} else {
+			error = histedit_edit_script(&histedit_cmds, &commits,
+			    repo);
+			if (error)
+				goto done;
+
+		}
+
+		error = histedit_save_list(&histedit_cmds, worktree,
+		    repo);
+		if (error)
+			goto done;
+
+		error = got_worktree_histedit_prepare(&tmp_branch, &branch,
+		    &base_commit_id, worktree, repo);
+		if (error)
+			goto done;
+
+	}
+
+	 error = histedit_check_script(&histedit_cmds, &commits, repo);
+	 if (error)
+		goto done;
+
+	TAILQ_FOREACH(hle, &histedit_cmds, entry) {
+		if (resume_commit_id) {
+			if (got_object_id_cmp(hle->commit_id,
+			    resume_commit_id) != 0)
+				continue;
+
+			resume_commit_id = NULL;
+			if (hle->cmd->code == GOT_HISTEDIT_DROP ||
+			    hle->cmd->code == GOT_HISTEDIT_FOLD) {
+				error = histedit_skip_commit(hle, worktree,
+				   repo);
+			} else {
+				error = histedit_commit(NULL, worktree,
+				    tmp_branch, hle, repo);
+			}
+			if (error)
+				goto done;
+			continue;
+		}
+
+		if (hle->cmd->code == GOT_HISTEDIT_DROP) {
+			error = histedit_skip_commit(hle, worktree, repo);
+			if (error)
+				goto done;
+			continue;
+		}
+
+		error = got_object_open_as_commit(&commit, repo,
+		    hle->commit_id);
+		if (error)
+			goto done;
+		parent_ids = got_object_commit_get_parent_ids(commit);
+		pid = SIMPLEQ_FIRST(parent_ids);
+
+		error = got_worktree_histedit_merge_files(&merged_paths,
+		    worktree, pid->id, hle->commit_id, repo, rebase_progress,
+		    &rebase_status, check_cancelled, NULL);
+		if (error)
+			goto done;
+		got_object_commit_close(commit);
+		commit = NULL;
+
+		if (rebase_status == GOT_STATUS_CONFLICT) {
+			got_worktree_rebase_pathlist_free(&merged_paths);
+			break;
+		}
+
+		if (hle->cmd->code == GOT_HISTEDIT_EDIT) {
+			char *id_str;
+			error = got_object_id_str(&id_str, hle->commit_id);
+			if (error)
+				goto done;
+			printf("Stopping histedit for amending commit %s\n",
+			    id_str);
+			free(id_str);
+			got_worktree_rebase_pathlist_free(&merged_paths);
+			error = got_worktree_histedit_postpone(worktree);
+			goto done;
+		}
+
+		if (hle->cmd->code == GOT_HISTEDIT_FOLD) {
+			error = histedit_skip_commit(hle, worktree, repo);
+			if (error)
+				goto done;
+			continue;
+		}
+
+		error = histedit_commit(&merged_paths, worktree, tmp_branch,
+		    hle, repo);
+		got_worktree_rebase_pathlist_free(&merged_paths);
+		if (error)
+			goto done;
+	}
+
+	if (rebase_status == GOT_STATUS_CONFLICT) {
+		error = got_worktree_histedit_postpone(worktree);
+		if (error)
+			goto done;
+		error = got_error_msg(GOT_ERR_CONFLICTS,
+		    "conflicts must be resolved before rebasing can continue");
+	} else
+		error = histedit_complete(worktree, tmp_branch, branch, repo);
+done:
+	got_object_id_queue_free(&commits);
+	free(head_commit_id);
+	free(base_commit_id);
+	free(resume_commit_id);
+	if (commit)
+		got_object_commit_close(commit);
+	if (branch)
+		got_ref_close(branch);
+	if (tmp_branch)
+		got_ref_close(tmp_branch);
+	if (worktree)
+		got_worktree_close(worktree);
+	if (repo)
+		got_repo_close(repo);
+	return error;
+}
blob - 48ede3e31fd5acad44260af32cff4957fb97fdad
blob + 01c340bddf058f3296064d6b7a860341ce2eeeb2
--- include/got_error.h
+++ include/got_error.h
@@ -103,6 +103,14 @@
 #define GOT_ERR_REBASE_COMMITID	87
 #define GOT_ERR_REBASING	88
 #define GOT_ERR_REBASE_PATH	89
+#define GOT_ERR_NOT_HISTEDIT	90
+#define GOT_ERR_EMPTY_HISTEDIT	91
+#define GOT_ERR_NO_HISTEDIT_CMD	92
+#define GOT_ERR_HISTEDIT_SYNTAX	93
+#define GOT_ERR_HISTEDIT_CANCEL	94
+#define GOT_ERR_HISTEDIT_COMMITID 95
+#define GOT_ERR_HISTEDIT_BUSY	96
+#define GOT_ERR_HISTEDIT_CMD	97
 
 static const struct got_error {
 	int code;
@@ -204,6 +212,15 @@ static const struct got_error {
 	    "work tree and must be continued or aborted first" },
 	{ GOT_ERR_REBASE_PATH,	"cannot rebase branch which contains "
 	    "changes outside of this work tree's path prefix" },
+	{ GOT_ERR_NOT_HISTEDIT,	"histedit operation not in progress" },
+	{ GOT_ERR_EMPTY_HISTEDIT,"no commits to edit" },
+	{ GOT_ERR_NO_HISTEDIT_CMD,"no histedit commands provided" },
+	{ GOT_ERR_HISTEDIT_SYNTAX,"syntax error in histedit command list" },
+	{ GOT_ERR_HISTEDIT_CANCEL,"histedit operation cancelled" },
+	{ GOT_ERR_HISTEDIT_COMMITID,"histedit commit ID mismatch" },
+	{ GOT_ERR_HISTEDIT_BUSY,"histedit operation is in progress in this "
+	    "work tree and must be continued or aborted first" },
+	{ GOT_ERR_HISTEDIT_CMD, "bad histedit command" },
 };
 
 /*
blob - 4581e987c69013fec31a525f414b1596bf5ce460
blob + 6e6d2f53d8d2d5dd4b9b32075806433ee2a4cb49
--- include/got_worktree.h
+++ include/got_worktree.h
@@ -281,4 +281,86 @@ const struct got_error *got_worktree_rebase_complete(s
  */
 const struct got_error *got_worktree_rebase_abort(struct got_worktree *,
     struct got_repository *, struct got_reference *,
+    got_worktree_checkout_cb, void *);
+
+/*
+ * Prepare for editing the history of the work tree's current branch.
+ * This function creates references to a temporary branch, and the
+ * work tree's current branch, under the "got/worktree/histedit/" namespace.
+ * These references are used to keep track of histedit operation state and
+ * are used as input and/or output arguments with other histedit-related
+ * functions.
+ */
+const struct got_error *got_worktree_histedit_prepare(struct got_reference **,
+    struct got_reference **, struct got_object_id **, struct got_worktree *,
+    struct got_repository *);
+
+/*
+ * Continue an interrupted histedit operation.
+ * This function returns existing references created when histedit was
+ * prepared and the ID of the commit currently being edited.
+ * It should be called before resuming or aborting a histedit operation.
+ */
+const struct got_error *got_worktree_histedit_continue(struct got_object_id **,
+    struct got_reference **, struct got_reference **, struct got_object_id **,
+    struct got_worktree *, struct got_repository *);
+
+/* Check whether a histedit operation is in progress. */
+const struct got_error *got_worktree_histedit_in_progress(int *,
+    struct got_worktree *);
+
+/*
+ * Merge changes from the commit currently being edited into the work tree.
+ * Report affected files, including merge conflicts, via the specified
+ * progress callback. Also populate a list of affected paths which should
+ * be passed to got_worktree_histedit_commit() after a conflict-free merge.
+ * This list must be initialized with TAILQ_INIT() and disposed of with
+ * got_worktree_rebase_pathlist_free().
+ */
+const struct got_error *got_worktree_histedit_merge_files(
+    struct got_pathlist_head *, struct got_worktree *,
+    struct got_object_id *, struct got_object_id *, struct got_repository *,
+    got_worktree_checkout_cb, void *, got_worktree_cancel_cb, void *);
+
+/*
+ * Commit changes merged by got_worktree_histedit_merge_files() to a temporary
+ * branch and return the ID of the newly created commit. An optional list of
+ * merged paths can be provided; otherwise this function will perform a status
+ * crawl across the entire work tree to find paths to commit.
+ * An optional log message can be provided which will be used instead of the
+ * commit's original message.
+ */
+const struct got_error *got_worktree_histedit_commit(struct got_object_id **,
+    struct got_pathlist_head *, struct got_worktree *,
+    struct got_reference *, struct got_commit_object *,
+    struct got_object_id *, const char *, struct got_repository *);
+
+/*
+ * Record the specified commit as skipped during histedit.
+ * This should be called for commits which get dropped or get folded into
+ * a subsequent commit.
+ */
+const struct got_error *got_worktree_histedit_skip_commit(struct got_worktree *,
+    struct got_object_id *, struct got_repository *);
+
+/* Postpone the histedit operation. */
+const struct got_error *got_worktree_histedit_postpone(struct got_worktree *);
+
+/*
+ * Complete the current histedit operation. This should be called once all
+ * commits have been edited successfully.
+ */
+const struct got_error *got_worktree_histedit_complete(struct got_worktree *,
+    struct got_reference *, struct got_reference *, struct got_repository *);
+
+/*
+ * Abort the current histedit operation.
+ * Report reverted files via the specified progress callback.
+ */
+const struct got_error *got_worktree_histedit_abort(struct got_worktree *,
+    struct got_repository *, struct got_reference *, struct got_object_id *,
     got_worktree_checkout_cb, void *);
+
+/* Get the path to this work tree's histedit command list file. */
+const struct got_error *got_worktree_get_histedit_list_path(char **,
+    struct got_worktree *);
blob - 317d0fdf6a3b2bf43047f3743caed1b085199df0
blob + 2a0626057ea68d7928c4de3678836630f02cca24
--- lib/got_lib_worktree.h
+++ lib/got_lib_worktree.h
@@ -57,6 +57,7 @@ struct got_commitable {
 #define GOT_WORKTREE_LOCK		"lock"
 #define GOT_WORKTREE_FORMAT		"format"
 #define GOT_WORKTREE_UUID		"uuid"
+#define GOT_WORKTREE_HISTEDIT_LIST	"histedit-list"
 
 #define GOT_WORKTREE_FORMAT_VERSION	1
 #define GOT_WORKTREE_INVALID_COMMIT_ID	GOT_SHA1_STRING_ZERO
@@ -77,3 +78,18 @@ const struct got_error *got_worktree_get_base_ref_name
 
 /* Reference pointing at the ID of the current commit being rebased. */
 #define GOT_WORKTREE_REBASE_COMMIT_REF_PREFIX "refs/got/worktree/rebase/commit"
+
+/* Temporary branch which accumulates commits during a histedit operation. */
+#define GOT_WORKTREE_HISTEDIT_TMP_REF_PREFIX "refs/got/worktree/histedit/tmp"
+
+/* Symbolic reference pointing at the name of the branch being edited. */
+#define GOT_WORKTREE_HISTEDIT_BRANCH_REF_PREFIX \
+	"refs/got/worktree/histedit/branch"
+
+/* Reference pointing at the ID of the work tree's pre-edit base commit. */
+#define GOT_WORKTREE_HISTEDIT_BASE_COMMIT_REF_PREFIX \
+	"refs/got/worktree/histedit/base-commit"
+
+/* Reference pointing at the ID of the current commit being edited. */
+#define GOT_WORKTREE_HISTEDIT_COMMIT_REF_PREFIX \
+	"refs/got/worktree/histedit/commit"
blob - 7a096570fcf9fc2d50fbd56c6a4a29ad6b170e54
blob + cdff663f385ece6d4196b6e18a30d7ddcb7ab947
--- lib/worktree.c
+++ lib/worktree.c
@@ -1438,9 +1438,47 @@ get_rebase_commit_ref_name(char **refname, struct got_
 {
 	return get_ref_name(refname, worktree,
 	    GOT_WORKTREE_REBASE_COMMIT_REF_PREFIX);
+}
+
+static const struct got_error *
+get_histedit_tmp_ref_name(char **refname, struct got_worktree *worktree)
+{
+	return get_ref_name(refname, worktree,
+	    GOT_WORKTREE_HISTEDIT_TMP_REF_PREFIX);
+}
+
+static const struct got_error *
+get_histedit_branch_symref_name(char **refname, struct got_worktree *worktree)
+{
+	return get_ref_name(refname, worktree,
+	    GOT_WORKTREE_HISTEDIT_BRANCH_REF_PREFIX);
+}
+
+static const struct got_error *
+get_histedit_base_commit_ref_name(char **refname, struct got_worktree *worktree)
+{
+	return get_ref_name(refname, worktree,
+	    GOT_WORKTREE_HISTEDIT_BASE_COMMIT_REF_PREFIX);
 }
 
+static const struct got_error *
+get_histedit_commit_ref_name(char **refname, struct got_worktree *worktree)
+{
+	return get_ref_name(refname, worktree,
+	    GOT_WORKTREE_HISTEDIT_COMMIT_REF_PREFIX);
+}
 
+const struct got_error *
+got_worktree_get_histedit_list_path(char **path, struct got_worktree *worktree)
+{
+	if (asprintf(path, "%s/%s/%s", worktree->root_path,
+	    GOT_WORKTREE_GOT_DIR, GOT_WORKTREE_HISTEDIT_LIST) == -1) {
+		*path = NULL;
+		return got_error_from_errno("asprintf");
+	}
+	return NULL;
+}
+
 /*
  * Prevent Git's garbage collector from deleting our base commit by
  * setting a reference to our base commit's ID.
@@ -3761,12 +3799,7 @@ static const struct got_error *
 collect_rebase_commit_msg(struct got_pathlist_head *commitable_paths,
     char **logmsg, void *arg)
 {
-	struct got_commit_object *commit = arg;
-
-	*logmsg = strdup(got_object_commit_get_logmsg(commit));
-	if (*logmsg == NULL)
-		return got_error_from_errno("strdup");
-
+	*logmsg = arg;
 	return NULL;
 }
 
@@ -3822,28 +3855,13 @@ got_worktree_rebase_pathlist_free(struct got_pathlist_
 	got_pathlist_free(merged_paths);
 }
 
-const struct got_error *
-got_worktree_rebase_merge_files(struct got_pathlist_head *merged_paths,
-    struct got_worktree *worktree, struct got_object_id *parent_commit_id,
-    struct got_object_id *commit_id, struct got_repository *repo,
-    got_worktree_checkout_cb progress_cb, void *progress_arg,
-    got_worktree_cancel_cb cancel_cb, void *cancel_arg)
+static const struct got_error *
+store_commit_id(const char *commit_ref_name, struct got_object_id *commit_id,
+    struct got_repository *repo)
 {
 	const struct got_error *err;
-	struct got_fileindex *fileindex;
-	char *fileindex_path, *commit_ref_name = NULL;
 	struct got_reference *commit_ref = NULL;
-	struct collect_merged_paths_arg cmp_arg;
 
-	/* Work tree is locked/unlocked during rebase preparation/teardown. */
-
-	err = open_fileindex(&fileindex, &fileindex_path, worktree);
-	if (err)
-		return err;
-
-	err = get_rebase_commit_ref_name(&commit_ref_name, worktree);
-	if (err)
-		goto done;
 	err = got_ref_open(&commit_ref, repo, commit_ref_name, 0);
 	if (err) {
 		if (err->code != GOT_ERR_NOT_REF)
@@ -3868,14 +3886,37 @@ got_worktree_rebase_merge_files(struct got_pathlist_he
 			goto done;
 		}
 	}
+done:
+	if (commit_ref)
+		got_ref_close(commit_ref);
+	return err;
+}
+
+static const struct got_error *
+rebase_merge_files(struct got_pathlist_head *merged_paths,
+    const char *commit_ref_name, struct got_worktree *worktree,
+    struct got_object_id *parent_commit_id, struct got_object_id *commit_id,
+    struct got_repository *repo, got_worktree_checkout_cb progress_cb,
+    void *progress_arg, got_worktree_cancel_cb cancel_cb, void *cancel_arg)
+{
+	const struct got_error *err;
+	struct got_fileindex *fileindex;
+	char *fileindex_path;
+	struct got_reference *commit_ref = NULL;
+	struct collect_merged_paths_arg cmp_arg;
 
+	/* Work tree is locked/unlocked during rebase preparation/teardown. */
+
+	err = open_fileindex(&fileindex, &fileindex_path, worktree);
+	if (err)
+		return err;
+
 	cmp_arg.progress_cb = progress_cb;
 	cmp_arg.progress_arg = progress_arg;
 	cmp_arg.merged_paths = merged_paths;
 	err = merge_files(worktree, fileindex, fileindex_path,
 	    parent_commit_id, commit_id, repo, collect_merged_paths,
 	    &cmp_arg, cancel_cb, cancel_arg);
-done:
 	got_fileindex_free(fileindex);
 	free(fileindex_path);
 	if (commit_ref)
@@ -3884,39 +3925,77 @@ done:
 }
 
 const struct got_error *
-got_worktree_rebase_commit(struct got_object_id **new_commit_id,
-    struct got_pathlist_head *merged_paths, struct got_worktree *worktree,
-    struct got_reference *tmp_branch, struct got_commit_object *orig_commit,
-    struct got_object_id *orig_commit_id, struct got_repository *repo)
+got_worktree_rebase_merge_files(struct got_pathlist_head *merged_paths,
+    struct got_worktree *worktree, struct got_object_id *parent_commit_id,
+    struct got_object_id *commit_id, struct got_repository *repo,
+    got_worktree_checkout_cb progress_cb, void *progress_arg,
+    got_worktree_cancel_cb cancel_cb, void *cancel_arg)
 {
+	const struct got_error *err;
+	char *commit_ref_name;
+
+	err = get_rebase_commit_ref_name(&commit_ref_name, worktree);
+	if (err)
+		return err;
+
+	err = store_commit_id(commit_ref_name, commit_id, repo);
+	if (err)
+		goto done;
+
+	err = rebase_merge_files(merged_paths, commit_ref_name, worktree,
+	    parent_commit_id, commit_id, repo, progress_cb, progress_arg,
+	    cancel_cb, cancel_arg);
+done:
+	free(commit_ref_name);
+	return err;
+}
+
+const struct got_error *
+got_worktree_histedit_merge_files(struct got_pathlist_head *merged_paths,
+    struct got_worktree *worktree, struct got_object_id *parent_commit_id,
+    struct got_object_id *commit_id, struct got_repository *repo,
+    got_worktree_checkout_cb progress_cb, void *progress_arg,
+    got_worktree_cancel_cb cancel_cb, void *cancel_arg)
+{
+	const struct got_error *err;
+	char *commit_ref_name;
+
+	err = get_histedit_commit_ref_name(&commit_ref_name, worktree);
+	if (err)
+		return err;
+
+	err = store_commit_id(commit_ref_name, commit_id, repo);
+	if (err)
+		goto done;
+
+	err = rebase_merge_files(merged_paths, commit_ref_name, worktree,
+	    parent_commit_id, commit_id, repo, progress_cb, progress_arg,
+	    cancel_cb, cancel_arg);
+done:
+	free(commit_ref_name);
+	return err;
+}
+
+static const struct got_error *
+rebase_commit(struct got_object_id **new_commit_id,
+    struct got_pathlist_head *merged_paths, struct got_reference *commit_ref,
+    struct got_worktree *worktree, struct got_reference *tmp_branch,
+    struct got_commit_object *orig_commit, const char *new_logmsg,
+    struct got_repository *repo)
+{
 	const struct got_error *err, *sync_err;
 	struct got_pathlist_head commitable_paths;
 	struct collect_commitables_arg cc_arg;
 	struct got_fileindex *fileindex = NULL;
-	char *fileindex_path = NULL, *commit_ref_name = NULL;
+	char *fileindex_path = NULL;
 	struct got_reference *head_ref = NULL;
 	struct got_object_id *head_commit_id = NULL;
-	struct got_reference *commit_ref = NULL;
-	struct got_object_id *commit_id = NULL;
+	char *logmsg = NULL;
 
 	TAILQ_INIT(&commitable_paths);
 	*new_commit_id = NULL;
 
 	/* Work tree is locked/unlocked during rebase preparation/teardown. */
-
-	err = get_rebase_commit_ref_name(&commit_ref_name, worktree);
-	if (err)
-		return err;
-	err = got_ref_open(&commit_ref, repo, commit_ref_name, 0);
-	if (err)
-		goto done;
-	err = got_ref_resolve(&commit_id, repo, commit_ref);
-	if (err)
-		goto done;
-	if (got_object_id_cmp(commit_id, orig_commit_id) != 0) {
-		err = got_error(GOT_ERR_REBASE_COMMITID);
-		goto done;
-	}
 
 	err = open_fileindex(&fileindex, &fileindex_path, worktree);
 	if (err)
@@ -3963,11 +4042,17 @@ got_worktree_rebase_commit(struct got_object_id **new_
 	if (err)
 		goto done;
 
+	if (new_logmsg)
+		logmsg = strdup(new_logmsg);
+	else
+		logmsg = strdup(got_object_commit_get_logmsg(orig_commit));
+	if (logmsg == NULL)
+		return got_error_from_errno("strdup");
+
 	err = commit_worktree(new_commit_id, &commitable_paths, head_commit_id,
 	    worktree, NULL, got_object_commit_get_author(orig_commit),
 	    got_object_commit_get_committer(orig_commit),
-	    collect_rebase_commit_msg, orig_commit,
-	    rebase_status, NULL, repo);
+	    collect_rebase_commit_msg, logmsg, rebase_status, NULL, repo);
 	if (err)
 		goto done;
 
@@ -3988,9 +4073,6 @@ done:
 	if (fileindex)
 		got_fileindex_free(fileindex);
 	free(fileindex_path);
-	free(commit_ref_name);
-	if (commit_ref)
-		got_ref_close(commit_ref);
 	free(head_commit_id);
 	if (head_ref)
 		got_ref_close(head_ref);
@@ -4002,6 +4084,79 @@ done:
 }
 
 const struct got_error *
+got_worktree_rebase_commit(struct got_object_id **new_commit_id,
+    struct got_pathlist_head *merged_paths, struct got_worktree *worktree,
+    struct got_reference *tmp_branch, struct got_commit_object *orig_commit,
+    struct got_object_id *orig_commit_id, struct got_repository *repo)
+{
+	const struct got_error *err;
+	char *commit_ref_name;
+	struct got_reference *commit_ref = NULL;
+	struct got_object_id *commit_id = NULL;
+
+	err = get_rebase_commit_ref_name(&commit_ref_name, worktree);
+	if (err)
+		return err;
+
+	err = got_ref_open(&commit_ref, repo, commit_ref_name, 0);
+	if (err)
+		goto done;
+	err = got_ref_resolve(&commit_id, repo, commit_ref);
+	if (err)
+		goto done;
+	if (got_object_id_cmp(commit_id, orig_commit_id) != 0) {
+		err = got_error(GOT_ERR_REBASE_COMMITID);
+		goto done;
+	}
+
+	err = rebase_commit(new_commit_id, merged_paths, commit_ref,
+	    worktree, tmp_branch, orig_commit, NULL, repo);
+done:
+	if (commit_ref)
+		got_ref_close(commit_ref);
+	free(commit_ref_name);
+	free(commit_id);
+	return err;
+}
+
+const struct got_error *
+got_worktree_histedit_commit(struct got_object_id **new_commit_id,
+    struct got_pathlist_head *merged_paths, struct got_worktree *worktree,
+    struct got_reference *tmp_branch, struct got_commit_object *orig_commit,
+    struct got_object_id *orig_commit_id, const char *new_logmsg,
+    struct got_repository *repo)
+{
+	const struct got_error *err;
+	char *commit_ref_name;
+	struct got_reference *commit_ref = NULL;
+	struct got_object_id *commit_id = NULL;
+
+	err = get_histedit_commit_ref_name(&commit_ref_name, worktree);
+	if (err)
+		return err;
+
+	err = got_ref_open(&commit_ref, repo, commit_ref_name, 0);
+	if (err)
+		goto done;
+	err = got_ref_resolve(&commit_id, repo, commit_ref);
+	if (err)
+		goto done;
+	if (got_object_id_cmp(commit_id, orig_commit_id) != 0) {
+		err = got_error(GOT_ERR_HISTEDIT_COMMITID);
+		goto done;
+	}
+
+	err = rebase_commit(new_commit_id, merged_paths, commit_ref,
+	    worktree, tmp_branch, orig_commit, new_logmsg, repo);
+done:
+	if (commit_ref)
+		got_ref_close(commit_ref);
+	free(commit_ref_name);
+	free(commit_id);
+	return err;
+}
+
+const struct got_error *
 got_worktree_rebase_postpone(struct got_worktree *worktree)
 {
 	return lock_worktree(worktree, LOCK_SH);
@@ -4212,8 +4367,339 @@ done:
 	got_ref_close(resolved);
 	free(tree_id);
 	free(commit_id);
+	if (fileindex)
+		got_fileindex_free(fileindex);
+	free(fileindex_path);
+	TAILQ_FOREACH(pe, &revertible_paths, entry)
+		free((char *)pe->path);
+	got_pathlist_free(&revertible_paths);
+
+	unlockerr = lock_worktree(worktree, LOCK_SH);
+	if (unlockerr && err == NULL)
+		err = unlockerr;
+	return err;
+}
+
+const struct got_error *
+got_worktree_histedit_prepare(struct got_reference **tmp_branch,
+    struct got_reference **branch_ref, struct got_object_id **base_commit_id,
+    struct got_worktree *worktree, struct got_repository *repo)
+{
+	const struct got_error *err = NULL;
+	char *tmp_branch_name = NULL;
+	char *branch_ref_name = NULL;
+	char *base_commit_ref_name = NULL;
+	struct got_fileindex *fileindex = NULL;
+	char *fileindex_path = NULL;
+	struct check_rebase_ok_arg ok_arg;
+	struct got_reference *wt_branch = NULL;
+	struct got_reference *base_commit_ref = NULL;
+
+	*tmp_branch = NULL;
+	*branch_ref = NULL;
+	*base_commit_id = NULL;
+
+	err = lock_worktree(worktree, LOCK_EX);
+	if (err)
+		return err;
+
+	err = open_fileindex(&fileindex, &fileindex_path, worktree);
+	if (err)
+		goto done;
+
+	ok_arg.worktree = worktree;
+	ok_arg.repo = repo;
+	ok_arg.rebase_in_progress = 0;
+	err = got_fileindex_for_each_entry_safe(fileindex, check_rebase_ok,
+	    &ok_arg);
+	if (err)
+		goto done;
+
+	err = get_histedit_tmp_ref_name(&tmp_branch_name, worktree);
+	if (err)
+		goto done;
+
+	err = get_histedit_branch_symref_name(&branch_ref_name, worktree);
+	if (err)
+		goto done;
+
+	err = get_histedit_base_commit_ref_name(&base_commit_ref_name,
+	    worktree);
+	if (err)
+		goto done;
+
+	err = got_ref_open(&wt_branch, repo, worktree->head_ref_name,
+	    0);
+	if (err)
+		goto done;
+
+	err = got_ref_alloc_symref(branch_ref, branch_ref_name, wt_branch);
+	if (err)
+		goto done;
+
+	err = got_ref_write(*branch_ref, repo);
+	if (err)
+		goto done;
+
+	err = got_ref_alloc(&base_commit_ref, base_commit_ref_name,
+	    worktree->base_commit_id);
+	if (err)
+		goto done;
+	err = got_ref_write(base_commit_ref, repo);
+	if (err)
+		goto done;
+	*base_commit_id = got_object_id_dup(worktree->base_commit_id);
+	if (*base_commit_id == NULL) {
+		err = got_error_from_errno("got_object_id_dup");
+		goto done;
+	}
+
+	err = got_ref_alloc(tmp_branch, tmp_branch_name,
+	    worktree->base_commit_id);
+	if (err)
+		goto done;
+	err = got_ref_write(*tmp_branch, repo);
+	if (err)
+		goto done;
+
+	err = got_worktree_set_head_ref(worktree, *tmp_branch);
+	if (err)
+		goto done;
+done:
+	free(fileindex_path);
 	if (fileindex)
 		got_fileindex_free(fileindex);
+	free(tmp_branch_name);
+	free(branch_ref_name);
+	free(base_commit_ref_name);
+	if (wt_branch)
+		got_ref_close(wt_branch);
+	if (err) {
+		if (*branch_ref) {
+			got_ref_close(*branch_ref);
+			*branch_ref = NULL;
+		}
+		if (*tmp_branch) {
+			got_ref_close(*tmp_branch);
+			*tmp_branch = NULL;
+		}
+		free(*base_commit_id);
+		lock_worktree(worktree, LOCK_SH);
+	}
+	return err;
+}
+
+const struct got_error *
+got_worktree_histedit_postpone(struct got_worktree *worktree)
+{
+	return lock_worktree(worktree, LOCK_SH);
+}
+
+const struct got_error *
+got_worktree_histedit_in_progress(int *in_progress,
+    struct got_worktree *worktree)
+{
+	const struct got_error *err;
+	char *tmp_branch_name = NULL;
+
+	err = get_histedit_tmp_ref_name(&tmp_branch_name, worktree);
+	if (err)
+		return err;
+
+	*in_progress = (strcmp(tmp_branch_name, worktree->head_ref_name) == 0);
+	free(tmp_branch_name);
+	return NULL;
+}
+
+const struct got_error *
+got_worktree_histedit_continue(struct got_object_id **commit_id,
+    struct got_reference **tmp_branch, struct got_reference **branch_ref,
+    struct got_object_id **base_commit_id,
+    struct got_worktree *worktree, struct got_repository *repo)
+{
+	const struct got_error *err;
+	char *commit_ref_name = NULL, *base_commit_ref_name = NULL;
+	char *tmp_branch_name = NULL, *branch_ref_name = NULL;
+	struct got_reference *commit_ref = NULL;
+	struct got_reference *base_commit_ref = NULL;
+
+	*commit_id = NULL;
+	*tmp_branch = NULL;
+	*base_commit_id = NULL;
+
+	err = get_histedit_tmp_ref_name(&tmp_branch_name, worktree);
+	if (err)
+		return err;
+
+	err = get_histedit_branch_symref_name(&branch_ref_name, worktree);
+	if (err)
+		goto done;
+
+	err = get_histedit_commit_ref_name(&commit_ref_name, worktree);
+	if (err)
+		goto done;
+
+	err = get_histedit_base_commit_ref_name(&base_commit_ref_name,
+	    worktree);
+	if (err)
+		goto done;
+
+	err = got_ref_open(branch_ref, repo, branch_ref_name, 0);
+	if (err)
+		goto done;
+
+	err = got_ref_open(&commit_ref, repo, commit_ref_name, 0);
+	if (err)
+		goto done;
+	err = got_ref_resolve(commit_id, repo, commit_ref);
+	if (err)
+		goto done;
+
+	err = got_ref_open(&base_commit_ref, repo, base_commit_ref_name, 0);
+	if (err)
+		goto done;
+	err = got_ref_resolve(base_commit_id, repo, base_commit_ref);
+	if (err)
+		goto done;
+
+	err = got_ref_open(tmp_branch, repo, tmp_branch_name, 0);
+	if (err)
+		goto done;
+done:
+	free(commit_ref_name);
+	free(branch_ref_name);
+	if (commit_ref)
+		got_ref_close(commit_ref);
+	if (base_commit_ref)
+		got_ref_close(base_commit_ref);
+	if (err) {
+		free(*commit_id);
+		*commit_id = NULL;
+		free(*base_commit_id);
+		*base_commit_id = NULL;
+		if (*tmp_branch) {
+			got_ref_close(*tmp_branch);
+			*tmp_branch = NULL;
+		}
+	}
+	return err;
+}
+
+static const struct got_error *
+delete_histedit_refs(struct got_worktree *worktree, struct got_repository *repo)
+{
+	const struct got_error *err;
+	char *tmp_branch_name = NULL, *base_commit_ref_name = NULL;
+	char *branch_ref_name = NULL, *commit_ref_name = NULL;
+
+	err = get_histedit_tmp_ref_name(&tmp_branch_name, worktree);
+	if (err)
+		goto done;
+	err = delete_ref(tmp_branch_name, repo);
+	if (err)
+		goto done;
+
+	err = get_histedit_base_commit_ref_name(&base_commit_ref_name,
+	    worktree);
+	if (err)
+		goto done;
+	err = delete_ref(base_commit_ref_name, repo);
+	if (err)
+		goto done;
+
+	err = get_histedit_branch_symref_name(&branch_ref_name, worktree);
+	if (err)
+		goto done;
+	err = delete_ref(branch_ref_name, repo);
+	if (err)
+		goto done;
+
+	err = get_histedit_commit_ref_name(&commit_ref_name, worktree);
+	if (err)
+		goto done;
+	err = delete_ref(commit_ref_name, repo);
+	if (err)
+		goto done;
+done:
+	free(tmp_branch_name);
+	free(base_commit_ref_name);
+	free(branch_ref_name);
+	free(commit_ref_name);
+	return err;
+}
+
+const struct got_error *
+got_worktree_histedit_abort(struct got_worktree *worktree,
+    struct got_repository *repo, struct got_reference *branch,
+    struct got_object_id *base_commit_id,
+    got_worktree_checkout_cb progress_cb, void *progress_arg)
+{
+	const struct got_error *err, *unlockerr, *sync_err;
+	struct got_reference *resolved = NULL;
+	struct got_fileindex *fileindex = NULL;
+	char *fileindex_path = NULL;
+	struct got_pathlist_head revertible_paths;
+	struct got_pathlist_entry *pe;
+	struct collect_revertible_paths_arg crp_arg;
+	struct got_object_id *tree_id = NULL;
+
+	TAILQ_INIT(&revertible_paths);
+
+	err = lock_worktree(worktree, LOCK_EX);
+	if (err)
+		return err;
+
+	err = got_ref_open(&resolved, repo,
+	    got_ref_get_symref_target(branch), 0);
+	if (err)
+		goto done;
+
+	err = got_worktree_set_head_ref(worktree, resolved);
+	if (err)
+		goto done;
+
+	err = got_worktree_set_base_commit_id(worktree, repo, base_commit_id);
+	if (err)
+		goto done;
+
+	err = got_object_id_by_path(&tree_id, repo, base_commit_id,
+	    worktree->path_prefix);
+	if (err)
+		goto done;
+
+	err = delete_histedit_refs(worktree, repo);
+	if (err)
+		goto done;
+
+	err = open_fileindex(&fileindex, &fileindex_path, worktree);
+	if (err)
+		goto done;
+
+	crp_arg.revertible_paths = &revertible_paths;
+	crp_arg.worktree = worktree;
+	err = worktree_status(worktree, "", fileindex, repo,
+	    collect_revertible_paths, &crp_arg, NULL, NULL);
+	if (err)
+		goto done;
+
+	TAILQ_FOREACH(pe, &revertible_paths, entry) {
+		err = revert_file(worktree, fileindex, pe->path,
+		    progress_cb, progress_arg, repo);
+		if (err)
+			goto sync;
+	}
+
+	err = checkout_files(worktree, fileindex, "", tree_id, NULL,
+	    repo, progress_cb, progress_arg, NULL, NULL);
+sync:
+	sync_err = sync_fileindex(fileindex, fileindex_path);
+	if (sync_err && err == NULL)
+		err = sync_err;
+done:
+	got_ref_close(resolved);
+	free(tree_id);
+	if (fileindex)
+		got_fileindex_free(fileindex);
 	free(fileindex_path);
 	TAILQ_FOREACH(pe, &revertible_paths, entry)
 		free((char *)pe->path);
@@ -4224,3 +4710,63 @@ done:
 		err = unlockerr;
 	return err;
 }
+
+const struct got_error *
+got_worktree_histedit_complete(struct got_worktree *worktree,
+    struct got_reference *tmp_branch, struct got_reference *edited_branch,
+    struct got_repository *repo)
+{
+	const struct got_error *err, *unlockerr;
+	struct got_object_id *new_head_commit_id = NULL;
+	struct got_reference *resolved = NULL;
+
+	err = got_ref_resolve(&new_head_commit_id, repo, tmp_branch);
+	if (err)
+		return err;
+
+	err = got_ref_open(&resolved, repo,
+	    got_ref_get_symref_target(edited_branch), 0);
+	if (err)
+		goto done;
+
+	err = got_ref_change_ref(resolved, new_head_commit_id);
+	if (err)
+		goto done;
+
+	err = got_ref_write(resolved, repo);
+	if (err)
+		goto done;
+
+	err = got_worktree_set_head_ref(worktree, resolved);
+	if (err)
+		goto done;
+
+	err = delete_histedit_refs(worktree, repo);
+done:
+	free(new_head_commit_id);
+	unlockerr = lock_worktree(worktree, LOCK_SH);
+	if (unlockerr && err == NULL)
+		err = unlockerr;
+	return err;
+}
+
+const struct got_error *
+got_worktree_histedit_skip_commit(struct got_worktree *worktree,
+    struct got_object_id *commit_id, struct got_repository *repo)
+{
+	const struct got_error *err;
+	char *commit_ref_name;
+
+	err = get_histedit_commit_ref_name(&commit_ref_name, worktree);
+	if (err)
+		return err;
+
+	err = store_commit_id(commit_ref_name, commit_id, repo);
+	if (err)
+		goto done;
+
+	err = delete_ref(commit_ref_name, repo);
+done:
+	free(commit_ref_name);
+	return err;
+}
blob - 682c9e1542649798fd47bc71d7532329397d40f9
blob + 191b50efa64f9e598f4f8865387623806d8453c5
--- regress/cmdline/Makefile
+++ regress/cmdline/Makefile
@@ -1,5 +1,5 @@
 REGRESS_TARGETS=checkout update status log add rm diff commit \
-	cherrypick backout rebase import
+	cherrypick backout rebase import histedit
 NOOBJ=Yes
 
 checkout:
@@ -38,4 +38,7 @@ rebase:
 import:
 	./import.sh
 
+histedit:
+	./histedit.sh
+
 .include <bsd.regress.mk>
blob - /dev/null
blob + 98604e6814628faa295d9b0a155046626d878a18 (mode 755)
--- /dev/null
+++ regress/cmdline/histedit.sh
@@ -0,0 +1,772 @@
+#!/bin/sh
+#
+# Copyright (c) 2019 Stefan Sperling <stsp@openbsd.org>
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+. ./common.sh
+
+function test_histedit_no_op {
+	local testroot=`test_init histedit_no_op`
+
+	local orig_commit=`git_show_head $testroot/repo`
+
+	echo "modified alpha on master" > $testroot/repo/alpha
+	(cd $testroot/repo && git rm -q beta)
+	echo "new file on master" > $testroot/repo/epsilon/new
+	(cd $testroot/repo && git add epsilon/new)
+	git_commit $testroot/repo -m "committing changes"
+	local old_commit1=`git_show_head $testroot/repo`
+
+	echo "modified zeta on master" > $testroot/repo/epsilon/zeta
+	git_commit $testroot/repo -m "committing to zeta on master"
+	local old_commit2=`git_show_head $testroot/repo`
+
+	got checkout -c $orig_commit $testroot/repo $testroot/wt > /dev/null
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	echo "pick $old_commit1" > $testroot/histedit-script
+	echo "pick $old_commit2" >> $testroot/histedit-script
+
+	(cd $testroot/wt && got histedit -F $testroot/histedit-script \
+		> $testroot/stdout)
+
+	local new_commit1=`git_show_parent_commit $testroot/repo`
+	local new_commit2=`git_show_head $testroot/repo`
+
+	local short_old_commit1=`trim_obj_id 28 $old_commit1`
+	local short_old_commit2=`trim_obj_id 28 $old_commit2`
+	local short_new_commit1=`trim_obj_id 28 $new_commit1`
+	local short_new_commit2=`trim_obj_id 28 $new_commit2`
+
+	echo "G  alpha" > $testroot/stdout.expected
+	echo "D  beta" >> $testroot/stdout.expected
+	echo "A  epsilon/new" >> $testroot/stdout.expected
+	echo "$short_old_commit1 -> $short_new_commit1: committing changes" \
+		>> $testroot/stdout.expected
+	echo "G  epsilon/zeta" >> $testroot/stdout.expected
+	echo -n "$short_old_commit2 -> $short_new_commit2: " \
+		>> $testroot/stdout.expected
+	echo "committing to zeta on master" >> $testroot/stdout.expected
+	echo "Switching work tree to refs/heads/master" \
+		>> $testroot/stdout.expected
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	echo "modified alpha on master" > $testroot/content.expected
+	cat $testroot/wt/alpha > $testroot/content
+	cmp -s $testroot/content.expected $testroot/content
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/content.expected $testroot/content
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	if [ -e $testroot/wt/beta ]; then
+		echo "removed file beta still exists on disk" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	echo "new file on master" > $testroot/content.expected
+	cat $testroot/wt/epsilon/new > $testroot/content
+	cmp -s $testroot/content.expected $testroot/content
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/content.expected $testroot/content
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	(cd $testroot/wt && got status > $testroot/stdout)
+
+	echo -n > $testroot/stdout.expected
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	(cd $testroot/wt && got log -l3 | grep ^commit > $testroot/stdout)
+	echo "commit $new_commit2 (master)" > $testroot/stdout.expected
+	echo "commit $new_commit1" >> $testroot/stdout.expected
+	echo "commit $orig_commit" >> $testroot/stdout.expected
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+	fi
+	test_done "$testroot" "$ret"
+}
+
+function test_histedit_swap {
+	local testroot=`test_init histedit_swap`
+
+	local orig_commit=`git_show_head $testroot/repo`
+
+	echo "modified alpha on master" > $testroot/repo/alpha
+	(cd $testroot/repo && git rm -q beta)
+	echo "new file on master" > $testroot/repo/epsilon/new
+	(cd $testroot/repo && git add epsilon/new)
+	git_commit $testroot/repo -m "committing changes"
+	local old_commit1=`git_show_head $testroot/repo`
+
+	echo "modified zeta on master" > $testroot/repo/epsilon/zeta
+	git_commit $testroot/repo -m "committing to zeta on master"
+	local old_commit2=`git_show_head $testroot/repo`
+
+	got checkout -c $orig_commit $testroot/repo $testroot/wt > /dev/null
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	echo "pick $old_commit2" > $testroot/histedit-script
+	echo "pick $old_commit1" >> $testroot/histedit-script
+
+	(cd $testroot/wt && got histedit -F $testroot/histedit-script \
+		> $testroot/stdout)
+
+	local new_commit2=`git_show_parent_commit $testroot/repo`
+	local new_commit1=`git_show_head $testroot/repo`
+
+	local short_old_commit1=`trim_obj_id 28 $old_commit1`
+	local short_old_commit2=`trim_obj_id 28 $old_commit2`
+	local short_new_commit1=`trim_obj_id 28 $new_commit1`
+	local short_new_commit2=`trim_obj_id 28 $new_commit2`
+
+	echo "G  epsilon/zeta" > $testroot/stdout.expected
+	echo -n "$short_old_commit2 -> $short_new_commit2: " \
+		>> $testroot/stdout.expected
+	echo "committing to zeta on master" >> $testroot/stdout.expected
+	echo "G  alpha" >> $testroot/stdout.expected
+	echo "D  beta" >> $testroot/stdout.expected
+	echo "A  epsilon/new" >> $testroot/stdout.expected
+	echo "$short_old_commit1 -> $short_new_commit1: committing changes" \
+		>> $testroot/stdout.expected
+	echo "Switching work tree to refs/heads/master" \
+		>> $testroot/stdout.expected
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	echo "modified alpha on master" > $testroot/content.expected
+	cat $testroot/wt/alpha > $testroot/content
+	cmp -s $testroot/content.expected $testroot/content
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/content.expected $testroot/content
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	if [ -e $testroot/wt/beta ]; then
+		echo "removed file beta still exists on disk" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	echo "new file on master" > $testroot/content.expected
+	cat $testroot/wt/epsilon/new > $testroot/content
+	cmp -s $testroot/content.expected $testroot/content
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/content.expected $testroot/content
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	(cd $testroot/wt && got status > $testroot/stdout)
+
+	echo -n > $testroot/stdout.expected
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	(cd $testroot/wt && got log -l3 | grep ^commit > $testroot/stdout)
+	echo "commit $new_commit1 (master)" > $testroot/stdout.expected
+	echo "commit $new_commit2" >> $testroot/stdout.expected
+	echo "commit $orig_commit" >> $testroot/stdout.expected
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+	fi
+	test_done "$testroot" "$ret"
+}
+
+function test_histedit_drop {
+	local testroot=`test_init histedit_drop`
+
+	local orig_commit=`git_show_head $testroot/repo`
+
+	echo "modified alpha on master" > $testroot/repo/alpha
+	(cd $testroot/repo && git rm -q beta)
+	echo "new file on master" > $testroot/repo/epsilon/new
+	(cd $testroot/repo && git add epsilon/new)
+	git_commit $testroot/repo -m "committing changes"
+	local old_commit1=`git_show_head $testroot/repo`
+
+	echo "modified zeta on master" > $testroot/repo/epsilon/zeta
+	git_commit $testroot/repo -m "committing to zeta on master"
+	local old_commit2=`git_show_head $testroot/repo`
+
+	got checkout -c $orig_commit $testroot/repo $testroot/wt > /dev/null
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	echo "drop $old_commit1" > $testroot/histedit-script
+	echo "pick $old_commit2" >> $testroot/histedit-script
+
+	(cd $testroot/wt && got histedit -F $testroot/histedit-script \
+		> $testroot/stdout)
+
+	local new_commit1=`git_show_parent_commit $testroot/repo`
+	local new_commit2=`git_show_head $testroot/repo`
+
+	local short_old_commit1=`trim_obj_id 28 $old_commit1`
+	local short_old_commit2=`trim_obj_id 28 $old_commit2`
+	local short_new_commit1=`trim_obj_id 28 $new_commit1`
+	local short_new_commit2=`trim_obj_id 28 $new_commit2`
+
+	echo "$short_old_commit1 ->  drop commit: committing changes" \
+		> $testroot/stdout.expected
+	echo "G  epsilon/zeta" >> $testroot/stdout.expected
+	echo -n "$short_old_commit2 -> $short_new_commit2: " \
+		>> $testroot/stdout.expected
+	echo "committing to zeta on master" >> $testroot/stdout.expected
+	echo "Switching work tree to refs/heads/master" \
+		>> $testroot/stdout.expected
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	for f in alpha beta; do
+		echo "$f" > $testroot/content.expected
+		cat $testroot/wt/$f > $testroot/content
+		cmp -s $testroot/content.expected $testroot/content
+		ret="$?"
+		if [ "$ret" != "0" ]; then
+			diff -u $testroot/content.expected $testroot/content
+			test_done "$testroot" "$ret"
+			return 1
+		fi
+	done
+
+	if [ -e $testroot/wt/new ]; then
+		echo "file new exists on disk but should not" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	(cd $testroot/wt && got status > $testroot/stdout)
+
+	echo -n > $testroot/stdout.expected
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	(cd $testroot/wt && got log -l3 | grep ^commit > $testroot/stdout)
+	echo "commit $new_commit2 (master)" > $testroot/stdout.expected
+	echo "commit $orig_commit" >> $testroot/stdout.expected
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+	fi
+	test_done "$testroot" "$ret"
+}
+
+function test_histedit_fold {
+	local testroot=`test_init histedit_fold`
+
+	local orig_commit=`git_show_head $testroot/repo`
+
+	echo "modified alpha on master" > $testroot/repo/alpha
+	(cd $testroot/repo && git rm -q beta)
+	echo "new file on master" > $testroot/repo/epsilon/new
+	(cd $testroot/repo && git add epsilon/new)
+	git_commit $testroot/repo -m "committing changes"
+	local old_commit1=`git_show_head $testroot/repo`
+
+	echo "modified zeta on master" > $testroot/repo/epsilon/zeta
+	git_commit $testroot/repo -m "committing to zeta on master"
+	local old_commit2=`git_show_head $testroot/repo`
+
+	got checkout -c $orig_commit $testroot/repo $testroot/wt > /dev/null
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	echo "fold $old_commit1" > $testroot/histedit-script
+	echo "pick $old_commit2" >> $testroot/histedit-script
+	echo "mesg committing folded changes" >> $testroot/histedit-script
+
+	(cd $testroot/wt && got histedit -F $testroot/histedit-script \
+		> $testroot/stdout)
+
+	local new_commit1=`git_show_parent_commit $testroot/repo`
+	local new_commit2=`git_show_head $testroot/repo`
+
+	local short_old_commit1=`trim_obj_id 28 $old_commit1`
+	local short_old_commit2=`trim_obj_id 28 $old_commit2`
+	local short_new_commit1=`trim_obj_id 28 $new_commit1`
+	local short_new_commit2=`trim_obj_id 28 $new_commit2`
+
+	echo "G  alpha" > $testroot/stdout.expected
+	echo "D  beta" >> $testroot/stdout.expected
+	echo "A  epsilon/new" >> $testroot/stdout.expected
+	echo "$short_old_commit1 ->  fold commit: committing changes" \
+		>> $testroot/stdout.expected
+	echo "G  epsilon/zeta" >> $testroot/stdout.expected
+	echo -n "$short_old_commit2 -> $short_new_commit2: " \
+		>> $testroot/stdout.expected
+	echo "committing folded changes" >> $testroot/stdout.expected
+	echo "Switching work tree to refs/heads/master" \
+		>> $testroot/stdout.expected
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	echo "modified alpha on master" > $testroot/content.expected
+	cat $testroot/wt/alpha > $testroot/content
+	cmp -s $testroot/content.expected $testroot/content
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/content.expected $testroot/content
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	if [ -e $testroot/wt/beta ]; then
+		echo "removed file beta still exists on disk" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	echo "new file on master" > $testroot/content.expected
+	cat $testroot/wt/epsilon/new > $testroot/content
+	cmp -s $testroot/content.expected $testroot/content
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/content.expected $testroot/content
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	(cd $testroot/wt && got status > $testroot/stdout)
+
+	echo -n > $testroot/stdout.expected
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	(cd $testroot/wt && got log -l3 | grep ^commit > $testroot/stdout)
+	echo "commit $new_commit2 (master)" > $testroot/stdout.expected
+	echo "commit $orig_commit" >> $testroot/stdout.expected
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+	fi
+	test_done "$testroot" "$ret"
+}
+
+function test_histedit_edit {
+	local testroot=`test_init histedit_edit`
+
+	local orig_commit=`git_show_head $testroot/repo`
+
+	echo "modified alpha on master" > $testroot/repo/alpha
+	(cd $testroot/repo && git rm -q beta)
+	echo "new file on master" > $testroot/repo/epsilon/new
+	(cd $testroot/repo && git add epsilon/new)
+	git_commit $testroot/repo -m "committing changes"
+	local old_commit1=`git_show_head $testroot/repo`
+
+	echo "modified zeta on master" > $testroot/repo/epsilon/zeta
+	git_commit $testroot/repo -m "committing to zeta on master"
+	local old_commit2=`git_show_head $testroot/repo`
+
+	got checkout -c $orig_commit $testroot/repo $testroot/wt > /dev/null
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	echo "edit $old_commit1" > $testroot/histedit-script
+	echo "mesg committing changes" >> $testroot/histedit-script
+	echo "pick $old_commit2" >> $testroot/histedit-script
+
+	(cd $testroot/wt && got histedit -F $testroot/histedit-script \
+		> $testroot/stdout)
+
+	local short_old_commit1=`trim_obj_id 28 $old_commit1`
+	local short_old_commit2=`trim_obj_id 28 $old_commit2`
+
+	echo "G  alpha" > $testroot/stdout.expected
+	echo "D  beta" >> $testroot/stdout.expected
+	echo "A  epsilon/new" >> $testroot/stdout.expected
+	echo "Stopping histedit for amending commit $old_commit1" \
+		>> $testroot/stdout.expected
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	echo "edited modified alpha on master" > $testroot/wt/alpha
+
+	(cd $testroot/wt && got histedit -c > $testroot/stdout)
+
+	local new_commit1=`git_show_parent_commit $testroot/repo`
+	local new_commit2=`git_show_head $testroot/repo`
+
+	local short_new_commit1=`trim_obj_id 28 $new_commit1`
+	local short_new_commit2=`trim_obj_id 28 $new_commit2`
+
+	echo "$short_old_commit1 -> $short_new_commit1: committing changes" \
+		> $testroot/stdout.expected
+	echo "G  epsilon/zeta" >> $testroot/stdout.expected
+	echo -n "$short_old_commit2 -> $short_new_commit2: " \
+		>> $testroot/stdout.expected
+	echo "committing to zeta on master" >> $testroot/stdout.expected
+	echo "Switching work tree to refs/heads/master" \
+		>> $testroot/stdout.expected
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	echo "edited modified alpha on master" > $testroot/content.expected
+	cat $testroot/wt/alpha > $testroot/content
+	cmp -s $testroot/content.expected $testroot/content
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/content.expected $testroot/content
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	if [ -e $testroot/wt/beta ]; then
+		echo "removed file beta still exists on disk" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	echo "new file on master" > $testroot/content.expected
+	cat $testroot/wt/epsilon/new > $testroot/content
+	cmp -s $testroot/content.expected $testroot/content
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/content.expected $testroot/content
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	(cd $testroot/wt && got status > $testroot/stdout)
+
+	echo -n > $testroot/stdout.expected
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	(cd $testroot/wt && got log -l3 | grep ^commit > $testroot/stdout)
+	echo "commit $new_commit2 (master)" > $testroot/stdout.expected
+	echo "commit $new_commit1" >> $testroot/stdout.expected
+	echo "commit $orig_commit" >> $testroot/stdout.expected
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+	fi
+	test_done "$testroot" "$ret"
+}
+
+function test_histedit_fold_last_commit {
+	local testroot=`test_init histedit_fold_last_commit`
+
+	local orig_commit=`git_show_head $testroot/repo`
+
+	echo "modified alpha on master" > $testroot/repo/alpha
+	(cd $testroot/repo && git rm -q beta)
+	echo "new file on master" > $testroot/repo/epsilon/new
+	(cd $testroot/repo && git add epsilon/new)
+	git_commit $testroot/repo -m "committing changes"
+	local old_commit1=`git_show_head $testroot/repo`
+
+	echo "modified zeta on master" > $testroot/repo/epsilon/zeta
+	git_commit $testroot/repo -m "committing to zeta on master"
+	local old_commit2=`git_show_head $testroot/repo`
+
+	got checkout -c $orig_commit $testroot/repo $testroot/wt > /dev/null
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	echo "pick $old_commit1" > $testroot/histedit-script
+	echo "fold $old_commit2" >> $testroot/histedit-script
+	echo "mesg committing folded changes" >> $testroot/histedit-script
+
+	(cd $testroot/wt && got histedit -F $testroot/histedit-script \
+		> $testroot/stdout 2> $testroot/stderr)
+
+	ret="$?"
+	if [ "$ret" == "0" ]; then
+		echo "histedit succeeded unexpectedly" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	echo "got: last commit in histedit script cannot be folded" \
+		> $testroot/stderr.expected
+
+	cmp -s $testroot/stderr.expected $testroot/stderr
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stderr.expected $testroot/stderr
+	fi
+	test_done "$testroot" "$ret"
+}
+
+function test_histedit_missing_commit {
+	local testroot=`test_init histedit_missing_commit`
+
+	local orig_commit=`git_show_head $testroot/repo`
+
+	echo "modified alpha on master" > $testroot/repo/alpha
+	(cd $testroot/repo && git rm -q beta)
+	echo "new file on master" > $testroot/repo/epsilon/new
+	(cd $testroot/repo && git add epsilon/new)
+	git_commit $testroot/repo -m "committing changes"
+	local old_commit1=`git_show_head $testroot/repo`
+
+	echo "modified zeta on master" > $testroot/repo/epsilon/zeta
+	git_commit $testroot/repo -m "committing to zeta on master"
+	local old_commit2=`git_show_head $testroot/repo`
+
+	got checkout -c $orig_commit $testroot/repo $testroot/wt > /dev/null
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	echo "pick $old_commit1" > $testroot/histedit-script
+	echo "mesg committing changes" >> $testroot/histedit-script
+
+	(cd $testroot/wt && got histedit -F $testroot/histedit-script \
+		> $testroot/stdout 2> $testroot/stderr)
+
+	ret="$?"
+	if [ "$ret" == "0" ]; then
+		echo "histedit succeeded unexpectedly" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	echo "got: commit $old_commit2 missing from histedit script" \
+		> $testroot/stderr.expected
+
+	cmp -s $testroot/stderr.expected $testroot/stderr
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stderr.expected $testroot/stderr
+	fi
+	test_done "$testroot" "$ret"
+}
+
+function test_histedit_abort {
+	local testroot=`test_init histedit_abort`
+
+	local orig_commit=`git_show_head $testroot/repo`
+
+	echo "modified alpha on master" > $testroot/repo/alpha
+	(cd $testroot/repo && git rm -q beta)
+	echo "new file on master" > $testroot/repo/epsilon/new
+	(cd $testroot/repo && git add epsilon/new)
+	git_commit $testroot/repo -m "committing changes"
+	local old_commit1=`git_show_head $testroot/repo`
+
+	echo "modified zeta on master" > $testroot/repo/epsilon/zeta
+	git_commit $testroot/repo -m "committing to zeta on master"
+	local old_commit2=`git_show_head $testroot/repo`
+
+	got checkout -c $orig_commit $testroot/repo $testroot/wt > /dev/null
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	echo "edit $old_commit1" > $testroot/histedit-script
+	echo "mesg committing changes" >> $testroot/histedit-script
+	echo "pick $old_commit2" >> $testroot/histedit-script
+
+	(cd $testroot/wt && got histedit -F $testroot/histedit-script \
+		> $testroot/stdout)
+
+	local short_old_commit1=`trim_obj_id 28 $old_commit1`
+	local short_old_commit2=`trim_obj_id 28 $old_commit2`
+
+	echo "G  alpha" > $testroot/stdout.expected
+	echo "D  beta" >> $testroot/stdout.expected
+	echo "A  epsilon/new" >> $testroot/stdout.expected
+	echo "Stopping histedit for amending commit $old_commit1" \
+		>> $testroot/stdout.expected
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	echo "edited modified alpha on master" > $testroot/wt/alpha
+
+	(cd $testroot/wt && got histedit -a > $testroot/stdout)
+
+	local new_commit1=`git_show_parent_commit $testroot/repo`
+	local new_commit2=`git_show_head $testroot/repo`
+
+	local short_new_commit1=`trim_obj_id 28 $new_commit1`
+	local short_new_commit2=`trim_obj_id 28 $new_commit2`
+
+	echo "Switching work tree to refs/heads/master" \
+		> $testroot/stdout.expected
+	echo "R  alpha" >> $testroot/stdout.expected
+	echo "R  beta" >> $testroot/stdout.expected
+	echo "R  epsilon/new" >> $testroot/stdout.expected
+	echo "Histedit of refs/heads/master aborted" \
+		>> $testroot/stdout.expected
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	for f in alpha beta; do
+		echo "$f" > $testroot/content.expected
+		cat $testroot/wt/$f > $testroot/content
+		cmp -s $testroot/content.expected $testroot/content
+		ret="$?"
+		if [ "$ret" != "0" ]; then
+			diff -u $testroot/content.expected $testroot/content
+			test_done "$testroot" "$ret"
+			return 1
+		fi
+	done
+
+	echo "new file on master" > $testroot/content.expected
+	cat $testroot/wt/epsilon/new > $testroot/content
+	cmp -s $testroot/content.expected $testroot/content
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/content.expected $testroot/content
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	(cd $testroot/wt && got status > $testroot/stdout)
+
+	echo "?  epsilon/new" > $testroot/stdout.expected
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	(cd $testroot/wt && got log -l3 | grep ^commit > $testroot/stdout)
+	echo "commit $new_commit2 (master)" > $testroot/stdout.expected
+	echo "commit $new_commit1" >> $testroot/stdout.expected
+	echo "commit $orig_commit" >> $testroot/stdout.expected
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+	fi
+	test_done "$testroot" "$ret"
+}
+
+run_test test_histedit_no_op
+run_test test_histedit_swap
+run_test test_histedit_drop
+run_test test_histedit_fold
+run_test test_histedit_edit
+run_test test_histedit_fold_last_commit
+run_test test_histedit_missing_commit
+run_test test_histedit_abort