commit 10604dce6e625d11974fb5491598bbb54069e5d3 from: Stefan Sperling via: Thomas Adam date: Fri Sep 24 22:01:09 2021 UTC add a 'got merge' command for creating merge commits Additional testing by Thomas Adam. ok tracey commit - ac4dc26386a91b9c488d78fd7065780e1eaf33d7 commit + 10604dce6e625d11974fb5491598bbb54069e5d3 blob - 7908e0d4a3f37ad292df5a94592a0e75437876a3 blob + 55846aa7e98871dabc52f2ac079d93817150bc11 --- got/got.1 +++ got/got.1 @@ -2132,6 +2132,123 @@ or reverted with .It Cm ig Short alias for .Cm integrate . +.It Cm merge Oo Fl a Oc Oo Fl c Oc Op Ar branch +Create a merge commit based on the current branch of the work tree and +the specified +.Ar branch . +If a linear project history is desired, then use of +.Cm got rebase +should be preferred over +.Cm got merge . +However, even strictly linear projects may require merge commits in order +to merge in new versions of code imported from third-party projects on +vendor branches. +.Pp +Merge commits are commits based on multiple parent commits. +The tip commit of the work tree's current branch, which must be set with +.Cm got update -b +before starting the +.Cm merge +operation, will be used as the first parent. +The tip commit of the specified +.Ar branch +will be used as the second parent. +.Pp +It is not possible to create merge commits with more than two parents. +If more than one branch needs to be merged, then multiple merge commits +with two parents each can be created in sequence. +.Pp +The +.Ar branch +must share common ancestry with the work tree's current branch. +.Pp +While merging changes found on the +.Ar branch +into the work tree, show the status of each affected file, +using the following status codes: +.Bl -column YXZ description +.It G Ta file was merged +.It C Ta file was merged and conflicts occurred during merge +.It ! Ta changes destined for a missing file were not merged +.It D Ta file was deleted +.It d Ta file's deletion was obstructed by local modifications +.It A Ta new file was added +.It \(a~ Ta changes destined for a non-regular file were not merged +.It ? Ta changes destined for an unversioned file were not merged +.El +.Pp +If merge conflicts occur, the merge operation is interrupted and conflicts +must be resolved before the merge operation can continue. +If any files with destined changes are found to be missing or obstructed, +the merge operation will be interrupted to prevent potentially incomplete +changes from being committed to the repository without user intervention. +The work tree may be modified as desired and the merge can be continued +once the changes present in the work tree are considered complete. +Alternatively, the merge operation may be aborted which will leave +the work tree's current branch unmodified. +.Pp +If a merge conflict is resolved in a way which renders all merged +changes into no-op changes, the merge operation cannot continue +and must be aborted. +.Pp +.Cm got merge +will refuse to run if certain preconditions are not met. +If history of the +.Ar branch +is based on the work tree's current branch, then no merge commit can +be created and +.Cm got integrate +may be used to integrate the +.Ar branch +instead. +If the work tree is not yet fully updated to the tip commit of its +branch, then the work tree must first be updated with +.Cm got update . +If the work tree contains multiple base commits it must first be updated +to a single base commit with +.Cm got update . +If changes have been staged with +.Cm got stage , +these changes must first be committed with +.Cm got commit +or unstaged with +.Cm got unstage . +If the work tree contains local changes, these changes must first be +committed with +.Cm got commit +or reverted with +.Cm got revert . +If the +.Ar branch +contains changes to files outside of the work tree's path prefix, +the work tree cannot be used to merge this branch. +.Pp +The +.Cm got update , +.Cm got commit , +.Cm got rebase , +.Cm got histedit , +.Cm got integrate , +and +.Cm got stage +commands will refuse to run while a merge operation is in progress. +Other commands which manipulate the work tree may be used for +conflict resolution purposes. +.Pp +The options for +.Cm got merge +are as follows: +.Bl -tag -width Ds +.It Fl a +Abort an interrupted merge operation. +If this option is used, no other command-line arguments are allowed. +.It Fl c +Continue an interrupted merge operation. +If this option is used, no other command-line arguments are allowed. +.El +.It Cm mg +Short alias for +.Cm merge . .It Cm stage Oo Fl l Oc Oo Fl p Oc Oo Fl F Ar response-script Oc Oo Fl S Oc Op Ar path ... Stage local changes for inclusion in the next commit. If no blob - 34411f6f1015b3063c7652f4227a107f42a77dfa blob + 68051b649756089f26c38a6069faddb38304f590 --- got/got.c +++ got/got.c @@ -109,6 +109,7 @@ __dead static void usage_backout(void); __dead static void usage_rebase(void); __dead static void usage_histedit(void); __dead static void usage_integrate(void); +__dead static void usage_merge(void); __dead static void usage_stage(void); __dead static void usage_unstage(void); __dead static void usage_cat(void); @@ -138,6 +139,7 @@ static const struct got_error* cmd_backout(int, char static const struct got_error* cmd_rebase(int, char *[]); static const struct got_error* cmd_histedit(int, char *[]); static const struct got_error* cmd_integrate(int, char *[]); +static const struct got_error* cmd_merge(int, char *[]); static const struct got_error* cmd_stage(int, char *[]); static const struct got_error* cmd_unstage(int, char *[]); static const struct got_error* cmd_cat(int, char *[]); @@ -168,6 +170,7 @@ static struct got_cmd got_commands[] = { { "rebase", cmd_rebase, usage_rebase, "rb" }, { "histedit", cmd_histedit, usage_histedit, "he" }, { "integrate", cmd_integrate, usage_integrate,"ig" }, + { "merge", cmd_merge, usage_merge, "mg" }, { "stage", cmd_stage, usage_stage, "sg" }, { "unstage", cmd_unstage, usage_unstage, "ug" }, { "cat", cmd_cat, usage_cat, "" }, @@ -3058,6 +3061,7 @@ struct got_update_progress_arg { int conflicts; int obstructed; int not_updated; + int missing; int verbosity; }; @@ -3107,6 +3111,8 @@ update_progress(void *arg, unsigned char status, const upa->obstructed++; if (status == GOT_STATUS_CANNOT_UPDATE) upa->not_updated++; + if (status == GOT_STATUS_MISSING) + upa->missing++; while (path[0] == '/') path++; @@ -3173,6 +3179,22 @@ check_rebase_or_histedit_in_progress(struct got_worktr return err; if (in_progress) return got_error(GOT_ERR_HISTEDIT_BUSY); + + return NULL; +} + +static const struct got_error * +check_merge_in_progress(struct got_worktree *worktree, + struct got_repository *repo) +{ + const struct got_error *err; + int in_progress; + + err = got_worktree_merge_in_progress(&in_progress, worktree, repo); + if (err) + return err; + if (in_progress) + return got_error(GOT_ERR_MERGE_BUSY); return NULL; } @@ -3300,6 +3322,10 @@ cmd_update(int argc, char *argv[]) if (error) goto done; + error = check_merge_in_progress(worktree, repo); + if (error) + goto done; + error = get_worktree_paths_from_argv(&paths, argc, argv, worktree); if (error) goto done; @@ -7317,7 +7343,7 @@ cmd_commit(int argc, char *argv[]) struct collect_commit_logmsg_arg cl_arg; char *gitconfig_path = NULL, *editor = NULL, *author = NULL; int ch, rebase_in_progress, histedit_in_progress, preserve_logmsg = 0; - int allow_bad_symlinks = 0, non_interactive = 0; + int allow_bad_symlinks = 0, non_interactive = 0, merge_in_progress = 0; struct got_pathlist_head paths; TAILQ_INIT(&paths); @@ -7391,6 +7417,14 @@ cmd_commit(int argc, char *argv[]) if (error != NULL) goto done; + error = got_worktree_merge_in_progress(&merge_in_progress, worktree, repo); + if (error) + goto done; + if (merge_in_progress) { + error = got_error(GOT_ERR_MERGE_BUSY); + goto done; + } + error = get_author(&author, repo, worktree); if (error) return error; @@ -8157,8 +8191,8 @@ cmd_backout(int argc, char *argv[]) } memset(&upa, 0, sizeof(upa)); - error = got_worktree_merge_files(worktree, commit_id, pid->id, repo, - update_progress, &upa, check_cancelled, NULL); + error = got_worktree_merge_files(worktree, commit_id, pid->id, + repo, update_progress, &upa, check_cancelled, NULL); if (error != NULL) goto done; @@ -8737,8 +8771,8 @@ cmd_rebase(int argc, char *argv[]) struct got_object_id *branch_head_commit_id = NULL, *yca_id = NULL; struct got_commit_object *commit = NULL; int ch, rebase_in_progress = 0, abort_rebase = 0, continue_rebase = 0; - int histedit_in_progress = 0, create_backup = 1, list_backups = 0; - int delete_backups = 0; + int histedit_in_progress = 0, merge_in_progress = 0; + int create_backup = 1, list_backups = 0, delete_backups = 0; unsigned char rebase_status = GOT_STATUS_NO_CHANGE; struct got_object_id_queue commits; struct got_pathlist_head merged_paths; @@ -8845,6 +8879,15 @@ cmd_rebase(int argc, char *argv[]) goto done; if (histedit_in_progress) { error = got_error(GOT_ERR_HISTEDIT_BUSY); + goto done; + } + + error = got_worktree_merge_in_progress(&merge_in_progress, + worktree, repo); + if (error) + goto done; + if (merge_in_progress) { + error = got_error(GOT_ERR_MERGE_BUSY); goto done; } @@ -9901,7 +9944,7 @@ cmd_histedit(int argc, char *argv[]) struct got_object_id *base_commit_id = NULL; struct got_object_id *head_commit_id = NULL; struct got_commit_object *commit = NULL; - int ch, rebase_in_progress = 0; + int ch, rebase_in_progress = 0, merge_in_progress = 0; struct got_update_progress_arg upa; int edit_in_progress = 0, abort_edit = 0, continue_edit = 0; int edit_logmsg_only = 0, fold_only = 0; @@ -10059,6 +10102,15 @@ cmd_histedit(int argc, char *argv[]) goto done; if (rebase_in_progress) { error = got_error(GOT_ERR_REBASING); + goto done; + } + + error = got_worktree_merge_in_progress(&merge_in_progress, worktree, + repo); + if (error) + goto done; + if (merge_in_progress) { + error = got_error(GOT_ERR_MERGE_BUSY); goto done; } @@ -10456,6 +10508,10 @@ cmd_integrate(int argc, char *argv[]) if (error) goto done; + error = check_merge_in_progress(worktree, repo); + if (error) + goto done; + if (asprintf(&refname, "refs/heads/%s", branch_arg) == -1) { error = got_error_from_errno("asprintf"); goto done; @@ -10528,6 +10584,256 @@ done: free(commit_id); free(refname); free(base_refname); + return error; +} + +__dead static void +usage_merge(void) +{ + fprintf(stderr, "usage: %s merge [-a] [-c] [branch]\n", + getprogname()); + exit(1); +} + +static const struct got_error * +cmd_merge(int argc, char *argv[]) +{ + const struct got_error *error = NULL; + struct got_worktree *worktree = NULL; + struct got_repository *repo = NULL; + struct got_fileindex *fileindex = NULL; + char *cwd = NULL, *id_str = NULL, *author = NULL; + struct got_reference *branch = NULL, *wt_branch = NULL; + struct got_object_id *branch_tip = NULL, *yca_id = NULL; + struct got_object_id *wt_branch_tip = NULL; + int ch, merge_in_progress = 0, abort_merge = 0, continue_merge = 0; + struct got_update_progress_arg upa; + struct got_object_id *merge_commit_id = NULL; + char *branch_name = NULL; + + memset(&upa, 0, sizeof(upa)); + + while ((ch = getopt(argc, argv, "ac")) != -1) { + switch (ch) { + case 'a': + abort_merge = 1; + break; + case 'c': + continue_merge = 1; + break; + default: + usage_rebase(); + /* NOTREACHED */ + } + } + + argc -= optind; + argv += optind; + +#ifndef PROFILE + if (pledge("stdio rpath wpath cpath fattr flock proc exec sendfd " + "unveil", NULL) == -1) + err(1, "pledge"); +#endif + + if (abort_merge && continue_merge) + option_conflict('a', 'c'); + if (abort_merge || continue_merge) { + if (argc != 0) + usage_merge(); + } else if (argc != 1) + usage_merge(); + + cwd = getcwd(NULL, 0); + if (cwd == NULL) { + error = got_error_from_errno("getcwd"); + goto done; + } + + error = got_worktree_open(&worktree, cwd); + if (error) { + if (error->code == GOT_ERR_NOT_WORKTREE) + error = wrap_not_worktree_error(error, + "merge", cwd); + goto done; + } + + error = got_repo_open(&repo, + worktree ? got_worktree_get_repo_path(worktree) : cwd, NULL); + if (error != NULL) + goto done; + + error = apply_unveil(got_repo_get_path(repo), 0, + worktree ? got_worktree_get_root_path(worktree) : NULL); + if (error) + goto done; + + error = check_rebase_or_histedit_in_progress(worktree); + if (error) + goto done; + + error = got_worktree_merge_in_progress(&merge_in_progress, worktree, + repo); + if (error) + goto done; + + if (abort_merge) { + if (!merge_in_progress) { + error = got_error(GOT_ERR_NOT_MERGING); + goto done; + } + error = got_worktree_merge_continue(&branch_name, + &branch_tip, &fileindex, worktree, repo); + if (error) + goto done; + error = got_worktree_merge_abort(worktree, fileindex, repo, + update_progress, &upa); + if (error) + goto done; + printf("Merge of %s aborted\n", branch_name); + goto done; /* nothing else to do */ + } + + error = get_author(&author, repo, worktree); + if (error) + goto done; + + if (continue_merge) { + if (!merge_in_progress) { + error = got_error(GOT_ERR_NOT_MERGING); + goto done; + } + error = got_worktree_merge_continue(&branch_name, + &branch_tip, &fileindex, worktree, repo); + if (error) + goto done; + } else { + error = got_ref_open(&branch, repo, argv[0], 0); + if (error != NULL) + goto done; + branch_name = strdup(got_ref_get_name(branch)); + if (branch_name == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + error = got_ref_resolve(&branch_tip, repo, branch); + if (error) + goto done; + } + + error = got_ref_open(&wt_branch, repo, + got_worktree_get_head_ref_name(worktree), 0); + if (error) + goto done; + error = got_ref_resolve(&wt_branch_tip, repo, wt_branch); + if (error) + goto done; + error = got_commit_graph_find_youngest_common_ancestor(&yca_id, + wt_branch_tip, branch_tip, repo, + check_cancelled, NULL); + if (error) + goto done; + if (yca_id == NULL) { + error = got_error_msg(GOT_ERR_ANCESTRY, + "specified branch shares no common ancestry " + "with work tree's branch"); + goto done; + } + + if (!continue_merge) { + error = check_path_prefix(wt_branch_tip, branch_tip, + got_worktree_get_path_prefix(worktree), + GOT_ERR_MERGE_PATH, repo); + if (error) + goto done; + error = check_same_branch(wt_branch_tip, branch, + yca_id, repo); + if (error) { + if (error->code != GOT_ERR_ANCESTRY) + goto done; + error = NULL; + } else { + static char msg[512]; + snprintf(msg, sizeof(msg), + "cannot create a merge commit because " + "%s is based on %s; %s can be integrated " + "with 'got integrate' instead", branch_name, + got_worktree_get_head_ref_name(worktree), + branch_name); + error = got_error_msg(GOT_ERR_SAME_BRANCH, msg); + goto done; + } + error = got_worktree_merge_prepare(&fileindex, worktree, + branch, repo); + if (error) + goto done; + + error = got_worktree_merge_branch(worktree, fileindex, + yca_id, branch_tip, repo, update_progress, &upa, + check_cancelled, NULL); + if (error) + goto done; + print_update_progress_stats(&upa); + } + + if (upa.conflicts > 0 || upa.obstructed > 0 || upa.missing > 0) { + error = got_worktree_merge_postpone(worktree, fileindex); + if (error) + goto done; + if (upa.conflicts > 0 && + upa.obstructed == 0 && upa.missing == 0) { + error = got_error_msg(GOT_ERR_CONFLICTS, + "conflicts must be resolved before merging " + "can continue"); + } else if (upa.conflicts > 0) { + error = got_error_msg(GOT_ERR_CONFLICTS, + "conflicts must be resolved before merging " + "can continue; changes destined for missing " + "or obstructed files were not yet merged and " + "should be merged manually if required before the " + "merge operation is continued"); + } else { + error = got_error_msg(GOT_ERR_CONFLICTS, + "changes destined for missing or obstructed " + "files were not yet merged and should be " + "merged manually if required before the " + "merge operation is continued"); + } + goto done; + } else { + error = got_worktree_merge_commit(&merge_commit_id, worktree, + fileindex, author, NULL, 1, branch_tip, branch_name, repo); + if (error) + goto done; + error = got_worktree_merge_complete(worktree, fileindex, repo); + if (error) + goto done; + error = got_object_id_str(&id_str, merge_commit_id); + if (error) + goto done; + printf("Merged %s into %s: %s\n", branch_name, + got_worktree_get_head_ref_name(worktree), + id_str); + + } +done: + free(id_str); + free(merge_commit_id); + free(author); + free(branch_tip); + free(branch_name); + free(yca_id); + if (branch) + got_ref_close(branch); + if (wt_branch) + got_ref_close(wt_branch); + if (worktree) + got_worktree_close(worktree); + if (repo) { + const struct got_error *close_err = got_repo_close(repo); + if (error == NULL) + error = close_err; + } return error; } @@ -10647,6 +10953,10 @@ cmd_stage(int argc, char *argv[]) if (error) goto done; + error = check_merge_in_progress(worktree, repo); + if (error) + goto done; + error = get_worktree_paths_from_argv(&paths, argc, argv, worktree); if (error) goto done; blob - 33a8b4f9b4f13aab5a6bec4715c0e5a7f6939061 blob + 4e87ec59462a265ba7b4c932db9772fbd93373c1 --- include/got_error.h +++ include/got_error.h @@ -157,6 +157,12 @@ #define GOT_ERR_CAPA_DELETE_REFS 138 #define GOT_ERR_SEND_DELETE_REF 139 #define GOT_ERR_SEND_TAG_EXISTS 140 +#define GOT_ERR_NOT_MERGING 141 +#define GOT_ERR_MERGE_OUT_OF_DATE 142 +#define GOT_ERR_MERGE_STAGED_PATHS 143 +#define GOT_ERR_MERGE_COMMIT_OUT_OF_DATE 143 +#define GOT_ERR_MERGE_BUSY 144 +#define GOT_ERR_MERGE_PATH 145 static const struct got_error { int code; @@ -320,6 +326,19 @@ static const struct got_error { { GOT_ERR_CAPA_DELETE_REFS, "server cannot delete references" }, { GOT_ERR_SEND_DELETE_REF, "reference cannot be deleted" }, { GOT_ERR_SEND_TAG_EXISTS, "tag already exists on server" }, + { GOT_ERR_NOT_MERGING, "merge operation not in progress" }, + { GOT_ERR_MERGE_OUT_OF_DATE, "work tree must be updated before it " + "can be used to merge a branch" }, + { GOT_ERR_MERGE_STAGED_PATHS, "work tree contains files with staged " + "changes; these changes must be unstaged before merging can " + "proceed" }, + { GOT_ERR_MERGE_COMMIT_OUT_OF_DATE, "merging cannot proceed because " + "the work tree is no longer up-to-date; merge must be aborted " + "and retried" }, + { GOT_ERR_MERGE_BUSY,"a merge operation is in progress in this " + "work tree and must be continued or aborted first" }, + { GOT_ERR_MERGE_PATH, "cannot merge branch which contains " + "changes outside of this work tree's path prefix" }, }; /* blob - d9ae4873db1e494d8663703aea78cfee34d5f440 blob + 0dcba98cb99b5350473eff9a91f3600e24b7ff91 --- include/got_worktree.h +++ include/got_worktree.h @@ -436,8 +436,74 @@ const struct got_error *got_worktree_integrate_continu const struct got_error *got_worktree_integrate_abort(struct got_worktree *, struct got_fileindex *, struct got_repository *, struct got_reference *, struct got_reference *); + +/* Postpone the merge operation. Should be called after a merge conflict. */ +const struct got_error *got_worktree_merge_postpone(struct got_worktree *, + struct got_fileindex *); + +/* Merge changes from the merge source branch into the worktree. */ +const struct got_error * +got_worktree_merge_branch(struct got_worktree *worktree, + struct got_fileindex *fileindex, + struct got_object_id *yca_commit_id, + struct got_object_id *branch_tip, + struct got_repository *repo, got_worktree_checkout_cb progress_cb, + void *progress_arg, got_cancel_cb cancel_cb, void *cancel_arg); + +/* Attempt to commit merged changes. */ +const struct got_error * +got_worktree_merge_commit(struct got_object_id **new_commit_id, + struct got_worktree *worktree, struct got_fileindex *fileindex, + const char *author, const char *committer, int allow_bad_symlinks, + struct got_object_id *branch_tip, const char *branch_name, + struct got_repository *repo); + +/* + * Complete the merge operation. + * This should be called once changes have been successfully committed. + */ +const struct got_error *got_worktree_merge_complete( + struct got_worktree *worktree, struct got_fileindex *fileindex, + struct got_repository *repo); + +/* Check whether a merge operation is in progress. */ +const struct got_error *got_worktree_merge_in_progress(int *, + struct got_worktree *, struct got_repository *); + +/* + * Prepare for merging a branch into the work tree's current branch. + * This function creates a reference to the branch being merged, and to + * this branch's current tip commit, in the "got/worktree/merge/" namespace. + * These references are used to keep track of merge operation state and are + * used as input and/or output arguments with other merge-related functions. + * The function also returns a pointer to a fileindex which must be + * passed back to other merge-related functions. + */ +const struct got_error *got_worktree_merge_prepare(struct got_fileindex **, + struct got_worktree *, struct got_reference *, struct got_repository *); /* + * Continue an interrupted merge operation. + * This function returns name of the branch being merged, and the ID of the + * tip commit being merged. + * This function should be called before either resuming or aborting a + * merge operation. + * The function also returns a pointer to a fileindex which must be + * passed back to other merge-related functions. + */ +const struct got_error *got_worktree_merge_continue(char **, + struct got_object_id **, struct got_fileindex **, + struct got_worktree *, struct got_repository *); + +/* + * Abort the current rebase operation. + * Report reverted files via the specified progress callback. + */ +const struct got_error *got_worktree_merge_abort(struct got_worktree *, + struct got_fileindex *, struct got_repository *, + got_worktree_checkout_cb, void *); + +/* * Stage the specified paths for commit. * If the patch callback is not NULL, call it to select patch hunks for * staging. Otherwise, stage the full file content found at each path. blob - 651bcc8f757194fa0d5259093255234ac99ee09d blob + a35884552cf66d678f0db623e1dd92d976a7014e --- lib/got_lib_worktree.h +++ lib/got_lib_worktree.h @@ -102,3 +102,9 @@ const struct got_error *got_worktree_get_base_ref_name /* Reference pointing at the ID of the current commit being edited. */ #define GOT_WORKTREE_HISTEDIT_COMMIT_REF_PREFIX \ "refs/got/worktree/histedit/commit" + +/* Symbolic reference pointing at the name of the merge source branch. */ +#define GOT_WORKTREE_MERGE_BRANCH_REF_PREFIX "refs/got/worktree/merge/branch" + +/* Reference pointing at the ID of the merge source branches's tip commit. */ +#define GOT_WORKTREE_MERGE_COMMIT_REF_PREFIX "refs/got/worktree/merge/commit" blob - 4e86317f1d38e8899d2d7bcd6a35a17c6d03ae53 blob + fb842d05b1ddc1f853d2c8d63d5b6db4793a3a55 --- lib/object_create.c +++ lib/object_create.c @@ -491,11 +491,11 @@ got_object_commit_create(struct got_object_id **id, } if (parent_ids) { + free(id_str); + id_str = NULL; STAILQ_FOREACH(qid, parent_ids, entry) { char *parent_str = NULL; - free(id_str); - err = got_object_id_str(&id_str, qid->id); if (err) goto done; @@ -513,6 +513,8 @@ got_object_commit_create(struct got_object_id **id, goto done; } free(parent_str); + free(id_str); + id_str = NULL; } } @@ -569,6 +571,7 @@ got_object_commit_create(struct got_object_id **id, err = create_object_file(*id, commitfile, repo); done: + free(id_str); free(msg0); free(header); free(tree_str); blob - 264a17dbcf80c208a1facb0bd41b8a1ca780b455 blob + 6dd080eb395b1890c2edae59e78e5e795e2e0262 --- lib/worktree.c +++ lib/worktree.c @@ -2376,8 +2376,22 @@ got_worktree_get_histedit_script_path(char **path, return got_error_from_errno("asprintf"); } return NULL; +} + +static const struct got_error * +get_merge_branch_ref_name(char **refname, struct got_worktree *worktree) +{ + return get_ref_name(refname, worktree, + GOT_WORKTREE_MERGE_BRANCH_REF_PREFIX); } +static const struct got_error * +get_merge_commit_ref_name(char **refname, struct got_worktree *worktree) +{ + return get_ref_name(refname, worktree, + GOT_WORKTREE_MERGE_COMMIT_REF_PREFIX); +} + /* * Prevent Git's garbage collector from deleting our base commit by * setting a reference to our base commit's ID. @@ -3275,7 +3289,8 @@ got_worktree_merge_files(struct got_worktree *worktree goto done; err = merge_files(worktree, fileindex, fileindex_path, commit_id1, - commit_id2, repo, progress_cb, progress_arg, cancel_cb, cancel_arg); + commit_id2, repo, progress_cb, progress_arg, + cancel_cb, cancel_arg); done: if (fileindex) got_fileindex_free(fileindex); @@ -4409,6 +4424,7 @@ struct revert_file_args { got_worktree_patch_cb patch_cb; void *patch_arg; struct got_repository *repo; + int unlink_added_files; }; static const struct got_error * @@ -4689,6 +4705,19 @@ revert_file(void *arg, unsigned char status, unsigned if (err) goto done; got_fileindex_entry_remove(a->fileindex, ie); + if (a->unlink_added_files) { + if (asprintf(&ondisk_path, "%s/%s", + got_worktree_get_root_path(a->worktree), + relpath) == -1) { + err = got_error_from_errno("asprintf"); + goto done; + } + if (unlink(ondisk_path) == -1) { + err = got_error_from_errno2("unlink", + ondisk_path); + break; + } + } break; case GOT_STATUS_DELETE: if (a->patch_cb) { @@ -4826,6 +4855,7 @@ got_worktree_revert(struct got_worktree *worktree, rfa.patch_cb = patch_cb; rfa.patch_arg = patch_arg; rfa.repo = repo; + rfa.unlink_added_files = 0; TAILQ_FOREACH(pe, paths, entry) { err = worktree_status(worktree, pe->path, fileindex, repo, revert_file, &rfa, NULL, NULL, 0, 0); @@ -5581,7 +5611,9 @@ done: const struct got_error * commit_worktree(struct got_object_id **new_commit_id, struct got_pathlist_head *commitable_paths, - struct got_object_id *head_commit_id, struct got_worktree *worktree, + struct got_object_id *head_commit_id, + struct got_object_id *parent_id2, + struct got_worktree *worktree, const char *author, const char *committer, got_worktree_commit_msg_cb commit_msg_cb, void *commit_arg, got_worktree_status_cb status_cb, void *status_arg, @@ -5595,7 +5627,7 @@ commit_worktree(struct got_object_id **new_commit_id, struct got_object_id *head_commit_id2 = NULL; struct got_tree_object *head_tree = NULL; struct got_object_id *new_tree_id = NULL; - int nentries; + int nentries, nparents = 0; struct got_object_id_queue parent_ids; struct got_object_qid *pid = NULL; char *logmsg = NULL; @@ -5659,9 +5691,16 @@ commit_worktree(struct got_object_id **new_commit_id, if (err) goto done; STAILQ_INSERT_TAIL(&parent_ids, pid, entry); + nparents++; + if (parent_id2) { + err = got_object_qid_alloc(&pid, parent_id2); + if (err) + goto done; + STAILQ_INSERT_TAIL(&parent_ids, pid, entry); + nparents++; + } err = got_object_commit_create(new_commit_id, new_tree_id, &parent_ids, - 1, author, time(NULL), committer, time(NULL), logmsg, repo); - got_object_qid_free(pid); + nparents, author, time(NULL), committer, time(NULL), logmsg, repo); if (logmsg != NULL) free(logmsg); if (err) @@ -5700,6 +5739,7 @@ commit_worktree(struct got_object_id **new_commit_id, if (err) goto done; done: + got_object_id_queue_free(&parent_ids); if (head_tree) got_object_tree_close(head_tree); if (head_commit) @@ -5860,7 +5900,7 @@ got_worktree_commit(struct got_object_id **new_commit_ } err = commit_worktree(new_commit_id, &commitable_paths, - head_commit_id, worktree, author, committer, + head_commit_id, NULL, worktree, author, committer, commit_msg_cb, commit_arg, status_cb, status_arg, repo); if (err) goto done; @@ -6442,7 +6482,7 @@ rebase_commit(struct got_object_id **new_commit_id, /* NB: commit_worktree will call free(logmsg) */ err = commit_worktree(new_commit_id, &commitable_paths, head_commit_id, - worktree, got_object_commit_get_author(orig_commit), + NULL, worktree, got_object_commit_get_author(orig_commit), got_object_commit_get_committer(orig_commit), collect_rebase_commit_msg, logmsg, rebase_status, NULL, repo); if (err) @@ -6766,6 +6806,7 @@ got_worktree_rebase_abort(struct got_worktree *worktre rfa.patch_cb = NULL; rfa.patch_arg = NULL; rfa.repo = repo; + rfa.unlink_added_files = 0; err = worktree_status(worktree, "", fileindex, repo, revert_file, &rfa, NULL, NULL, 0, 0); if (err) @@ -7119,6 +7160,7 @@ got_worktree_histedit_abort(struct got_worktree *workt rfa.patch_cb = NULL; rfa.patch_arg = NULL; rfa.repo = repo; + rfa.unlink_added_files = 0; err = worktree_status(worktree, "", fileindex, repo, revert_file, &rfa, NULL, NULL, 0, 0); if (err) @@ -7369,10 +7411,493 @@ got_worktree_integrate_abort(struct got_worktree *work if (unlockerr && err == NULL) err = unlockerr; got_ref_close(base_branch_ref); + + return err; +} + +const struct got_error * +got_worktree_merge_postpone(struct got_worktree *worktree, + struct got_fileindex *fileindex) +{ + const struct got_error *err, *sync_err; + char *fileindex_path = NULL; + + err = get_fileindex_path(&fileindex_path, worktree); + if (err) + goto done; + sync_err = sync_fileindex(fileindex, fileindex_path); + + err = lock_worktree(worktree, LOCK_SH); + if (sync_err && err == NULL) + err = sync_err; +done: + got_fileindex_free(fileindex); + free(fileindex_path); return err; } +static const struct got_error * +delete_merge_refs(struct got_worktree *worktree, struct got_repository *repo) +{ + const struct got_error *err; + char *branch_refname = NULL, *commit_refname = NULL; + + err = get_merge_branch_ref_name(&branch_refname, worktree); + if (err) + goto done; + err = delete_ref(branch_refname, repo); + if (err) + goto done; + + err = get_merge_commit_ref_name(&commit_refname, worktree); + if (err) + goto done; + err = delete_ref(commit_refname, repo); + if (err) + goto done; + +done: + free(branch_refname); + free(commit_refname); + return err; +} + +struct merge_commit_msg_arg { + struct got_worktree *worktree; + const char *branch_name; +}; + +static const struct got_error * +merge_commit_msg_cb(struct got_pathlist_head *commitable_paths, char **logmsg, + void *arg) +{ + struct merge_commit_msg_arg *a = arg; + + if (asprintf(logmsg, "merge %s into %s\n", a->branch_name, + got_worktree_get_head_ref_name(a->worktree)) == -1) + return got_error_from_errno("asprintf"); + + return NULL; +} + +static const struct got_error * +merge_status_cb(void *arg, unsigned char status, unsigned char staged_status, + const char *path, struct got_object_id *blob_id, + struct got_object_id *staged_blob_id, struct got_object_id *commit_id, + int dirfd, const char *de_name) +{ + return NULL; +} + +const struct got_error * +got_worktree_merge_branch(struct got_worktree *worktree, + struct got_fileindex *fileindex, + struct got_object_id *yca_commit_id, + struct got_object_id *branch_tip, + struct got_repository *repo, got_worktree_checkout_cb progress_cb, + void *progress_arg, got_cancel_cb cancel_cb, void *cancel_arg) +{ + const struct got_error *err; + char *fileindex_path = NULL; + + err = get_fileindex_path(&fileindex_path, worktree); + if (err) + goto done; + + err = got_fileindex_for_each_entry_safe(fileindex, check_mixed_commits, + worktree); + if (err) + goto done; + + err = merge_files(worktree, fileindex, fileindex_path, yca_commit_id, + branch_tip, repo, progress_cb, progress_arg, + cancel_cb, cancel_arg); +done: + free(fileindex_path); + return err; +} + +const struct got_error * +got_worktree_merge_commit(struct got_object_id **new_commit_id, + struct got_worktree *worktree, struct got_fileindex *fileindex, + const char *author, const char *committer, int allow_bad_symlinks, + struct got_object_id *branch_tip, const char *branch_name, + struct got_repository *repo) +{ + const struct got_error *err = NULL, *sync_err; + struct got_pathlist_head commitable_paths; + struct collect_commitables_arg cc_arg; + struct got_pathlist_entry *pe; + struct got_reference *head_ref = NULL; + struct got_object_id *head_commit_id = NULL; + int have_staged_files = 0; + struct merge_commit_msg_arg mcm_arg; + char *fileindex_path = NULL; + + *new_commit_id = NULL; + + TAILQ_INIT(&commitable_paths); + + err = get_fileindex_path(&fileindex_path, worktree); + if (err) + goto done; + + err = got_ref_open(&head_ref, repo, worktree->head_ref_name, 0); + if (err) + goto done; + + err = got_ref_resolve(&head_commit_id, repo, head_ref); + if (err) + goto done; + + err = got_fileindex_for_each_entry_safe(fileindex, check_staged_file, + &have_staged_files); + if (err && err->code != GOT_ERR_CANCELLED) + goto done; + if (have_staged_files) { + err = got_error(GOT_ERR_MERGE_STAGED_PATHS); + goto done; + } + + cc_arg.commitable_paths = &commitable_paths; + cc_arg.worktree = worktree; + cc_arg.fileindex = fileindex; + cc_arg.repo = repo; + cc_arg.have_staged_files = have_staged_files; + cc_arg.allow_bad_symlinks = allow_bad_symlinks; + err = worktree_status(worktree, "", fileindex, repo, + collect_commitables, &cc_arg, NULL, NULL, 0, 0); + if (err) + goto done; + + if (TAILQ_EMPTY(&commitable_paths)) { + err = got_error_fmt(GOT_ERR_COMMIT_NO_CHANGES, + "merge of %s cannot proceed", branch_name); + goto done; + } + + TAILQ_FOREACH(pe, &commitable_paths, entry) { + struct got_commitable *ct = pe->data; + const char *ct_path = ct->in_repo_path; + + while (ct_path[0] == '/') + ct_path++; + err = check_out_of_date(ct_path, ct->status, + ct->staged_status, ct->base_blob_id, ct->base_commit_id, + head_commit_id, repo, GOT_ERR_MERGE_COMMIT_OUT_OF_DATE); + if (err) + goto done; + + } + + mcm_arg.worktree = worktree; + mcm_arg.branch_name = branch_name; + err = commit_worktree(new_commit_id, &commitable_paths, + head_commit_id, branch_tip, worktree, author, committer, + merge_commit_msg_cb, &mcm_arg, merge_status_cb, NULL, repo); + if (err) + goto done; + + err = update_fileindex_after_commit(worktree, &commitable_paths, + *new_commit_id, fileindex, have_staged_files); + sync_err = sync_fileindex(fileindex, fileindex_path); + if (sync_err && err == NULL) + err = sync_err; +done: + TAILQ_FOREACH(pe, &commitable_paths, entry) { + struct got_commitable *ct = pe->data; + free_commitable(ct); + } + got_pathlist_free(&commitable_paths); + free(fileindex_path); + return err; +} + +const struct got_error * +got_worktree_merge_complete(struct got_worktree *worktree, + struct got_fileindex *fileindex, struct got_repository *repo) +{ + const struct got_error *err, *unlockerr, *sync_err; + char *fileindex_path = NULL; + + err = delete_merge_refs(worktree, repo); + if (err) + goto done; + + err = get_fileindex_path(&fileindex_path, worktree); + if (err) + goto done; + err = bump_base_commit_id_everywhere(worktree, fileindex, NULL, NULL); + sync_err = sync_fileindex(fileindex, fileindex_path); + if (sync_err && err == NULL) + err = sync_err; +done: + got_fileindex_free(fileindex); + free(fileindex_path); + unlockerr = lock_worktree(worktree, LOCK_SH); + if (unlockerr && err == NULL) + err = unlockerr; + return err; +} + +const struct got_error * +got_worktree_merge_in_progress(int *in_progress, struct got_worktree *worktree, + struct got_repository *repo) +{ + const struct got_error *err; + char *branch_refname = NULL; + struct got_reference *branch_ref = NULL; + + *in_progress = 0; + + err = get_merge_branch_ref_name(&branch_refname, worktree); + if (err) + return err; + err = got_ref_open(&branch_ref, repo, branch_refname, 0); + if (err) { + if (err->code != GOT_ERR_NOT_REF) + return err; + } else + *in_progress = 1; + + return NULL; +} + +const struct got_error *got_worktree_merge_prepare( + struct got_fileindex **fileindex, struct got_worktree *worktree, + struct got_reference *branch, struct got_repository *repo) +{ + const struct got_error *err = NULL; + char *fileindex_path = NULL; + char *branch_refname = NULL, *commit_refname = NULL; + struct got_reference *wt_branch = NULL, *branch_ref = NULL; + struct got_reference *commit_ref = NULL; + struct got_object_id *branch_tip = NULL, *wt_branch_tip = NULL; + struct check_rebase_ok_arg ok_arg; + + *fileindex = NULL; + + err = lock_worktree(worktree, LOCK_EX); + if (err) + return err; + + err = open_fileindex(fileindex, &fileindex_path, worktree); + if (err) + goto done; + + /* Preconditions are the same as for rebase. */ + ok_arg.worktree = worktree; + ok_arg.repo = repo; + err = got_fileindex_for_each_entry_safe(*fileindex, check_rebase_ok, + &ok_arg); + if (err) + goto done; + + err = get_merge_branch_ref_name(&branch_refname, worktree); + if (err) + return err; + + err = get_merge_commit_ref_name(&commit_refname, worktree); + if (err) + return err; + + err = got_ref_open(&wt_branch, repo, worktree->head_ref_name, + 0); + if (err) + goto done; + + err = got_ref_resolve(&wt_branch_tip, repo, wt_branch); + if (err) + goto done; + + if (got_object_id_cmp(worktree->base_commit_id, wt_branch_tip) != 0) { + err = got_error(GOT_ERR_MERGE_OUT_OF_DATE); + goto done; + } + + err = got_ref_resolve(&branch_tip, repo, branch); + if (err) + goto done; + + err = got_ref_alloc_symref(&branch_ref, branch_refname, branch); + if (err) + goto done; + err = got_ref_write(branch_ref, repo); + if (err) + goto done; + + err = got_ref_alloc(&commit_ref, commit_refname, branch_tip); + if (err) + goto done; + err = got_ref_write(commit_ref, repo); + if (err) + goto done; + +done: + free(branch_refname); + free(commit_refname); + free(fileindex_path); + if (branch_ref) + got_ref_close(branch_ref); + if (commit_ref) + got_ref_close(commit_ref); + if (wt_branch) + got_ref_close(wt_branch); + free(wt_branch_tip); + if (err) { + if (*fileindex) { + got_fileindex_free(*fileindex); + *fileindex = NULL; + } + lock_worktree(worktree, LOCK_SH); + } + return err; +} + +const struct got_error * +got_worktree_merge_continue(char **branch_name, + struct got_object_id **branch_tip, struct got_fileindex **fileindex, + struct got_worktree *worktree, struct got_repository *repo) +{ + const struct got_error *err; + char *commit_refname = NULL, *branch_refname = NULL; + struct got_reference *commit_ref = NULL, *branch_ref = NULL; + char *fileindex_path = NULL; + int have_staged_files = 0; + + *branch_name = NULL; + *branch_tip = NULL; + *fileindex = NULL; + + err = lock_worktree(worktree, LOCK_EX); + if (err) + return err; + + err = open_fileindex(fileindex, &fileindex_path, worktree); + if (err) + goto done; + + err = got_fileindex_for_each_entry_safe(*fileindex, check_staged_file, + &have_staged_files); + if (err && err->code != GOT_ERR_CANCELLED) + goto done; + if (have_staged_files) { + err = got_error(GOT_ERR_STAGED_PATHS); + goto done; + } + + err = get_merge_branch_ref_name(&branch_refname, worktree); + if (err) + goto done; + + err = get_merge_commit_ref_name(&commit_refname, worktree); + if (err) + goto done; + + err = got_ref_open(&branch_ref, repo, branch_refname, 0); + if (err) + goto done; + + if (!got_ref_is_symbolic(branch_ref)) { + err = got_error_fmt(GOT_ERR_BAD_REF_TYPE, + "%s is not a symbolic reference", + got_ref_get_name(branch_ref)); + goto done; + } + *branch_name = strdup(got_ref_get_symref_target(branch_ref)); + if (*branch_name == NULL) { + err = got_error_from_errno("strdup"); + goto done; + } + + err = got_ref_open(&commit_ref, repo, commit_refname, 0); + if (err) + goto done; + + err = got_ref_resolve(branch_tip, repo, commit_ref); + if (err) + goto done; +done: + free(commit_refname); + free(branch_refname); + free(fileindex_path); + if (commit_ref) + got_ref_close(commit_ref); + if (branch_ref) + got_ref_close(branch_ref); + if (err) { + if (*branch_name) { + free(*branch_name); + *branch_name = NULL; + } + free(*branch_tip); + *branch_tip = NULL; + if (*fileindex) { + got_fileindex_free(*fileindex); + *fileindex = NULL; + } + lock_worktree(worktree, LOCK_SH); + } + return err; +} + +const struct got_error * +got_worktree_merge_abort(struct got_worktree *worktree, + struct got_fileindex *fileindex, struct got_repository *repo, + got_worktree_checkout_cb progress_cb, void *progress_arg) +{ + const struct got_error *err, *unlockerr, *sync_err; + struct got_object_id *commit_id = NULL; + char *fileindex_path = NULL; + struct revert_file_args rfa; + struct got_object_id *tree_id = NULL; + + err = got_object_id_by_path(&tree_id, repo, + worktree->base_commit_id, worktree->path_prefix); + if (err) + goto done; + + err = delete_merge_refs(worktree, repo); + if (err) + goto done; + + err = get_fileindex_path(&fileindex_path, worktree); + if (err) + goto done; + + rfa.worktree = worktree; + rfa.fileindex = fileindex; + rfa.progress_cb = progress_cb; + rfa.progress_arg = progress_arg; + rfa.patch_cb = NULL; + rfa.patch_arg = NULL; + rfa.repo = repo; + rfa.unlink_added_files = 1; + err = worktree_status(worktree, "", fileindex, repo, + revert_file, &rfa, NULL, NULL, 0, 0); + if (err) + goto sync; + + err = checkout_files(worktree, fileindex, "", tree_id, NULL, + repo, progress_cb, progress_arg, NULL, NULL); +sync: + sync_err = sync_fileindex(fileindex, fileindex_path); + if (sync_err && err == NULL) + err = sync_err; +done: + free(tree_id); + free(commit_id); + if (fileindex) + got_fileindex_free(fileindex); + free(fileindex_path); + + unlockerr = lock_worktree(worktree, LOCK_SH); + if (unlockerr && err == NULL) + err = unlockerr; + return err; +} + struct check_stage_ok_arg { struct got_object_id *head_commit_id; struct got_worktree *worktree; blob - 68314f1e77bb35025aeb5401ca14f24b773350a5 blob + 54055c09da65df95bc8676121ad774abaed5f07c --- regress/cmdline/Makefile +++ regress/cmdline/Makefile @@ -1,6 +1,6 @@ REGRESS_TARGETS=checkout update status log add rm diff blame branch tag \ ref commit revert cherrypick backout rebase import histedit \ - integrate stage unstage cat clone fetch tree pack cleanup + integrate merge stage unstage cat clone fetch tree pack cleanup NOOBJ=Yes GOT_TEST_ROOT=/tmp @@ -62,6 +62,9 @@ histedit: integrate: ./integrate.sh -q -r "$(GOT_TEST_ROOT)" +merge: + ./merge.sh -q -r "$(GOT_TEST_ROOT)" + stage: ./stage.sh -q -r "$(GOT_TEST_ROOT)" blob - /dev/null blob + a23abe444913229eb71b51e04a842fa7ffb9b4a0 (mode 755) --- /dev/null +++ regress/cmdline/merge.sh @@ -0,0 +1,979 @@ +#!/bin/sh +# +# Copyright (c) 2021 Stefan Sperling +# +# 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