From: Tracey Emery Subject: Re: add a 'got merge' command To: Stefan Sperling Cc: gameoftrees@openbsd.org Date: Fri, 24 Sep 2021 13:42:44 -0600 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 > +# > +# 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