"GOT", but the "O" is a cute, smiling pufferfish. Index | Thread | Search

From:
Stefan Sperling <stsp@stsp.name>
Subject:
add a 'got merge' command
To:
gameoftrees@openbsd.org
Date:
Fri, 24 Sep 2021 18:35:57 +0200

Download raw body.

Thread
Adding a merge command is an item in our TODO file, and it was now also
requested by a -portable user on IRC. This patch provides an initial
implementation to accommodate workflows which mandate merge commits.

This does not yet implement the "sub directory" merge idea mentioned
in the TODO file. That requires more work and would also apply to
other commands which merge changes, such as cherrypick and backout.

Test coverage could be improved but I wanted to share what I already have.

ok?

diff refs/heads/main refs/heads/merge-cmd
blob - 7908e0d4a3f37ad292df5a94592a0e75437876a3
blob + 47ed7daac9b47b50711a04958e81be7d7cddafb8
--- got/got.1
+++ got/got.1
@@ -2132,6 +2132,123 @@ or reverted with
 .It Cm ig
 Short alias for
 .Cm integrate .
+.It Cm merge Oo Fl a Oc Oo Fl c Oc Op Ar branch
+Create a merge commit based on the current branch of the work tree and
+the specified
+.Ar branch .
+If a linear project history is desired then use of
+.Cm got rebase
+should be preferred over
+.Cm got merge .
+However, even strictly linear projects may require merge commits in order
+to merge in new versions of code imported from third-party projects on
+vendor branches.
+.Pp
+Merge commits are commits based on multiple parent commits.
+The tip commit of the work tree's current branch, which must be set with
+.Cm got update -b
+before starting the
+.Cm merge
+operation, will be used as the first parent.
+The tip commit of the specified
+.Ar branch
+will be used as the second parent.
+.Pp
+It is not possible to create merge commits with more than two parents.
+If more than one branch needs to be merged then multiple merge commits
+with two parents each can be created in sequence.
+.Pp
+The
+.Ar branch
+must share common ancestry with the work tree's current branch.
+.Pp
+While merging changes found on the
+.Ar branch
+into the work tree, 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 \(a~ Ta changes destined for a non-regular file were not merged
+.It ? Ta changes destined for an unversioned file were not merged
+.El
+.Pp
+If merge conflicts occur the merge operation is interrupted and conflicts
+must be resolved before the merge operation can continue.
+If any files with destined changes are found to be missing or obstructed
+the merge operation will be interrupted to prevent potentially incomplete
+changes from being committed to the repository without user intervention.
+The work tree may be modified as desired and the merge can be continued
+once the changes present in the work tree are considered complete.
+Alternatively, the merge operation may be aborted which will leave
+the work tree's current branch unmodified.
+.Pp
+If a merge conflict is resolved in a way which renders all merged
+changes into no-op changes, the merge operation cannot continue
+and must be aborted.
+.Pp
+.Cm got merge
+will refuse to run if certain preconditions are not met.
+If history of the
+.Ar branch
+is based on the work tree's current branch then no merge commit can
+be created and
+.Cm got integrate
+may be used to integrate the
+.Ar branch
+instead.
+If the work tree is not yet fully updated to the tip commit of its
+branch then the work tree must first be updated with
+.Cm got update .
+If the work tree contains multiple base commits it must first be updated
+to a single base commit with
+.Cm got update .
+If changes have been staged with
+.Cm got stage ,
+these changes must first be committed with
+.Cm got commit
+or unstaged with
+.Cm got unstage .
+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
+.Ar branch
+contains changes to files outside of the work tree's path prefix,
+the work tree cannot be used to merge this branch.
+.Pp
+The
+.Cm got update ,
+.Cm got commit ,
+.Cm got rebase ,
+.Cm got histedit , 
+.Cm got integrate ,
+and
+.Cm got stage
+commands will refuse to run while a merge operation is in progress.
+Other commands which manipulate the work tree may be used for
+conflict resolution purposes.
+.Pp
+The options for
+.Cm got merge
+are as follows:
+.Bl -tag -width Ds
+.It Fl a
+Abort an interrupted merge operation.
+If this option is used, no other command-line arguments are allowed.
+.It Fl c
+Continue an interrupted merge operation.
+If this option is used, no other command-line arguments are allowed.
+.El
+.It Cm mg
+Short alias for
+.Cm merge .
 .It Cm stage Oo Fl l Oc Oo Fl p Oc Oo Fl F Ar response-script Oc Oo Fl S Oc Op Ar path ...
 Stage local changes for inclusion in the next commit.
 If no
blob - 4765f7e169b5a1009aa5f38fc148e35c702fda31
blob + d67f2a857566980a828112cd410b9439d76b3116
--- got/got.c
+++ got/got.c
@@ -109,6 +109,7 @@ __dead static void	usage_backout(void);
 __dead static void	usage_rebase(void);
 __dead static void	usage_histedit(void);
 __dead static void	usage_integrate(void);
+__dead static void	usage_merge(void);
 __dead static void	usage_stage(void);
 __dead static void	usage_unstage(void);
 __dead static void	usage_cat(void);
@@ -138,6 +139,7 @@ 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 const struct got_error*		cmd_integrate(int, char *[]);
+static const struct got_error*		cmd_merge(int, char *[]);
 static const struct got_error*		cmd_stage(int, char *[]);
 static const struct got_error*		cmd_unstage(int, char *[]);
 static const struct got_error*		cmd_cat(int, char *[]);
@@ -168,6 +170,7 @@ static struct got_cmd got_commands[] = {
 	{ "rebase",	cmd_rebase,	usage_rebase,	"rb" },
 	{ "histedit",	cmd_histedit,	usage_histedit,	"he" },
 	{ "integrate",  cmd_integrate,  usage_integrate,"ig" },
+	{ "merge",	cmd_merge,	usage_merge,	"mg" },
 	{ "stage",	cmd_stage,	usage_stage,	"sg" },
 	{ "unstage",	cmd_unstage,	usage_unstage,	"ug" },
 	{ "cat",	cmd_cat,	usage_cat,	"" },
@@ -3058,6 +3061,7 @@ struct got_update_progress_arg {
 	int conflicts;
 	int obstructed;
 	int not_updated;
+	int missing;
 	int verbosity;
 };
 
@@ -3107,6 +3111,8 @@ update_progress(void *arg, unsigned char status, const
 		upa->obstructed++;
 	if (status == GOT_STATUS_CANNOT_UPDATE)
 		upa->not_updated++;
+	if (status == GOT_STATUS_MISSING)
+		upa->missing++;
 
 	while (path[0] == '/')
 		path++;
@@ -3178,6 +3184,22 @@ check_rebase_or_histedit_in_progress(struct got_worktr
 }
 
 static const struct got_error *
+check_merge_in_progress(struct got_worktree *worktree,
+    struct got_repository *repo)
+{
+	const struct got_error *err;
+	int in_progress;
+
+	err = got_worktree_merge_in_progress(&in_progress, worktree, repo);
+	if (err)
+		return err;
+	if (in_progress)
+		return got_error(GOT_ERR_MERGE_BUSY);
+
+	return NULL;
+}
+
+static const struct got_error *
 get_worktree_paths_from_argv(struct got_pathlist_head *paths, int argc,
     char *argv[], struct got_worktree *worktree)
 {
@@ -3300,6 +3322,10 @@ cmd_update(int argc, char *argv[])
 	if (error)
 		goto done;
 
+	error = check_merge_in_progress(worktree, repo);
+	if (error)
+		goto done;
+
 	error = get_worktree_paths_from_argv(&paths, argc, argv, worktree);
 	if (error)
 		goto done;
@@ -7317,7 +7343,7 @@ cmd_commit(int argc, char *argv[])
 	struct collect_commit_logmsg_arg cl_arg;
 	char *gitconfig_path = NULL, *editor = NULL, *author = NULL;
 	int ch, rebase_in_progress, histedit_in_progress, preserve_logmsg = 0;
-	int allow_bad_symlinks = 0, non_interactive = 0;
+	int allow_bad_symlinks = 0, non_interactive = 0, merge_in_progress = 0;
 	struct got_pathlist_head paths;
 
 	TAILQ_INIT(&paths);
@@ -7391,6 +7417,14 @@ cmd_commit(int argc, char *argv[])
 	if (error != NULL)
 		goto done;
 
+	error = got_worktree_merge_in_progress(&merge_in_progress, worktree, repo);
+	if (error)
+		goto done;
+	if (merge_in_progress) {
+		error = got_error(GOT_ERR_MERGE_BUSY);
+		goto done;
+	}
+
 	error = get_author(&author, repo, worktree);
 	if (error)
 		return error;
@@ -8157,8 +8191,8 @@ cmd_backout(int argc, char *argv[])
 	}
 
 	memset(&upa, 0, sizeof(upa));
-	error = got_worktree_merge_files(worktree, commit_id, pid->id, repo,
-	    update_progress, &upa, check_cancelled, NULL);
+	error = got_worktree_merge_files(worktree, commit_id, pid->id,
+	    repo, update_progress, &upa, check_cancelled, NULL);
 	if (error != NULL)
 		goto done;
 
@@ -8737,8 +8771,8 @@ cmd_rebase(int argc, char *argv[])
 	struct got_object_id *branch_head_commit_id = NULL, *yca_id = NULL;
 	struct got_commit_object *commit = NULL;
 	int ch, rebase_in_progress = 0, abort_rebase = 0, continue_rebase = 0;
-	int histedit_in_progress = 0, create_backup = 1, list_backups = 0;
-	int delete_backups = 0;
+	int histedit_in_progress = 0, merge_in_progress = 0;
+	int create_backup = 1, list_backups = 0, delete_backups = 0;
 	unsigned char rebase_status = GOT_STATUS_NO_CHANGE;
 	struct got_object_id_queue commits;
 	struct got_pathlist_head merged_paths;
@@ -8848,6 +8882,15 @@ cmd_rebase(int argc, char *argv[])
 		goto done;
 	}
 
+	error = got_worktree_merge_in_progress(&merge_in_progress,
+	    worktree, repo);
+	if (error)
+		goto done;
+	if (merge_in_progress) {
+		error = got_error(GOT_ERR_MERGE_BUSY);
+		goto done;
+	}
+
 	error = got_worktree_rebase_in_progress(&rebase_in_progress, worktree);
 	if (error)
 		goto done;
@@ -9901,7 +9944,7 @@ cmd_histedit(int argc, char *argv[])
 	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 ch, rebase_in_progress = 0, merge_in_progress = 0;
 	struct got_update_progress_arg upa;
 	int edit_in_progress = 0, abort_edit = 0, continue_edit = 0;
 	int edit_logmsg_only = 0, fold_only = 0;
@@ -10062,6 +10105,15 @@ cmd_histedit(int argc, char *argv[])
 		goto done;
 	}
 
+	error = got_worktree_merge_in_progress(&merge_in_progress, worktree,
+	    repo);
+	if (error)
+		goto done;
+	if (merge_in_progress) {
+		error = got_error(GOT_ERR_MERGE_BUSY);
+		goto done;
+	}
+
 	error = got_worktree_histedit_in_progress(&edit_in_progress, worktree);
 	if (error)
 		goto done;
@@ -10456,6 +10508,10 @@ cmd_integrate(int argc, char *argv[])
 	if (error)
 		goto done;
 
+	error = check_merge_in_progress(worktree, repo);
+	if (error)
+		goto done;
+
 	if (asprintf(&refname, "refs/heads/%s", branch_arg) == -1) {
 		error = got_error_from_errno("asprintf");
 		goto done;
@@ -10532,6 +10588,256 @@ done:
 }
 
 __dead static void
+usage_merge(void)
+{
+	fprintf(stderr, "usage: %s merge [-a] [-c] [branch]\n",
+	    getprogname());
+	exit(1);
+}
+
+static const struct got_error *
+cmd_merge(int argc, char *argv[])
+{
+	const struct got_error *error = NULL;
+	struct got_worktree *worktree = NULL;
+	struct got_repository *repo = NULL;
+	struct got_fileindex *fileindex = NULL;
+	char *cwd = NULL, *id_str = NULL, *author = NULL;
+	struct got_reference *branch = NULL, *wt_branch = NULL;
+	struct got_object_id *branch_tip = NULL, *yca_id = NULL;
+	struct got_object_id *wt_branch_tip = NULL;
+	int ch, merge_in_progress = 0, abort_merge = 0, continue_merge = 0;
+	struct got_update_progress_arg upa;
+	struct got_object_id *merge_commit_id = NULL;
+	char *branch_name = NULL;
+
+	memset(&upa, 0, sizeof(upa));
+
+	while ((ch = getopt(argc, argv, "ac")) != -1) {
+		switch (ch) {
+		case 'a':
+			abort_merge = 1;
+			break;
+		case 'c':
+			continue_merge = 1;
+			break;
+		default:
+			usage_rebase();
+			/* 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_merge && continue_merge)
+		option_conflict('a', 'c');
+	if (abort_merge || continue_merge) {
+		if (argc != 0)
+			usage_merge();
+	} else if (argc != 1)
+		usage_merge();
+
+	cwd = getcwd(NULL, 0);
+	if (cwd == NULL) {
+		error = got_error_from_errno("getcwd");
+		goto done;
+	}
+
+	error = got_worktree_open(&worktree, cwd);
+	if (error) {
+		if (error->code == GOT_ERR_NOT_WORKTREE)
+			error = wrap_not_worktree_error(error,
+			    "merge", cwd);
+		goto done;
+	}
+
+	error = got_repo_open(&repo,
+	    worktree ? got_worktree_get_repo_path(worktree) : cwd, NULL);
+	if (error != NULL)
+		goto done;
+
+	error = apply_unveil(got_repo_get_path(repo), 0,
+	    worktree ? got_worktree_get_root_path(worktree) : NULL);
+	if (error)
+		goto done;
+
+	error = check_rebase_or_histedit_in_progress(worktree);
+	if (error)
+		goto done;
+
+	error = got_worktree_merge_in_progress(&merge_in_progress, worktree,
+	    repo);
+	if (error)
+		goto done;
+
+	if (abort_merge) {
+		if (!merge_in_progress) {
+			error = got_error(GOT_ERR_NOT_MERGING);
+			goto done;
+		}
+		error = got_worktree_merge_continue(&branch_name,
+		    &branch_tip, &fileindex, worktree, repo);
+		if (error)
+			goto done;
+		error = got_worktree_merge_abort(worktree, fileindex, repo,
+		    update_progress, &upa);
+		if (error)
+			goto done;
+		printf("Merge of %s aborted\n", branch_name);
+		goto done; /* nothing else to do */
+	}
+
+	error = get_author(&author, repo, worktree);
+	if (error)
+		goto done;
+
+	if (continue_merge) {
+		if (!merge_in_progress) {
+			error = got_error(GOT_ERR_NOT_MERGING);
+			goto done;
+		}
+		error = got_worktree_merge_continue(&branch_name,
+		    &branch_tip, &fileindex, worktree, repo);
+		if (error)
+			goto done;
+	} else {
+		error = got_ref_open(&branch, repo, argv[0], 0);
+		if (error != NULL)
+			goto done;
+		branch_name = strdup(got_ref_get_name(branch));
+		if (branch_name == NULL) {
+			error = got_error_from_errno("strdup");
+			goto done;
+		}
+		error = got_ref_resolve(&branch_tip, repo, branch);
+		if (error)
+			goto done;
+	}
+
+	error = got_ref_open(&wt_branch, repo,
+	    got_worktree_get_head_ref_name(worktree), 0);
+	if (error)
+		goto done;
+	error = got_ref_resolve(&wt_branch_tip, repo, wt_branch);
+	if (error)
+		goto done;
+	error = got_commit_graph_find_youngest_common_ancestor(&yca_id,
+	    wt_branch_tip, branch_tip, repo,
+	    check_cancelled, NULL);
+	if (error)
+		goto done;
+	if (yca_id == NULL) {
+		error = got_error_msg(GOT_ERR_ANCESTRY,
+		    "specified branch shares no common ancestry "
+		    "with work tree's branch");
+		goto done;
+	}
+
+	if (!continue_merge) {
+		error = check_path_prefix(wt_branch_tip, branch_tip,
+		    got_worktree_get_path_prefix(worktree),
+		    GOT_ERR_MERGE_PATH, repo);
+		if (error)
+			goto done;
+		error = check_same_branch(wt_branch_tip, branch,
+		    yca_id, repo);
+		if (error) {
+			if (error->code != GOT_ERR_ANCESTRY)
+				goto done;
+			error = NULL;
+		} else {
+			static char msg[512];
+			snprintf(msg, sizeof(msg),
+			    "cannot create a merge commit because "
+			    "%s is based on %s; %s can be integrated "
+			    "with 'got integrate' instead", branch_name,
+			    got_worktree_get_head_ref_name(worktree),
+			    branch_name);
+			error = got_error_msg(GOT_ERR_SAME_BRANCH, msg);
+			goto done;
+		}
+		error = got_worktree_merge_prepare(&fileindex, worktree,
+		    branch, repo);
+		if (error)
+			goto done;
+
+		error = got_worktree_merge_branch(worktree, fileindex,
+		    yca_id, branch_tip, repo, update_progress, &upa,
+		    check_cancelled, NULL);
+		if (error)
+			goto done;
+		print_update_progress_stats(&upa);
+	}
+
+	if (upa.conflicts > 0 || upa.obstructed > 0 || upa.missing > 0) {
+		error = got_worktree_merge_postpone(worktree, fileindex);
+		if (error)
+			goto done;
+		if (upa.conflicts > 0 &&
+		    upa.obstructed == 0 && upa.missing == 0) {
+			error = got_error_msg(GOT_ERR_CONFLICTS,
+			    "conflicts must be resolved before merging "
+			    "can continue");
+		} else if (upa.conflicts > 0) {
+			error = got_error_msg(GOT_ERR_CONFLICTS,
+			    "conflicts must be resolved before merging "
+			    "can continue; changes destined for missing "
+			    "or obstructed files were not yet merged and "
+			    "should be merged manually if required before "
+			    "merge operation is continued");
+		} else {
+			error = got_error_msg(GOT_ERR_CONFLICTS,
+			    "changes destined for missing or obstructed "
+			    "files were not yet merged and should be "
+			    "merged manually if required before the "
+			    "merge operation is continued");
+		}
+		goto done;
+	} else {
+		error = got_worktree_merge_commit(&merge_commit_id, worktree,
+		    fileindex, author, NULL, 1, branch_tip, branch_name, repo);
+		if (error)
+			goto done;
+		error = got_worktree_merge_complete(worktree, fileindex, repo);
+		if (error)
+			goto done;
+		error = got_object_id_str(&id_str, merge_commit_id);
+		if (error)
+			goto done;
+		printf("Merged %s into %s: %s\n", branch_name,
+		    got_worktree_get_head_ref_name(worktree),
+		    id_str);
+
+	}
+done:
+	free(id_str);
+	free(merge_commit_id);
+	free(author);
+	free(branch_tip);
+	free(branch_name);
+	free(yca_id);
+	if (branch)
+		got_ref_close(branch);
+	if (wt_branch)
+		got_ref_close(wt_branch);
+	if (worktree)
+		got_worktree_close(worktree);
+	if (repo) {
+		const struct got_error *close_err = got_repo_close(repo);
+		if (error == NULL)
+			error = close_err;
+	}
+	return error;
+}
+
+__dead static void
 usage_stage(void)
 {
 	fprintf(stderr, "usage: %s stage [-l] | [-p] [-F response-script] "
@@ -10647,6 +10953,10 @@ cmd_stage(int argc, char *argv[])
 	if (error)
 		goto done;
 
+	error = check_merge_in_progress(worktree, repo);
+	if (error)
+		goto done;
+
 	error = get_worktree_paths_from_argv(&paths, argc, argv, worktree);
 	if (error)
 		goto done;
blob - 5eb5cc5ebde432af823128fc7227bf48b866369a
blob + f87c06cad9b95c07cc3e6ce8f0e0f733de70a1a6
--- include/got_error.h
+++ include/got_error.h
@@ -155,6 +155,12 @@
 #define GOT_ERR_CAPA_DELETE_REFS 138
 #define GOT_ERR_SEND_DELETE_REF	139
 #define GOT_ERR_SEND_TAG_EXISTS	140
+#define GOT_ERR_NOT_MERGING	141
+#define GOT_ERR_MERGE_OUT_OF_DATE 142
+#define GOT_ERR_MERGE_STAGED_PATHS 143
+#define GOT_ERR_MERGE_COMMIT_OUT_OF_DATE 143
+#define GOT_ERR_MERGE_BUSY	144
+#define GOT_ERR_MERGE_PATH	145
 
 static const struct got_error {
 	int code;
@@ -318,6 +324,19 @@ static const struct got_error {
 	{ GOT_ERR_CAPA_DELETE_REFS, "server cannot delete references" },
 	{ GOT_ERR_SEND_DELETE_REF, "reference cannot be deleted" },
 	{ GOT_ERR_SEND_TAG_EXISTS, "tag already exists on server" },
+	{ GOT_ERR_NOT_MERGING,	"merge operation not in progress" },
+	{ GOT_ERR_MERGE_OUT_OF_DATE, "work tree must be updated before it "
+	    "can be used to merge a branch" },
+	{ GOT_ERR_MERGE_STAGED_PATHS, "work tree contains files with staged "
+	    "changes; these changes must be unstaged before merging can "
+	    "proceed" },
+	{ GOT_ERR_MERGE_COMMIT_OUT_OF_DATE, "merging cannot proceed because "
+	    "the work tree is no longer up-to-date; merge must be aborted "
+	    "and retried" },
+	{ GOT_ERR_MERGE_BUSY,"a merge operation is in progress in this "
+	    "work tree and must be continued or aborted first" },
+	{ GOT_ERR_MERGE_PATH,	"cannot merge branch which contains "
+	    "changes outside of this work tree's path prefix" },
 };
 
 /*
blob - d9ae4873db1e494d8663703aea78cfee34d5f440
blob + f1f2919be24717377a3190cb6d410224cd9118be
--- include/got_worktree.h
+++ include/got_worktree.h
@@ -437,7 +437,74 @@ const struct got_error *got_worktree_integrate_abort(s
     struct got_fileindex *, struct got_repository *,
     struct got_reference *, struct got_reference *);
 
+/* Postpone the merge operation. Should be called after a merge conflict. */
+const struct got_error *got_worktree_merge_postpone(struct got_worktree *,
+    struct got_fileindex *);
+
+/* Merge changes from the merge source branch into the worktree. */
+const struct got_error *
+got_worktree_merge_branch(struct got_worktree *worktree,
+    struct got_fileindex *fileindex,
+    struct got_object_id *yca_commit_id,
+    struct got_object_id *branch_tip,
+    struct got_repository *repo, got_worktree_checkout_cb progress_cb,
+    void *progress_arg, got_cancel_cb cancel_cb, void *cancel_arg);
+
+/* Attempt to commit merged changes. */
+const struct got_error *
+got_worktree_merge_commit(struct got_object_id **new_commit_id,
+    struct got_worktree *worktree, struct got_fileindex *fileindex,
+    const char *author, const char *committer, int allow_bad_symlinks,
+    struct got_object_id *branch_tip, const char *branch_name,
+    struct got_repository *repo);
+
 /*
+ * Complete the merge operation.
+ * This should be called once changes have been successfully committed.
+ */
+const struct got_error *got_worktree_merge_complete(
+    struct got_worktree *worktree, struct got_fileindex *fileindex,
+    struct got_repository *repo);
+
+/* Check whether a merge operation is in progress. */
+const struct got_error *got_worktree_merge_in_progress(int *,
+    struct got_worktree *, struct got_repository *);
+
+/*
+ * Prepare for merging a branch into the work tree's current branch.
+ * This function creates a reference to the branch being merged, and
+ * this branches current tip commit in the "got/worktree/merge/"
+ * namespace. These references are used to keep track of merge operation
+ * state and are used as input and/or output arguments with other
+ * merge-related functions.
+ * The function also returns a pointer to a fileindex which must be
+ * passed back to other merge-related functions.
+ */
+const struct got_error *got_worktree_merge_prepare(struct got_fileindex **,
+    struct got_worktree *, struct got_reference *, struct got_repository *);
+
+/*
+ * Continue an interrupted merge operation.
+ * This function returns name of the branch being merged, and the ID of the
+ * tip commit being merged.
+ * This function should be called before either resuming or aborting a
+ * merge operation.
+ * The function also returns a pointer to a fileindex which must be
+ * passed back to other merge-related functions.
+ */
+const struct got_error *got_worktree_merge_continue(char **,
+    struct got_object_id **, struct got_fileindex **,
+    struct got_worktree *, struct got_repository *);
+
+/*
+ * Abort the current rebase operation.
+ * Report reverted files via the specified progress callback.
+ */
+const struct got_error *got_worktree_merge_abort(struct got_worktree *,
+    struct got_fileindex *, struct got_repository *,
+    got_worktree_checkout_cb, void *);
+
+/*
  * Stage the specified paths for commit.
  * If the patch callback is not NULL, call it to select patch hunks for
  * staging. Otherwise, stage the full file content found at each path.
blob - 651bcc8f757194fa0d5259093255234ac99ee09d
blob + a35884552cf66d678f0db623e1dd92d976a7014e
--- lib/got_lib_worktree.h
+++ lib/got_lib_worktree.h
@@ -102,3 +102,9 @@ const struct got_error *got_worktree_get_base_ref_name
 /* Reference pointing at the ID of the current commit being edited. */
 #define GOT_WORKTREE_HISTEDIT_COMMIT_REF_PREFIX \
 	"refs/got/worktree/histedit/commit"
+
+/* Symbolic reference pointing at the name of the merge source branch. */
+#define GOT_WORKTREE_MERGE_BRANCH_REF_PREFIX "refs/got/worktree/merge/branch"
+
+/* Reference pointing at the ID of the merge source branches's tip commit. */
+#define GOT_WORKTREE_MERGE_COMMIT_REF_PREFIX "refs/got/worktree/merge/commit"
blob - 70dc0d95f90464c02d691b2e2157c818079650c3
blob + d2c6246c748ba52478619a448ec02bf61c9e9729
--- lib/object_create.c
+++ lib/object_create.c
@@ -490,11 +490,11 @@ got_object_commit_create(struct got_object_id **id,
 	}
 
 	if (parent_ids) {
+		free(id_str);
+		id_str = NULL;
 		STAILQ_FOREACH(qid, parent_ids, entry) {
 			char *parent_str = NULL;
 
-			free(id_str);
-
 			err = got_object_id_str(&id_str, qid->id);
 			if (err)
 				goto done;
@@ -512,6 +512,8 @@ got_object_commit_create(struct got_object_id **id,
 				goto done;
 			}
 			free(parent_str);
+			free(id_str);
+			id_str = NULL;
 		}
 	}
 
@@ -568,6 +570,7 @@ got_object_commit_create(struct got_object_id **id,
 
 	err = create_object_file(*id, commitfile, repo);
 done:
+	free(id_str);
 	free(msg0);
 	free(header);
 	free(tree_str);
blob - e8baa2aceccca3befe0c125fd5902bb082f1315f
blob + 84bdbf9300ff796c07ee1859aee637708b5b35c2
--- lib/worktree.c
+++ lib/worktree.c
@@ -2380,6 +2380,20 @@ got_worktree_get_histedit_script_path(char **path,
 	return NULL;
 }
 
+static const struct got_error *
+get_merge_branch_ref_name(char **refname, struct got_worktree *worktree)
+{
+	return get_ref_name(refname, worktree,
+	    GOT_WORKTREE_MERGE_BRANCH_REF_PREFIX);
+}
+
+static const struct got_error *
+get_merge_commit_ref_name(char **refname, struct got_worktree *worktree)
+{
+	return get_ref_name(refname, worktree,
+	    GOT_WORKTREE_MERGE_COMMIT_REF_PREFIX);
+}
+
 /*
  * Prevent Git's garbage collector from deleting our base commit by
  * setting a reference to our base commit's ID.
@@ -3277,7 +3291,8 @@ got_worktree_merge_files(struct got_worktree *worktree
 		goto done;
 
 	err = merge_files(worktree, fileindex, fileindex_path, commit_id1,
-	    commit_id2, repo, progress_cb, progress_arg, cancel_cb, cancel_arg);
+	    commit_id2, repo, progress_cb, progress_arg,
+	    cancel_cb, cancel_arg);
 done:
 	if (fileindex)
 		got_fileindex_free(fileindex);
@@ -4411,6 +4426,7 @@ struct revert_file_args {
 	got_worktree_patch_cb patch_cb;
 	void *patch_arg;
 	struct got_repository *repo;
+	int unlink_added_files;
 };
 
 static const struct got_error *
@@ -4691,6 +4707,19 @@ revert_file(void *arg, unsigned char status, unsigned 
 		if (err)
 			goto done;
 		got_fileindex_entry_remove(a->fileindex, ie);
+		if (a->unlink_added_files) {
+			if (asprintf(&ondisk_path, "%s/%s",
+			    got_worktree_get_root_path(a->worktree),
+			    relpath) == -1) {
+				err = got_error_from_errno("asprintf");
+				goto done;
+			}
+			if (unlink(ondisk_path) == -1) {
+				err = got_error_from_errno2("unlink",
+				    ondisk_path);
+				break;
+			}
+		}
 		break;
 	case GOT_STATUS_DELETE:
 		if (a->patch_cb) {
@@ -4828,6 +4857,7 @@ got_worktree_revert(struct got_worktree *worktree,
 	rfa.patch_cb = patch_cb;
 	rfa.patch_arg = patch_arg;
 	rfa.repo = repo;
+	rfa.unlink_added_files = 0;
 	TAILQ_FOREACH(pe, paths, entry) {
 		err = worktree_status(worktree, pe->path, fileindex, repo,
 		    revert_file, &rfa, NULL, NULL, 0, 0);
@@ -5583,7 +5613,9 @@ done:
 const struct got_error *
 commit_worktree(struct got_object_id **new_commit_id,
     struct got_pathlist_head *commitable_paths,
-    struct got_object_id *head_commit_id, struct got_worktree *worktree,
+    struct got_object_id *head_commit_id,
+    struct got_object_id *parent_id2,
+    struct got_worktree *worktree,
     const char *author, const char *committer,
     got_worktree_commit_msg_cb commit_msg_cb, void *commit_arg,
     got_worktree_status_cb status_cb, void *status_arg,
@@ -5597,7 +5629,7 @@ commit_worktree(struct got_object_id **new_commit_id,
 	struct got_object_id *head_commit_id2 = NULL;
 	struct got_tree_object *head_tree = NULL;
 	struct got_object_id *new_tree_id = NULL;
-	int nentries;
+	int nentries, nparents = 0;
 	struct got_object_id_queue parent_ids;
 	struct got_object_qid *pid = NULL;
 	char *logmsg = NULL;
@@ -5661,9 +5693,16 @@ commit_worktree(struct got_object_id **new_commit_id,
 	if (err)
 		goto done;
 	STAILQ_INSERT_TAIL(&parent_ids, pid, entry);
+	nparents++;
+	if (parent_id2) {
+		err = got_object_qid_alloc(&pid, parent_id2);
+		if (err)
+			goto done;
+		STAILQ_INSERT_TAIL(&parent_ids, pid, entry);
+		nparents++;
+	}
 	err = got_object_commit_create(new_commit_id, new_tree_id, &parent_ids,
-	    1, author, time(NULL), committer, time(NULL), logmsg, repo);
-	got_object_qid_free(pid);
+	    nparents, author, time(NULL), committer, time(NULL), logmsg, repo);
 	if (logmsg != NULL)
 		free(logmsg);
 	if (err)
@@ -5702,6 +5741,7 @@ commit_worktree(struct got_object_id **new_commit_id,
 	if (err)
 		goto done;
 done:
+	got_object_id_queue_free(&parent_ids);
 	if (head_tree)
 		got_object_tree_close(head_tree);
 	if (head_commit)
@@ -5862,7 +5902,7 @@ got_worktree_commit(struct got_object_id **new_commit_
 	}
 
 	err = commit_worktree(new_commit_id, &commitable_paths,
-	    head_commit_id, worktree, author, committer,
+	    head_commit_id, NULL, worktree, author, committer,
 	    commit_msg_cb, commit_arg, status_cb, status_arg, repo);
 	if (err)
 		goto done;
@@ -6444,7 +6484,7 @@ rebase_commit(struct got_object_id **new_commit_id,
 
 	/* NB: commit_worktree will call free(logmsg) */
 	err = commit_worktree(new_commit_id, &commitable_paths, head_commit_id,
-	    worktree, got_object_commit_get_author(orig_commit),
+	    NULL, worktree, got_object_commit_get_author(orig_commit),
 	    got_object_commit_get_committer(orig_commit),
 	    collect_rebase_commit_msg, logmsg, rebase_status, NULL, repo);
 	if (err)
@@ -6768,6 +6808,7 @@ got_worktree_rebase_abort(struct got_worktree *worktre
 	rfa.patch_cb = NULL;
 	rfa.patch_arg = NULL;
 	rfa.repo = repo;
+	rfa.unlink_added_files = 0;
 	err = worktree_status(worktree, "", fileindex, repo,
 	    revert_file, &rfa, NULL, NULL, 0, 0);
 	if (err)
@@ -7121,6 +7162,7 @@ got_worktree_histedit_abort(struct got_worktree *workt
 	rfa.patch_cb = NULL;
 	rfa.patch_arg = NULL;
 	rfa.repo = repo;
+	rfa.unlink_added_files = 0;
 	err = worktree_status(worktree, "", fileindex, repo,
 	    revert_file, &rfa, NULL, NULL, 0, 0);
 	if (err)
@@ -7375,6 +7417,489 @@ got_worktree_integrate_abort(struct got_worktree *work
 	return err;
 }
 
+const struct got_error *
+got_worktree_merge_postpone(struct got_worktree *worktree,
+    struct got_fileindex *fileindex)
+{
+	const struct got_error *err, *sync_err;
+	char *fileindex_path = NULL;
+
+	err = get_fileindex_path(&fileindex_path, worktree);
+	if (err)
+		goto done;
+
+	sync_err = sync_fileindex(fileindex, fileindex_path);
+
+	err = lock_worktree(worktree, LOCK_SH);
+	if (sync_err && err == NULL)
+		err = sync_err;
+done:
+	got_fileindex_free(fileindex);
+	free(fileindex_path);
+	return err;
+}
+
+static const struct got_error *
+delete_merge_refs(struct got_worktree *worktree, struct got_repository *repo)
+{
+	const struct got_error *err;
+	char *branch_refname = NULL, *commit_refname = NULL;
+
+	err = get_merge_branch_ref_name(&branch_refname, worktree);
+	if (err)
+		goto done;
+	err = delete_ref(branch_refname, repo);
+	if (err)
+		goto done;
+
+	err = get_merge_commit_ref_name(&commit_refname, worktree);
+	if (err)
+		goto done;
+	err = delete_ref(commit_refname, repo);
+	if (err)
+		goto done;
+
+done:
+	free(branch_refname);
+	free(commit_refname);
+	return err;
+}
+
+struct merge_commit_msg_arg {
+	struct got_worktree *worktree;
+	const char *branch_name;
+};
+
+static const struct got_error *
+merge_commit_msg_cb(struct got_pathlist_head *commitable_paths, char **logmsg,
+    void *arg)
+{
+	struct merge_commit_msg_arg *a = arg;
+
+	if (asprintf(logmsg, "merge %s into %s\n", a->branch_name,
+	    got_worktree_get_head_ref_name(a->worktree)) == -1)
+		return got_error_from_errno("asprintf");
+
+	return NULL;
+}
+
+static const struct got_error *
+merge_status_cb(void *arg, unsigned char status, unsigned char staged_status,
+    const char *path, struct got_object_id *blob_id,
+    struct got_object_id *staged_blob_id, struct got_object_id *commit_id,
+    int dirfd, const char *de_name)
+{
+	return NULL;
+}
+
+const struct got_error *
+got_worktree_merge_branch(struct got_worktree *worktree,
+    struct got_fileindex *fileindex,
+    struct got_object_id *yca_commit_id,
+    struct got_object_id *branch_tip,
+    struct got_repository *repo, got_worktree_checkout_cb progress_cb,
+    void *progress_arg, got_cancel_cb cancel_cb, void *cancel_arg)
+{
+	const struct got_error *err;
+	char *fileindex_path = NULL;
+
+	err = get_fileindex_path(&fileindex_path, worktree);
+	if (err)
+		goto done;
+
+	err = got_fileindex_for_each_entry_safe(fileindex, check_mixed_commits,
+	    worktree);
+	if (err)
+		goto done;
+
+	err = merge_files(worktree, fileindex, fileindex_path, yca_commit_id,
+	    branch_tip, repo, progress_cb, progress_arg,
+	    cancel_cb, cancel_arg);
+done:
+	free(fileindex_path);
+	return err;
+}
+
+const struct got_error *
+got_worktree_merge_commit(struct got_object_id **new_commit_id,
+    struct got_worktree *worktree, struct got_fileindex *fileindex,
+    const char *author, const char *committer, int allow_bad_symlinks,
+    struct got_object_id *branch_tip, const char *branch_name,
+    struct got_repository *repo)
+{
+	const struct got_error *err = NULL, *sync_err;
+	struct got_pathlist_head commitable_paths;
+	struct collect_commitables_arg cc_arg;
+	struct got_pathlist_entry *pe;
+	struct got_reference *head_ref = NULL;
+	struct got_object_id *head_commit_id = NULL;
+	int have_staged_files = 0;
+	struct merge_commit_msg_arg mcm_arg;
+	char *fileindex_path = NULL;
+
+	*new_commit_id = NULL;
+
+	TAILQ_INIT(&commitable_paths);
+
+	err = get_fileindex_path(&fileindex_path, worktree);
+	if (err)
+		goto done;
+
+	err = got_ref_open(&head_ref, repo, worktree->head_ref_name, 0);
+	if (err)
+		goto done;
+
+	err = got_ref_resolve(&head_commit_id, repo, head_ref);
+	if (err)
+		goto done;
+
+	err = got_fileindex_for_each_entry_safe(fileindex, check_staged_file,
+	    &have_staged_files);
+	if (err && err->code != GOT_ERR_CANCELLED)
+		goto done;
+	if (have_staged_files) {
+		err = got_error(GOT_ERR_MERGE_STAGED_PATHS);
+		goto done;
+	}
+
+	cc_arg.commitable_paths = &commitable_paths;
+	cc_arg.worktree = worktree;
+	cc_arg.fileindex = fileindex;
+	cc_arg.repo = repo;
+	cc_arg.have_staged_files = have_staged_files;
+	cc_arg.allow_bad_symlinks = allow_bad_symlinks;
+	err = worktree_status(worktree, "", fileindex, repo,
+	    collect_commitables, &cc_arg, NULL, NULL, 0, 0);
+	if (err)
+		goto done;
+
+	if (TAILQ_EMPTY(&commitable_paths)) {
+		err = got_error_fmt(GOT_ERR_COMMIT_NO_CHANGES,
+		    "merge of %s cannot proceed", branch_name);
+		goto done;
+	}
+
+	TAILQ_FOREACH(pe, &commitable_paths, entry) {
+		struct got_commitable *ct = pe->data;
+		const char *ct_path = ct->in_repo_path;
+
+		while (ct_path[0] == '/')
+			ct_path++;
+		err = check_out_of_date(ct_path, ct->status,
+		    ct->staged_status, ct->base_blob_id, ct->base_commit_id,
+		    head_commit_id, repo, GOT_ERR_MERGE_COMMIT_OUT_OF_DATE);
+		if (err)
+			goto done;
+
+	}
+
+	mcm_arg.worktree = worktree;
+	mcm_arg.branch_name = branch_name;
+	err = commit_worktree(new_commit_id, &commitable_paths,
+	    head_commit_id, branch_tip, worktree, author, committer,
+	    merge_commit_msg_cb, &mcm_arg, merge_status_cb, NULL, repo);
+	if (err)
+		goto done;
+
+	err = update_fileindex_after_commit(worktree, &commitable_paths,
+	    *new_commit_id, fileindex, have_staged_files);
+	sync_err = sync_fileindex(fileindex, fileindex_path);
+	if (sync_err && err == NULL)
+		err = sync_err;
+done:
+	TAILQ_FOREACH(pe, &commitable_paths, entry) {
+		struct got_commitable *ct = pe->data;
+		free_commitable(ct);
+	}
+	got_pathlist_free(&commitable_paths);
+	free(fileindex_path);
+	return err;
+}
+
+const struct got_error *
+got_worktree_merge_complete(struct got_worktree *worktree,
+    struct got_fileindex *fileindex, struct got_repository *repo)
+{
+	const struct got_error *err, *unlockerr, *sync_err;
+	char *fileindex_path = NULL;
+
+	err = delete_merge_refs(worktree, repo);
+	if (err)
+		goto done;
+
+	err = get_fileindex_path(&fileindex_path, worktree);
+	if (err)
+		goto done;
+	err = bump_base_commit_id_everywhere(worktree, fileindex, NULL, NULL);
+	sync_err = sync_fileindex(fileindex, fileindex_path);
+	if (sync_err && err == NULL)
+		err = sync_err;
+done:
+	got_fileindex_free(fileindex);
+	free(fileindex_path);
+	unlockerr = lock_worktree(worktree, LOCK_SH);
+	if (unlockerr && err == NULL)
+		err = unlockerr;
+	return err;
+}
+
+const struct got_error *
+got_worktree_merge_in_progress(int *in_progress, struct got_worktree *worktree,
+    struct got_repository *repo)
+{
+	const struct got_error *err;
+	char *branch_refname = NULL;
+	struct got_reference *branch_ref = NULL;
+
+	*in_progress = 0;
+
+	err = get_merge_branch_ref_name(&branch_refname, worktree);
+	if (err)
+		return err;
+	err = got_ref_open(&branch_ref, repo, branch_refname, 0);
+	if (err) {
+		if (err->code != GOT_ERR_NOT_REF)
+			return err;
+	} else
+		*in_progress = 1;
+
+	return NULL;
+}
+
+const struct got_error *got_worktree_merge_prepare(
+    struct got_fileindex **fileindex, struct got_worktree *worktree,
+    struct got_reference *branch, struct got_repository *repo)
+{
+	const struct got_error *err = NULL;
+	char *fileindex_path = NULL;
+	char *branch_refname = NULL, *commit_refname = NULL;
+	struct got_reference *wt_branch = NULL, *branch_ref = NULL;
+	struct got_reference *commit_ref = NULL;
+	struct got_object_id *branch_tip = NULL, *wt_branch_tip = NULL;
+	struct check_rebase_ok_arg ok_arg;
+
+	*fileindex = NULL;
+
+	err = lock_worktree(worktree, LOCK_EX);
+	if (err)
+		return err;
+
+	err = open_fileindex(fileindex, &fileindex_path, worktree);
+	if (err)
+		goto done;
+
+	/* Preconditions are the same as for rebase. */
+	ok_arg.worktree = worktree;
+	ok_arg.repo = repo;
+	err = got_fileindex_for_each_entry_safe(*fileindex, check_rebase_ok,
+	    &ok_arg);
+	if (err)
+		goto done;
+
+	err = get_merge_branch_ref_name(&branch_refname, worktree);
+	if (err)
+		return err;
+
+	err = get_merge_commit_ref_name(&commit_refname, worktree);
+	if (err)
+		return err;
+
+	err = got_ref_open(&wt_branch, repo, worktree->head_ref_name,
+	    0);
+	if (err)
+		goto done;
+
+	err = got_ref_resolve(&wt_branch_tip, repo, wt_branch);
+	if (err)
+		goto done;
+
+	if (got_object_id_cmp(worktree->base_commit_id, wt_branch_tip) != 0) {
+		err = got_error(GOT_ERR_MERGE_OUT_OF_DATE);
+		goto done;
+	}
+
+	err = got_ref_resolve(&branch_tip, repo, branch);
+	if (err)
+		goto done;
+
+	err = got_ref_alloc_symref(&branch_ref, branch_refname, branch);
+	if (err)
+		goto done;
+	err = got_ref_write(branch_ref, repo);
+	if (err)
+		goto done;
+
+	err = got_ref_alloc(&commit_ref, commit_refname, branch_tip);
+	if (err)
+		goto done;
+	err = got_ref_write(commit_ref, repo);
+	if (err)
+		goto done;
+
+done:
+	free(branch_refname);
+	free(commit_refname);
+	free(fileindex_path);
+	if (branch_ref)
+		got_ref_close(branch_ref);
+	if (commit_ref)
+		got_ref_close(commit_ref);
+	if (wt_branch)
+		got_ref_close(wt_branch);
+	free(wt_branch_tip);
+	if (err) {
+		if (*fileindex) {
+			got_fileindex_free(*fileindex);
+			*fileindex = NULL;
+		}
+		lock_worktree(worktree, LOCK_SH);
+	}
+	return err;
+}
+
+const struct got_error *
+got_worktree_merge_continue(char **branch_name,
+    struct got_object_id **branch_tip, struct got_fileindex **fileindex,
+    struct got_worktree *worktree, struct got_repository *repo)
+{
+	const struct got_error *err;
+	char *commit_refname = NULL, *branch_refname = NULL;
+	struct got_reference *commit_ref = NULL, *branch_ref = NULL;
+	char *fileindex_path = NULL;
+	int have_staged_files = 0;
+
+	*branch_name = NULL;
+	*branch_tip = NULL;
+	*fileindex = NULL;
+
+	err = lock_worktree(worktree, LOCK_EX);
+	if (err)
+		return err;
+
+	err = open_fileindex(fileindex, &fileindex_path, worktree);
+	if (err)
+		goto done;
+
+	err = got_fileindex_for_each_entry_safe(*fileindex, check_staged_file,
+	    &have_staged_files);
+	if (err && err->code != GOT_ERR_CANCELLED)
+		goto done;
+	if (have_staged_files) {
+		err = got_error(GOT_ERR_STAGED_PATHS);
+		goto done;
+	}
+
+	err = get_merge_branch_ref_name(&branch_refname, worktree);
+	if (err)
+		goto done;
+
+	err = get_merge_commit_ref_name(&commit_refname, worktree);
+	if (err)
+		goto done;
+
+	err = got_ref_open(&branch_ref, repo, branch_refname, 0);
+	if (err)
+		goto done;
+
+	if (!got_ref_is_symbolic(branch_ref)) {
+		err = got_error_fmt(GOT_ERR_BAD_REF_TYPE,
+		    "%s is not a symbolic reference",
+		    got_ref_get_name(branch_ref));
+		goto done;
+	}
+	*branch_name = strdup(got_ref_get_symref_target(branch_ref));
+	if (*branch_name == NULL) {
+		err = got_error_from_errno("strdup");
+		goto done;
+	}
+
+	err = got_ref_open(&commit_ref, repo, commit_refname, 0);
+	if (err)
+		goto done;
+
+	err = got_ref_resolve(branch_tip, repo, commit_ref);
+	if (err)
+		goto done;
+done:
+	free(commit_refname);
+	free(branch_refname);
+	free(fileindex_path);
+	if (commit_ref)
+		got_ref_close(commit_ref);
+	if (branch_ref)
+		got_ref_close(branch_ref);
+	if (err) {
+		if (*branch_name) {
+			free(*branch_name);
+			*branch_name = NULL;
+		}
+		free(*branch_tip);
+		*branch_tip = NULL;
+		if (*fileindex) {
+			got_fileindex_free(*fileindex);
+			*fileindex = NULL;
+		}
+		lock_worktree(worktree, LOCK_SH);
+	}
+	return err;
+}
+
+const struct got_error *
+got_worktree_merge_abort(struct got_worktree *worktree,
+    struct got_fileindex *fileindex, struct got_repository *repo,
+    got_worktree_checkout_cb progress_cb, void *progress_arg)
+{
+	const struct got_error *err, *unlockerr, *sync_err;
+	struct got_object_id *commit_id = NULL;
+	char *fileindex_path = NULL;
+	struct revert_file_args rfa;
+	struct got_object_id *tree_id = NULL;
+
+	err = got_object_id_by_path(&tree_id, repo,
+	    worktree->base_commit_id, worktree->path_prefix);
+	if (err)
+		goto done;
+
+	err = delete_merge_refs(worktree, repo);
+	if (err)
+		goto done;
+
+	err = get_fileindex_path(&fileindex_path, worktree);
+	if (err)
+		goto done;
+
+	rfa.worktree = worktree;
+	rfa.fileindex = fileindex;
+	rfa.progress_cb = progress_cb;
+	rfa.progress_arg = progress_arg;
+	rfa.patch_cb = NULL;
+	rfa.patch_arg = NULL;
+	rfa.repo = repo;
+	rfa.unlink_added_files = 1;
+	err = worktree_status(worktree, "", fileindex, repo,
+	    revert_file, &rfa, NULL, NULL, 0, 0);
+	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:
+	free(tree_id);
+	free(commit_id);
+	if (fileindex)
+		got_fileindex_free(fileindex);
+	free(fileindex_path);
+
+	unlockerr = lock_worktree(worktree, LOCK_SH);
+	if (unlockerr && err == NULL)
+		err = unlockerr;
+	return err;
+}
+
 struct check_stage_ok_arg {
 	struct got_object_id *head_commit_id;
 	struct got_worktree *worktree;
blob - /dev/null
blob + a23abe444913229eb71b51e04a842fa7ffb9b4a0 (mode 755)
--- /dev/null
+++ regress/cmdline/merge.sh
@@ -0,0 +1,979 @@
+#!/bin/sh
+#
+# Copyright (c) 2021 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
+
+test_merge_basic() {
+	local testroot=`test_init merge_basic`
+	local commit0=`git_show_head $testroot/repo`
+	local commit0_author_time=`git_show_author_time $testroot/repo`
+
+	(cd $testroot/repo && git checkout -q -b newbranch)
+	echo "modified delta on branch" > $testroot/repo/gamma/delta
+	git_commit $testroot/repo -m "committing to delta on newbranch"
+	local branch_commit0=`git_show_branch_head $testroot/repo newbranch`
+
+	echo "modified alpha on branch" > $testroot/repo/alpha
+	git_commit $testroot/repo -m "committing to alpha on newbranch"
+	local branch_commit1=`git_show_branch_head $testroot/repo newbranch`
+	(cd $testroot/repo && git rm -q beta)
+	git_commit $testroot/repo -m "removing beta on newbranch"
+	local branch_commit2=`git_show_branch_head $testroot/repo newbranch`
+	echo "new file on branch" > $testroot/repo/epsilon/new
+	(cd $testroot/repo && git add epsilon/new)
+	git_commit $testroot/repo -m "adding new file on newbranch"
+	local branch_commit3=`git_show_branch_head $testroot/repo newbranch`
+	(cd $testroot/repo && ln -s alpha symlink && git add symlink)
+	git_commit $testroot/repo -m "adding symlink on newbranch"
+	local branch_commit4=`git_show_branch_head $testroot/repo newbranch`
+
+	got checkout -b master $testroot/repo $testroot/wt > /dev/null
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		echo "got checkout failed unexpectedly" >&2
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# need a divergant commit on the main branch for 'got merge' 
+	(cd $testroot/wt && got merge newbranch \
+		> $testroot/stdout 2> $testroot/stderr)
+	ret="$?"
+	if [ "$ret" == "0" ]; then
+		echo "got merge succeeded unexpectedly" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+	echo -n "got: cannot create a merge commit because " \
+		> $testroot/stderr.expected
+	echo -n "refs/heads/newbranch is based on refs/heads/master; " \
+		>> $testroot/stderr.expected
+	echo -n "refs/heads/newbranch can be integrated with " \
+		>> $testroot/stderr.expected
+	echo "'got integrate' instead" >> $testroot/stderr.expected
+	cmp -s $testroot/stderr.expected $testroot/stderr
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stderr.expected $testroot/stderr
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# create the required dirvergant commit
+	(cd $testroot/repo && git checkout -q master)
+	echo "modified zeta on master" > $testroot/repo/epsilon/zeta
+	git_commit $testroot/repo -m "committing to zeta on master"
+	local master_commit=`git_show_head $testroot/repo`
+
+	# need an up-to-date work tree for 'got merge' 
+	(cd $testroot/wt && got merge newbranch \
+		> $testroot/stdout 2> $testroot/stderr)
+	ret="$?"
+	if [ "$ret" == "0" ]; then
+		echo "got merge succeeded unexpectedly" >&2
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+	echo -n "got: work tree must be updated before it can be used " \
+		> $testroot/stderr.expected
+	echo "to merge a branch" >> $testroot/stderr.expected
+	cmp -s $testroot/stderr.expected $testroot/stderr
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stderr.expected $testroot/stderr
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	(cd $testroot/wt && got update > /dev/null)
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		echo "got update failed unexpectedly" >&2
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# must not use a mixed-commit work tree with 'got merge' 
+	(cd $testroot/wt && got update -c $commit0 alpha > /dev/null)
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		echo "got update failed unexpectedly" >&2
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+	(cd $testroot/wt && got merge newbranch \
+		> $testroot/stdout 2> $testroot/stderr)
+	ret="$?"
+	if [ "$ret" == "0" ]; then
+		echo "got merge succeeded unexpectedly" >&2
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+	echo -n "got: work tree contains files from multiple base commits; " \
+		> $testroot/stderr.expected
+	echo "the entire work tree must be updated first" \
+		>> $testroot/stderr.expected
+	cmp -s $testroot/stderr.expected $testroot/stderr
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stderr.expected $testroot/stderr
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	(cd $testroot/wt && got update > /dev/null)
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		echo "got update failed unexpectedly" >&2
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# must not have staged files with 'got merge' 
+	echo "modified file alpha"  > $testroot/wt/alpha
+	(cd $testroot/wt && got stage alpha > /dev/null)
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		echo "got stage failed unexpectedly" >&2
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+	(cd $testroot/wt && got merge newbranch \
+		> $testroot/stdout 2> $testroot/stderr)
+	ret="$?"
+	if [ "$ret" == "0" ]; then
+		echo "got merge succeeded unexpectedly" >&2
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+	echo "got: alpha: file is staged" > $testroot/stderr.expected
+	cmp -s $testroot/stderr.expected $testroot/stderr
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stderr.expected $testroot/stderr
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+	(cd $testroot/wt && got unstage alpha > /dev/null)
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		echo "got unstage failed unexpectedly" >&2
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# must not have local changes with 'got merge' 
+	(cd $testroot/wt && got merge newbranch \
+		> $testroot/stdout 2> $testroot/stderr)
+	ret="$?"
+	if [ "$ret" == "0" ]; then
+		echo "got merge succeeded unexpectedly" >&2
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+	echo -n "got: work tree contains local changes; " \
+		> $testroot/stderr.expected
+	echo "these changes must be committed or reverted first" \
+		>> $testroot/stderr.expected
+	cmp -s $testroot/stderr.expected $testroot/stderr
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stderr.expected $testroot/stderr
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	(cd $testroot/wt && got revert alpha > /dev/null)
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		echo "got revert failed unexpectedly" >&2
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	(cd $testroot/wt && got merge newbranch > $testroot/stdout)
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		echo "got merge failed unexpectedly" >&2
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	local merge_commit=`git_show_head $testroot/repo`
+
+	echo "G  alpha" >> $testroot/stdout.expected
+	echo "D  beta" >> $testroot/stdout.expected
+	echo "A  epsilon/new" >> $testroot/stdout.expected
+	echo "G  gamma/delta" >> $testroot/stdout.expected
+	echo "A  symlink" >> $testroot/stdout.expected
+	echo -n "Merged refs/heads/newbranch into refs/heads/master: " \
+		>> $testroot/stdout.expected
+	echo $merge_commit >> $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 delta on branch" > $testroot/content.expected
+	cat $testroot/wt/gamma/delta > $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
+
+	echo "modified alpha on branch" > $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 branch" > $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
+
+	readlink $testroot/wt/symlink > $testroot/stdout
+	echo "alpha" > $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 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 $merge_commit (master)" > $testroot/stdout.expected
+	echo "commit $master_commit" >> $testroot/stdout.expected
+	echo "commit $commit0" >> $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 update > $testroot/stdout)
+
+	echo 'Already up-to-date' > $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
+
+	# We should have created a merge commit with two parents.
+	(cd $testroot/wt && got log -l1 | grep ^parent > $testroot/stdout)
+	echo "parent 1: $master_commit" > $testroot/stdout.expected
+	echo "parent 2: $branch_commit4" >> $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"
+}
+
+test_merge_continue() {
+	local testroot=`test_init merge_continue`
+	local commit0=`git_show_head $testroot/repo`
+	local commit0_author_time=`git_show_author_time $testroot/repo`
+
+	(cd $testroot/repo && git checkout -q -b newbranch)
+	echo "modified delta on branch" > $testroot/repo/gamma/delta
+	git_commit $testroot/repo -m "committing to delta on newbranch"
+	local branch_commit0=`git_show_branch_head $testroot/repo newbranch`
+
+	echo "modified alpha on branch" > $testroot/repo/alpha
+	git_commit $testroot/repo -m "committing to alpha on newbranch"
+	local branch_commit1=`git_show_branch_head $testroot/repo newbranch`
+	(cd $testroot/repo && git rm -q beta)
+	git_commit $testroot/repo -m "removing beta on newbranch"
+	local branch_commit2=`git_show_branch_head $testroot/repo newbranch`
+	echo "new file on branch" > $testroot/repo/epsilon/new
+	(cd $testroot/repo && git add epsilon/new)
+	git_commit $testroot/repo -m "adding new file on newbranch"
+	local branch_commit3=`git_show_branch_head $testroot/repo newbranch`
+
+	got checkout -b master $testroot/repo $testroot/wt > /dev/null
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		echo "got checkout failed unexpectedly" >&2
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# create a conflicting commit
+	(cd $testroot/repo && git checkout -q master)
+	echo "modified alpha on master" > $testroot/repo/alpha
+	git_commit $testroot/repo -m "committing to alpha on master"
+	local master_commit=`git_show_head $testroot/repo`
+
+	# need an up-to-date work tree for 'got merge' 
+	(cd $testroot/wt && got update > /dev/null)
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		echo "got update failed unexpectedly" >&2
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	(cd $testroot/wt && got merge newbranch \
+		> $testroot/stdout 2> $testroot/stderr)
+	ret="$?"
+	if [ "$ret" == "0" ]; then
+		echo "got merge succeeded unexpectedly" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	echo "C  alpha" >> $testroot/stdout.expected
+	echo "D  beta" >> $testroot/stdout.expected
+	echo "A  epsilon/new" >> $testroot/stdout.expected
+	echo "G  gamma/delta" >> $testroot/stdout.expected
+	echo "Files with new merge conflicts: 1" >> $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 "got: conflicts must be resolved before merging can continue" \
+		> $testroot/stderr.expected
+	cmp -s $testroot/stderr.expected $testroot/stderr
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stderr.expected $testroot/stderr
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	(cd $testroot/wt && got status > $testroot/stdout)
+
+	echo "C  alpha" > $testroot/stdout.expected
+	echo "D  beta" >> $testroot/stdout.expected
+	echo "A  epsilon/new" >> $testroot/stdout.expected
+	echo "M  gamma/delta" >> $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 '<<<<<<<' > $testroot/content.expected
+	echo "modified alpha on master" >> $testroot/content.expected
+	echo "||||||| 3-way merge base: commit $commit0" \
+		>> $testroot/content.expected
+	echo "alpha" >> $testroot/content.expected
+	echo "=======" >> $testroot/content.expected
+	echo "modified alpha on branch" >> $testroot/content.expected
+	echo ">>>>>>> merged change: commit $branch_commit3" \
+		>> $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
+
+	# resolve the conflict
+	echo "modified alpha by both branches" > $testroot/wt/alpha
+
+	(cd $testroot/wt && got merge -c > $testroot/stdout)
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		echo "got merge failed unexpectedly" >&2
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	local merge_commit=`git_show_head $testroot/repo`
+
+	echo -n "Merged refs/heads/newbranch into refs/heads/master: " \
+		> $testroot/stdout.expected
+	echo $merge_commit >> $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 delta on branch" > $testroot/content.expected
+	cat $testroot/wt/gamma/delta > $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
+
+	echo "modified alpha by both branches" > $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 branch" > $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 $merge_commit (master)" > $testroot/stdout.expected
+	echo "commit $master_commit" >> $testroot/stdout.expected
+	echo "commit $commit0" >> $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 update > $testroot/stdout)
+
+	echo 'Already up-to-date' > $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
+
+	# We should have created a merge commit with two parents.
+	(cd $testroot/wt && got log -l1 | grep ^parent > $testroot/stdout)
+	echo "parent 1: $master_commit" > $testroot/stdout.expected
+	echo "parent 2: $branch_commit3" >> $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"
+}
+
+test_merge_abort() {
+	local testroot=`test_init merge_abort`
+	local commit0=`git_show_head $testroot/repo`
+	local commit0_author_time=`git_show_author_time $testroot/repo`
+
+	(cd $testroot/repo && git checkout -q -b newbranch)
+	echo "modified delta on branch" > $testroot/repo/gamma/delta
+	git_commit $testroot/repo -m "committing to delta on newbranch"
+	local branch_commit0=`git_show_branch_head $testroot/repo newbranch`
+
+	echo "modified alpha on branch" > $testroot/repo/alpha
+	git_commit $testroot/repo -m "committing to alpha on newbranch"
+	local branch_commit1=`git_show_branch_head $testroot/repo newbranch`
+	(cd $testroot/repo && git rm -q beta)
+	git_commit $testroot/repo -m "removing beta on newbranch"
+	local branch_commit2=`git_show_branch_head $testroot/repo newbranch`
+	echo "new file on branch" > $testroot/repo/epsilon/new
+	(cd $testroot/repo && git add epsilon/new)
+	git_commit $testroot/repo -m "adding new file on newbranch"
+	local branch_commit3=`git_show_branch_head $testroot/repo newbranch`
+	(cd $testroot/repo && ln -s alpha symlink && git add symlink)
+	git_commit $testroot/repo -m "adding symlink on newbranch"
+	local branch_commit4=`git_show_branch_head $testroot/repo newbranch`
+
+	got checkout -b master $testroot/repo $testroot/wt > /dev/null
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		echo "got checkout failed unexpectedly" >&2
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# create a conflicting commit
+	(cd $testroot/repo && git checkout -q master)
+	echo "modified alpha on master" > $testroot/repo/alpha
+	git_commit $testroot/repo -m "committing to alpha on master"
+	local master_commit=`git_show_head $testroot/repo`
+
+	# need an up-to-date work tree for 'got merge' 
+	(cd $testroot/wt && got update > /dev/null)
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		echo "got update failed unexpectedly" >&2
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	(cd $testroot/wt && got merge newbranch \
+		> $testroot/stdout 2> $testroot/stderr)
+	ret="$?"
+	if [ "$ret" == "0" ]; then
+		echo "got merge succeeded unexpectedly" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	echo "C  alpha" >> $testroot/stdout.expected
+	echo "D  beta" >> $testroot/stdout.expected
+	echo "A  epsilon/new" >> $testroot/stdout.expected
+	echo "G  gamma/delta" >> $testroot/stdout.expected
+	echo "A  symlink" >> $testroot/stdout.expected
+	echo "Files with new merge conflicts: 1" >> $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 "got: conflicts must be resolved before merging can continue" \
+		> $testroot/stderr.expected
+	cmp -s $testroot/stderr.expected $testroot/stderr
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stderr.expected $testroot/stderr
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	(cd $testroot/wt && got status > $testroot/stdout)
+
+	echo "C  alpha" > $testroot/stdout.expected
+	echo "D  beta" >> $testroot/stdout.expected
+	echo "A  epsilon/new" >> $testroot/stdout.expected
+	echo "M  gamma/delta" >> $testroot/stdout.expected
+	echo "A  symlink" >> $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 merge -a > $testroot/stdout)
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		echo "got merge failed unexpectedly" >&2
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	echo "R  alpha" > $testroot/stdout.expected
+	echo "R  beta" >> $testroot/stdout.expected
+	echo "R  epsilon/new" >> $testroot/stdout.expected
+	echo "R  gamma/delta" >> $testroot/stdout.expected
+	echo "R  symlink" >> $testroot/stdout.expected
+	echo "Merge of refs/heads/newbranch 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
+
+	echo "delta" > $testroot/content.expected
+	cat $testroot/wt/gamma/delta > $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
+
+	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
+
+	echo "beta" > $testroot/content.expected
+	cat $testroot/wt/beta > $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/epsilon/new ]; then
+		echo "reverted file epsilon/new still exists on disk" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	if [ -e $testroot/wt/symlink ]; then
+		echo "reverted symlink still exists on disk" >&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 $master_commit (master)" > $testroot/stdout.expected
+	echo "commit $commit0" >> $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 update > $testroot/stdout)
+
+	echo 'Already up-to-date' > $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"
+}
+
+test_merge_in_progress() {
+	local testroot=`test_init merge_in_progress`
+	local commit0=`git_show_head $testroot/repo`
+	local commit0_author_time=`git_show_author_time $testroot/repo`
+
+	(cd $testroot/repo && git checkout -q -b newbranch)
+	echo "modified alpha on branch" > $testroot/repo/alpha
+	git_commit $testroot/repo -m "committing to alpha on newbranch"
+	local branch_commit0=`git_show_branch_head $testroot/repo newbranch`
+
+	got checkout -b master $testroot/repo $testroot/wt > /dev/null
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		echo "got checkout failed unexpectedly" >&2
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# create a conflicting commit
+	(cd $testroot/repo && git checkout -q master)
+	echo "modified alpha on master" > $testroot/repo/alpha
+	git_commit $testroot/repo -m "committing to alpha on master"
+	local master_commit=`git_show_head $testroot/repo`
+
+	# need an up-to-date work tree for 'got merge' 
+	(cd $testroot/wt && got update > /dev/null)
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		echo "got update failed unexpectedly" >&2
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	(cd $testroot/wt && got merge newbranch \
+		> $testroot/stdout 2> $testroot/stderr)
+	ret="$?"
+	if [ "$ret" == "0" ]; then
+		echo "got merge succeeded unexpectedly" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	echo "C  alpha" >> $testroot/stdout.expected
+	echo "Files with new merge conflicts: 1" >> $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 "got: conflicts must be resolved before merging can continue" \
+		> $testroot/stderr.expected
+	cmp -s $testroot/stderr.expected $testroot/stderr
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stderr.expected $testroot/stderr
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	(cd $testroot/wt && got status > $testroot/stdout)
+
+	echo "C  alpha" > $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 cmd in update commit histedit "rebase newbranch" \
+		"integrate newbranch" "stage alpha"; do
+		(cd $testroot/wt && got $cmd > $testroot/stdout \
+			2> $testroot/stderr)
+
+		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
+
+		echo -n "got: a merge operation is in progress in this " \
+			> $testroot/stderr.expected
+		echo "work tree and must be continued or aborted first" \
+			>> $testroot/stderr.expected
+		cmp -s $testroot/stderr.expected $testroot/stderr
+		ret="$?"
+		if [ "$ret" != "0" ]; then
+			diff -u $testroot/stderr.expected $testroot/stderr
+			test_done "$testroot" "$ret"
+			return 1
+		fi
+	done
+
+	test_done "$testroot" "$ret"
+}
+
+test_merge_path_prefix() {
+	local testroot=`test_init merge_path_prefix`
+	local commit0=`git_show_head $testroot/repo`
+	local commit0_author_time=`git_show_author_time $testroot/repo`
+
+	(cd $testroot/repo && git checkout -q -b newbranch)
+	echo "modified alpha on branch" > $testroot/repo/alpha
+	git_commit $testroot/repo -m "committing to alpha on newbranch"
+	local branch_commit0=`git_show_branch_head $testroot/repo newbranch`
+
+	got checkout -p epsilon -b master $testroot/repo $testroot/wt \
+		> /dev/null
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		echo "got checkout failed unexpectedly" >&2
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# create a conflicting commit
+	(cd $testroot/repo && git checkout -q master)
+	echo "modified alpha on master" > $testroot/repo/alpha
+	git_commit $testroot/repo -m "committing to alpha on master"
+	local master_commit=`git_show_head $testroot/repo`
+
+	# need an up-to-date work tree for 'got merge' 
+	(cd $testroot/wt && got update > /dev/null)
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		echo "got update failed unexpectedly" >&2
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	(cd $testroot/wt && got merge newbranch \
+		> $testroot/stdout 2> $testroot/stderr)
+	ret="$?"
+	if [ "$ret" == "0" ]; then
+		echo "got merge succeeded unexpectedly" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	echo -n "got: cannot merge branch which contains changes outside " \
+		> $testroot/stderr.expected
+	echo "of this work tree's path prefix" >> $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"
+}
+
+test_merge_missing_file() {
+	local testroot=`test_init merge_missing_file`
+	local commit0=`git_show_head $testroot/repo`
+	local commit0_author_time=`git_show_author_time $testroot/repo`
+
+	(cd $testroot/repo && git checkout -q -b newbranch)
+	echo "modified alpha on branch" > $testroot/repo/alpha
+	echo "modified delta on branch" > $testroot/repo/gamma/delta
+	git_commit $testroot/repo -m "committing to alpha and delta"
+	local branch_commit0=`git_show_branch_head $testroot/repo newbranch`
+
+	got checkout -b master $testroot/repo $testroot/wt > /dev/null
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		echo "got checkout failed unexpectedly" >&2
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# create a conflicting commit which renames alpha
+	(cd $testroot/repo && git checkout -q master)
+	(cd $testroot/repo && git mv alpha epsilon/alpha-moved)
+	git_commit $testroot/repo -m "moving alpha on master"
+	local master_commit=`git_show_head $testroot/repo`
+
+	# need an up-to-date work tree for 'got merge' 
+	(cd $testroot/wt && got update > /dev/null)
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		echo "got update failed unexpectedly" >&2
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	(cd $testroot/wt && got merge newbranch \
+		> $testroot/stdout 2> $testroot/stderr)
+	ret="$?"
+	if [ "$ret" == "0" ]; then
+		echo "got merge succeeded unexpectedly" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	echo "!  alpha" > $testroot/stdout.expected
+	echo "G  gamma/delta" >> $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 -n "got: changes destined for missing or obstructed files " \
+		> $testroot/stderr.expected
+	echo -n "were not yet merged and should be merged manually if " \
+		>> $testroot/stderr.expected
+	echo "required before the merge operation is continued" \
+		>> $testroot/stderr.expected
+	cmp -s $testroot/stderr.expected $testroot/stderr
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stderr.expected $testroot/stderr
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	(cd $testroot/wt && got status > $testroot/stdout)
+
+	echo "M  gamma/delta" > $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
+
+	test_done "$testroot" "$ret"
+}
+
+test_parseargs "$@"
+run_test test_merge_basic
+run_test test_merge_continue
+run_test test_merge_abort
+run_test test_merge_in_progress
+run_test test_merge_path_prefix
+run_test test_merge_missing_file