Download raw body.
add a 'got merge' command
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
add a 'got merge' command