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

From:
Tracey Emery <tracey@traceyemery.net>
Subject:
Re: add a 'got merge' command
To:
Stefan Sperling <stsp@stsp.name>
Cc:
gameoftrees@openbsd.org
Date:
Fri, 24 Sep 2021 13:42:44 -0600

Download raw body.

Thread
On Fri, Sep 24, 2021 at 06:35:57PM +0200, Stefan Sperling wrote:
> 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?

See changes and thoughts below. Otherwise, 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

branch is required? rm Op

> +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

desired, then

> +.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

merged, then

> +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

occur, the
> +must be resolved before the merge operation can continue.
> +If any files with destined changes are found to be missing or obstructed

obstructed, the

> +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

branch, then

> +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

branch, the

> +.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",

Isn't branch required? It shouldn't be in brackets, if so.

> +	    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;

Are we leeking branch_refname here?

> +
> +	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

-- 

Tracey Emery