Commit Diff


commit - ac4dc26386a91b9c488d78fd7065780e1eaf33d7
commit + 10604dce6e625d11974fb5491598bbb54069e5d3
blob - 7908e0d4a3f37ad292df5a94592a0e75437876a3
blob + 55846aa7e98871dabc52f2ac079d93817150bc11
--- 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 - 34411f6f1015b3063c7652f4227a107f42a77dfa
blob + 68051b649756089f26c38a6069faddb38304f590
--- 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++;
@@ -3173,6 +3179,22 @@ check_rebase_or_histedit_in_progress(struct got_worktr
 		return err;
 	if (in_progress)
 		return got_error(GOT_ERR_HISTEDIT_BUSY);
+
+	return NULL;
+}
+
+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;
 }
@@ -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;
@@ -8845,6 +8879,15 @@ cmd_rebase(int argc, char *argv[])
 		goto done;
 	if (histedit_in_progress) {
 		error = got_error(GOT_ERR_HISTEDIT_BUSY);
+		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;
 	}
 
@@ -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;
@@ -10059,6 +10102,15 @@ cmd_histedit(int argc, char *argv[])
 		goto done;
 	if (rebase_in_progress) {
 		error = got_error(GOT_ERR_REBASING);
+		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;
 	}
 
@@ -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;
@@ -10528,6 +10584,256 @@ done:
 	free(commit_id);
 	free(refname);
 	free(base_refname);
+	return error;
+}
+
+__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 the "
+			    "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;
 }
 
@@ -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 - 33a8b4f9b4f13aab5a6bec4715c0e5a7f6939061
blob + 4e87ec59462a265ba7b4c932db9772fbd93373c1
--- include/got_error.h
+++ include/got_error.h
@@ -157,6 +157,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;
@@ -320,6 +326,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 + 0dcba98cb99b5350473eff9a91f3600e24b7ff91
--- include/got_worktree.h
+++ include/got_worktree.h
@@ -436,8 +436,74 @@ const struct got_error *got_worktree_integrate_continu
 const struct got_error *got_worktree_integrate_abort(struct got_worktree *,
     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 to
+ * this branch's 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 - 4e86317f1d38e8899d2d7bcd6a35a17c6d03ae53
blob + fb842d05b1ddc1f853d2c8d63d5b6db4793a3a55
--- lib/object_create.c
+++ lib/object_create.c
@@ -491,11 +491,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;
@@ -513,6 +513,8 @@ got_object_commit_create(struct got_object_id **id,
 				goto done;
 			}
 			free(parent_str);
+			free(id_str);
+			id_str = NULL;
 		}
 	}
 
@@ -569,6 +571,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 - 264a17dbcf80c208a1facb0bd41b8a1ca780b455
blob + 6dd080eb395b1890c2edae59e78e5e795e2e0262
--- lib/worktree.c
+++ lib/worktree.c
@@ -2376,8 +2376,22 @@ got_worktree_get_histedit_script_path(char **path,
 		return got_error_from_errno("asprintf");
 	}
 	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.
@@ -3275,7 +3289,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);
@@ -4409,6 +4424,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 *
@@ -4689,6 +4705,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) {
@@ -4826,6 +4855,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);
@@ -5581,7 +5611,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,
@@ -5595,7 +5627,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;
@@ -5659,9 +5691,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)
@@ -5700,6 +5739,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)
@@ -5860,7 +5900,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;
@@ -6442,7 +6482,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)
@@ -6766,6 +6806,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)
@@ -7119,6 +7160,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)
@@ -7369,10 +7411,493 @@ got_worktree_integrate_abort(struct got_worktree *work
 	if (unlockerr && err == NULL)
 		err = unlockerr;
 	got_ref_close(base_branch_ref);
+
+	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 - 68314f1e77bb35025aeb5401ca14f24b773350a5
blob + 54055c09da65df95bc8676121ad774abaed5f07c
--- regress/cmdline/Makefile
+++ regress/cmdline/Makefile
@@ -1,6 +1,6 @@
 REGRESS_TARGETS=checkout update status log add rm diff blame branch tag \
 	ref commit revert cherrypick backout rebase import histedit \
-	integrate stage unstage cat clone fetch tree pack cleanup
+	integrate merge stage unstage cat clone fetch tree pack cleanup
 NOOBJ=Yes
 
 GOT_TEST_ROOT=/tmp
@@ -62,6 +62,9 @@ histedit:
 integrate:
 	./integrate.sh -q -r "$(GOT_TEST_ROOT)"
 
+merge:
+	./merge.sh -q -r "$(GOT_TEST_ROOT)"
+
 stage:
 	./stage.sh -q -r "$(GOT_TEST_ROOT)"
 
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