commit - ac4dc26386a91b9c488d78fd7065780e1eaf33d7
commit + 10604dce6e625d11974fb5491598bbb54069e5d3
blob - 7908e0d4a3f37ad292df5a94592a0e75437876a3
blob + 55846aa7e98871dabc52f2ac079d93817150bc11
--- got/got.1
+++ got/got.1
.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
__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);
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 *[]);
{ "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, "" },
int conflicts;
int obstructed;
int not_updated;
+ int missing;
int verbosity;
};
upa->obstructed++;
if (status == GOT_STATUS_CANNOT_UPDATE)
upa->not_updated++;
+ if (status == GOT_STATUS_MISSING)
+ upa->missing++;
while (path[0] == '/')
path++;
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;
}
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;
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);
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;
}
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;
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;
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;
}
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;
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;
}
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;
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;
}
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
#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;
{ 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
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
/* 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
}
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;
goto done;
}
free(parent_str);
+ free(id_str);
+ id_str = NULL;
}
}
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
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.
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);
got_worktree_patch_cb patch_cb;
void *patch_arg;
struct got_repository *repo;
+ int unlink_added_files;
};
static const struct got_error *
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) {
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);
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,
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;
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)
if (err)
goto done;
done:
+ got_object_id_queue_free(&parent_ids);
if (head_tree)
got_object_tree_close(head_tree);
if (head_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;
/* 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)
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)
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)
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
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
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
+#!/bin/sh
+#
+# Copyright (c) 2021 Stefan Sperling <stsp@openbsd.org>
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+. ./common.sh
+
+test_merge_basic() {
+ local testroot=`test_init merge_basic`
+ local commit0=`git_show_head $testroot/repo`
+ local commit0_author_time=`git_show_author_time $testroot/repo`
+
+ (cd $testroot/repo && git checkout -q -b newbranch)
+ echo "modified delta on branch" > $testroot/repo/gamma/delta
+ git_commit $testroot/repo -m "committing to delta on newbranch"
+ local branch_commit0=`git_show_branch_head $testroot/repo newbranch`
+
+ echo "modified alpha on branch" > $testroot/repo/alpha
+ git_commit $testroot/repo -m "committing to alpha on newbranch"
+ local branch_commit1=`git_show_branch_head $testroot/repo newbranch`
+ (cd $testroot/repo && git rm -q beta)
+ git_commit $testroot/repo -m "removing beta on newbranch"
+ local branch_commit2=`git_show_branch_head $testroot/repo newbranch`
+ echo "new file on branch" > $testroot/repo/epsilon/new
+ (cd $testroot/repo && git add epsilon/new)
+ git_commit $testroot/repo -m "adding new file on newbranch"
+ local branch_commit3=`git_show_branch_head $testroot/repo newbranch`
+ (cd $testroot/repo && ln -s alpha symlink && git add symlink)
+ git_commit $testroot/repo -m "adding symlink on newbranch"
+ local branch_commit4=`git_show_branch_head $testroot/repo newbranch`
+
+ got checkout -b master $testroot/repo $testroot/wt > /dev/null
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ echo "got checkout failed unexpectedly" >&2
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ # need a divergant commit on the main branch for 'got merge'
+ (cd $testroot/wt && got merge newbranch \
+ > $testroot/stdout 2> $testroot/stderr)
+ ret="$?"
+ if [ "$ret" == "0" ]; then
+ echo "got merge succeeded unexpectedly" >&2
+ test_done "$testroot" "1"
+ return 1
+ fi
+ echo -n "got: cannot create a merge commit because " \
+ > $testroot/stderr.expected
+ echo -n "refs/heads/newbranch is based on refs/heads/master; " \
+ >> $testroot/stderr.expected
+ echo -n "refs/heads/newbranch can be integrated with " \
+ >> $testroot/stderr.expected
+ echo "'got integrate' instead" >> $testroot/stderr.expected
+ cmp -s $testroot/stderr.expected $testroot/stderr
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/stderr.expected $testroot/stderr
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ # create the required dirvergant commit
+ (cd $testroot/repo && git checkout -q master)
+ echo "modified zeta on master" > $testroot/repo/epsilon/zeta
+ git_commit $testroot/repo -m "committing to zeta on master"
+ local master_commit=`git_show_head $testroot/repo`
+
+ # need an up-to-date work tree for 'got merge'
+ (cd $testroot/wt && got merge newbranch \
+ > $testroot/stdout 2> $testroot/stderr)
+ ret="$?"
+ if [ "$ret" == "0" ]; then
+ echo "got merge succeeded unexpectedly" >&2
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+ echo -n "got: work tree must be updated before it can be used " \
+ > $testroot/stderr.expected
+ echo "to merge a branch" >> $testroot/stderr.expected
+ cmp -s $testroot/stderr.expected $testroot/stderr
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/stderr.expected $testroot/stderr
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ (cd $testroot/wt && got update > /dev/null)
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ echo "got update failed unexpectedly" >&2
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ # must not use a mixed-commit work tree with 'got merge'
+ (cd $testroot/wt && got update -c $commit0 alpha > /dev/null)
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ echo "got update failed unexpectedly" >&2
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+ (cd $testroot/wt && got merge newbranch \
+ > $testroot/stdout 2> $testroot/stderr)
+ ret="$?"
+ if [ "$ret" == "0" ]; then
+ echo "got merge succeeded unexpectedly" >&2
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+ echo -n "got: work tree contains files from multiple base commits; " \
+ > $testroot/stderr.expected
+ echo "the entire work tree must be updated first" \
+ >> $testroot/stderr.expected
+ cmp -s $testroot/stderr.expected $testroot/stderr
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/stderr.expected $testroot/stderr
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ (cd $testroot/wt && got update > /dev/null)
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ echo "got update failed unexpectedly" >&2
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ # must not have staged files with 'got merge'
+ echo "modified file alpha" > $testroot/wt/alpha
+ (cd $testroot/wt && got stage alpha > /dev/null)
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ echo "got stage failed unexpectedly" >&2
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+ (cd $testroot/wt && got merge newbranch \
+ > $testroot/stdout 2> $testroot/stderr)
+ ret="$?"
+ if [ "$ret" == "0" ]; then
+ echo "got merge succeeded unexpectedly" >&2
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+ echo "got: alpha: file is staged" > $testroot/stderr.expected
+ cmp -s $testroot/stderr.expected $testroot/stderr
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/stderr.expected $testroot/stderr
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+ (cd $testroot/wt && got unstage alpha > /dev/null)
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ echo "got unstage failed unexpectedly" >&2
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ # must not have local changes with 'got merge'
+ (cd $testroot/wt && got merge newbranch \
+ > $testroot/stdout 2> $testroot/stderr)
+ ret="$?"
+ if [ "$ret" == "0" ]; then
+ echo "got merge succeeded unexpectedly" >&2
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+ echo -n "got: work tree contains local changes; " \
+ > $testroot/stderr.expected
+ echo "these changes must be committed or reverted first" \
+ >> $testroot/stderr.expected
+ cmp -s $testroot/stderr.expected $testroot/stderr
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/stderr.expected $testroot/stderr
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ (cd $testroot/wt && got revert alpha > /dev/null)
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ echo "got revert failed unexpectedly" >&2
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ (cd $testroot/wt && got merge newbranch > $testroot/stdout)
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ echo "got merge failed unexpectedly" >&2
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ local merge_commit=`git_show_head $testroot/repo`
+
+ echo "G alpha" >> $testroot/stdout.expected
+ echo "D beta" >> $testroot/stdout.expected
+ echo "A epsilon/new" >> $testroot/stdout.expected
+ echo "G gamma/delta" >> $testroot/stdout.expected
+ echo "A symlink" >> $testroot/stdout.expected
+ echo -n "Merged refs/heads/newbranch into refs/heads/master: " \
+ >> $testroot/stdout.expected
+ echo $merge_commit >> $testroot/stdout.expected
+
+ cmp -s $testroot/stdout.expected $testroot/stdout
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/stdout.expected $testroot/stdout
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ echo "modified delta on branch" > $testroot/content.expected
+ cat $testroot/wt/gamma/delta > $testroot/content
+ cmp -s $testroot/content.expected $testroot/content
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/content.expected $testroot/content
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ echo "modified alpha on branch" > $testroot/content.expected
+ cat $testroot/wt/alpha > $testroot/content
+ cmp -s $testroot/content.expected $testroot/content
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/content.expected $testroot/content
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ if [ -e $testroot/wt/beta ]; then
+ echo "removed file beta still exists on disk" >&2
+ test_done "$testroot" "1"
+ return 1
+ fi
+
+ echo "new file on branch" > $testroot/content.expected
+ cat $testroot/wt/epsilon/new > $testroot/content
+ cmp -s $testroot/content.expected $testroot/content
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/content.expected $testroot/content
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ readlink $testroot/wt/symlink > $testroot/stdout
+ echo "alpha" > $testroot/stdout.expected
+ cmp -s $testroot/stdout.expected $testroot/stdout
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/stdout.expected $testroot/stdout
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ (cd $testroot/wt && got status > $testroot/stdout)
+
+ echo -n > $testroot/stdout.expected
+ cmp -s $testroot/stdout.expected $testroot/stdout
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/stdout.expected $testroot/stdout
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ (cd $testroot/wt && got log -l3 | grep ^commit > $testroot/stdout)
+ echo "commit $merge_commit (master)" > $testroot/stdout.expected
+ echo "commit $master_commit" >> $testroot/stdout.expected
+ echo "commit $commit0" >> $testroot/stdout.expected
+ cmp -s $testroot/stdout.expected $testroot/stdout
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/stdout.expected $testroot/stdout
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ (cd $testroot/wt && got update > $testroot/stdout)
+
+ echo 'Already up-to-date' > $testroot/stdout.expected
+ cmp -s $testroot/stdout.expected $testroot/stdout
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/stdout.expected $testroot/stdout
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ # We should have created a merge commit with two parents.
+ (cd $testroot/wt && got log -l1 | grep ^parent > $testroot/stdout)
+ echo "parent 1: $master_commit" > $testroot/stdout.expected
+ echo "parent 2: $branch_commit4" >> $testroot/stdout.expected
+ cmp -s $testroot/stdout.expected $testroot/stdout
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/stdout.expected $testroot/stdout
+ fi
+ test_done "$testroot" "$ret"
+}
+
+test_merge_continue() {
+ local testroot=`test_init merge_continue`
+ local commit0=`git_show_head $testroot/repo`
+ local commit0_author_time=`git_show_author_time $testroot/repo`
+
+ (cd $testroot/repo && git checkout -q -b newbranch)
+ echo "modified delta on branch" > $testroot/repo/gamma/delta
+ git_commit $testroot/repo -m "committing to delta on newbranch"
+ local branch_commit0=`git_show_branch_head $testroot/repo newbranch`
+
+ echo "modified alpha on branch" > $testroot/repo/alpha
+ git_commit $testroot/repo -m "committing to alpha on newbranch"
+ local branch_commit1=`git_show_branch_head $testroot/repo newbranch`
+ (cd $testroot/repo && git rm -q beta)
+ git_commit $testroot/repo -m "removing beta on newbranch"
+ local branch_commit2=`git_show_branch_head $testroot/repo newbranch`
+ echo "new file on branch" > $testroot/repo/epsilon/new
+ (cd $testroot/repo && git add epsilon/new)
+ git_commit $testroot/repo -m "adding new file on newbranch"
+ local branch_commit3=`git_show_branch_head $testroot/repo newbranch`
+
+ got checkout -b master $testroot/repo $testroot/wt > /dev/null
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ echo "got checkout failed unexpectedly" >&2
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ # create a conflicting commit
+ (cd $testroot/repo && git checkout -q master)
+ echo "modified alpha on master" > $testroot/repo/alpha
+ git_commit $testroot/repo -m "committing to alpha on master"
+ local master_commit=`git_show_head $testroot/repo`
+
+ # need an up-to-date work tree for 'got merge'
+ (cd $testroot/wt && got update > /dev/null)
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ echo "got update failed unexpectedly" >&2
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ (cd $testroot/wt && got merge newbranch \
+ > $testroot/stdout 2> $testroot/stderr)
+ ret="$?"
+ if [ "$ret" == "0" ]; then
+ echo "got merge succeeded unexpectedly" >&2
+ test_done "$testroot" "1"
+ return 1
+ fi
+
+ echo "C alpha" >> $testroot/stdout.expected
+ echo "D beta" >> $testroot/stdout.expected
+ echo "A epsilon/new" >> $testroot/stdout.expected
+ echo "G gamma/delta" >> $testroot/stdout.expected
+ echo "Files with new merge conflicts: 1" >> $testroot/stdout.expected
+ cmp -s $testroot/stdout.expected $testroot/stdout
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/stdout.expected $testroot/stdout
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ echo "got: conflicts must be resolved before merging can continue" \
+ > $testroot/stderr.expected
+ cmp -s $testroot/stderr.expected $testroot/stderr
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/stderr.expected $testroot/stderr
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ (cd $testroot/wt && got status > $testroot/stdout)
+
+ echo "C alpha" > $testroot/stdout.expected
+ echo "D beta" >> $testroot/stdout.expected
+ echo "A epsilon/new" >> $testroot/stdout.expected
+ echo "M gamma/delta" >> $testroot/stdout.expected
+ cmp -s $testroot/stdout.expected $testroot/stdout
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/stdout.expected $testroot/stdout
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ echo '<<<<<<<' > $testroot/content.expected
+ echo "modified alpha on master" >> $testroot/content.expected
+ echo "||||||| 3-way merge base: commit $commit0" \
+ >> $testroot/content.expected
+ echo "alpha" >> $testroot/content.expected
+ echo "=======" >> $testroot/content.expected
+ echo "modified alpha on branch" >> $testroot/content.expected
+ echo ">>>>>>> merged change: commit $branch_commit3" \
+ >> $testroot/content.expected
+ cat $testroot/wt/alpha > $testroot/content
+ cmp -s $testroot/content.expected $testroot/content
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/content.expected $testroot/content
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ # resolve the conflict
+ echo "modified alpha by both branches" > $testroot/wt/alpha
+
+ (cd $testroot/wt && got merge -c > $testroot/stdout)
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ echo "got merge failed unexpectedly" >&2
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ local merge_commit=`git_show_head $testroot/repo`
+
+ echo -n "Merged refs/heads/newbranch into refs/heads/master: " \
+ > $testroot/stdout.expected
+ echo $merge_commit >> $testroot/stdout.expected
+
+ cmp -s $testroot/stdout.expected $testroot/stdout
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/stdout.expected $testroot/stdout
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ echo "modified delta on branch" > $testroot/content.expected
+ cat $testroot/wt/gamma/delta > $testroot/content
+ cmp -s $testroot/content.expected $testroot/content
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/content.expected $testroot/content
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ echo "modified alpha by both branches" > $testroot/content.expected
+ cat $testroot/wt/alpha > $testroot/content
+ cmp -s $testroot/content.expected $testroot/content
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/content.expected $testroot/content
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ if [ -e $testroot/wt/beta ]; then
+ echo "removed file beta still exists on disk" >&2
+ test_done "$testroot" "1"
+ return 1
+ fi
+
+ echo "new file on branch" > $testroot/content.expected
+ cat $testroot/wt/epsilon/new > $testroot/content
+ cmp -s $testroot/content.expected $testroot/content
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/content.expected $testroot/content
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ (cd $testroot/wt && got status > $testroot/stdout)
+
+ echo -n > $testroot/stdout.expected
+ cmp -s $testroot/stdout.expected $testroot/stdout
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/stdout.expected $testroot/stdout
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ (cd $testroot/wt && got log -l3 | grep ^commit > $testroot/stdout)
+ echo "commit $merge_commit (master)" > $testroot/stdout.expected
+ echo "commit $master_commit" >> $testroot/stdout.expected
+ echo "commit $commit0" >> $testroot/stdout.expected
+ cmp -s $testroot/stdout.expected $testroot/stdout
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/stdout.expected $testroot/stdout
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ (cd $testroot/wt && got update > $testroot/stdout)
+
+ echo 'Already up-to-date' > $testroot/stdout.expected
+ cmp -s $testroot/stdout.expected $testroot/stdout
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/stdout.expected $testroot/stdout
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ # We should have created a merge commit with two parents.
+ (cd $testroot/wt && got log -l1 | grep ^parent > $testroot/stdout)
+ echo "parent 1: $master_commit" > $testroot/stdout.expected
+ echo "parent 2: $branch_commit3" >> $testroot/stdout.expected
+ cmp -s $testroot/stdout.expected $testroot/stdout
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/stdout.expected $testroot/stdout
+ fi
+ test_done "$testroot" "$ret"
+}
+
+test_merge_abort() {
+ local testroot=`test_init merge_abort`
+ local commit0=`git_show_head $testroot/repo`
+ local commit0_author_time=`git_show_author_time $testroot/repo`
+
+ (cd $testroot/repo && git checkout -q -b newbranch)
+ echo "modified delta on branch" > $testroot/repo/gamma/delta
+ git_commit $testroot/repo -m "committing to delta on newbranch"
+ local branch_commit0=`git_show_branch_head $testroot/repo newbranch`
+
+ echo "modified alpha on branch" > $testroot/repo/alpha
+ git_commit $testroot/repo -m "committing to alpha on newbranch"
+ local branch_commit1=`git_show_branch_head $testroot/repo newbranch`
+ (cd $testroot/repo && git rm -q beta)
+ git_commit $testroot/repo -m "removing beta on newbranch"
+ local branch_commit2=`git_show_branch_head $testroot/repo newbranch`
+ echo "new file on branch" > $testroot/repo/epsilon/new
+ (cd $testroot/repo && git add epsilon/new)
+ git_commit $testroot/repo -m "adding new file on newbranch"
+ local branch_commit3=`git_show_branch_head $testroot/repo newbranch`
+ (cd $testroot/repo && ln -s alpha symlink && git add symlink)
+ git_commit $testroot/repo -m "adding symlink on newbranch"
+ local branch_commit4=`git_show_branch_head $testroot/repo newbranch`
+
+ got checkout -b master $testroot/repo $testroot/wt > /dev/null
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ echo "got checkout failed unexpectedly" >&2
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ # create a conflicting commit
+ (cd $testroot/repo && git checkout -q master)
+ echo "modified alpha on master" > $testroot/repo/alpha
+ git_commit $testroot/repo -m "committing to alpha on master"
+ local master_commit=`git_show_head $testroot/repo`
+
+ # need an up-to-date work tree for 'got merge'
+ (cd $testroot/wt && got update > /dev/null)
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ echo "got update failed unexpectedly" >&2
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ (cd $testroot/wt && got merge newbranch \
+ > $testroot/stdout 2> $testroot/stderr)
+ ret="$?"
+ if [ "$ret" == "0" ]; then
+ echo "got merge succeeded unexpectedly" >&2
+ test_done "$testroot" "1"
+ return 1
+ fi
+
+ echo "C alpha" >> $testroot/stdout.expected
+ echo "D beta" >> $testroot/stdout.expected
+ echo "A epsilon/new" >> $testroot/stdout.expected
+ echo "G gamma/delta" >> $testroot/stdout.expected
+ echo "A symlink" >> $testroot/stdout.expected
+ echo "Files with new merge conflicts: 1" >> $testroot/stdout.expected
+ cmp -s $testroot/stdout.expected $testroot/stdout
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/stdout.expected $testroot/stdout
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ echo "got: conflicts must be resolved before merging can continue" \
+ > $testroot/stderr.expected
+ cmp -s $testroot/stderr.expected $testroot/stderr
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/stderr.expected $testroot/stderr
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ (cd $testroot/wt && got status > $testroot/stdout)
+
+ echo "C alpha" > $testroot/stdout.expected
+ echo "D beta" >> $testroot/stdout.expected
+ echo "A epsilon/new" >> $testroot/stdout.expected
+ echo "M gamma/delta" >> $testroot/stdout.expected
+ echo "A symlink" >> $testroot/stdout.expected
+ cmp -s $testroot/stdout.expected $testroot/stdout
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/stdout.expected $testroot/stdout
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ (cd $testroot/wt && got merge -a > $testroot/stdout)
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ echo "got merge failed unexpectedly" >&2
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ echo "R alpha" > $testroot/stdout.expected
+ echo "R beta" >> $testroot/stdout.expected
+ echo "R epsilon/new" >> $testroot/stdout.expected
+ echo "R gamma/delta" >> $testroot/stdout.expected
+ echo "R symlink" >> $testroot/stdout.expected
+ echo "Merge of refs/heads/newbranch aborted" \
+ >> $testroot/stdout.expected
+
+ cmp -s $testroot/stdout.expected $testroot/stdout
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/stdout.expected $testroot/stdout
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ echo "delta" > $testroot/content.expected
+ cat $testroot/wt/gamma/delta > $testroot/content
+ cmp -s $testroot/content.expected $testroot/content
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/content.expected $testroot/content
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ echo "modified alpha on master" > $testroot/content.expected
+ cat $testroot/wt/alpha > $testroot/content
+ cmp -s $testroot/content.expected $testroot/content
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/content.expected $testroot/content
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ echo "beta" > $testroot/content.expected
+ cat $testroot/wt/beta > $testroot/content
+ cmp -s $testroot/content.expected $testroot/content
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/content.expected $testroot/content
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ if [ -e $testroot/wt/epsilon/new ]; then
+ echo "reverted file epsilon/new still exists on disk" >&2
+ test_done "$testroot" "1"
+ return 1
+ fi
+
+ if [ -e $testroot/wt/symlink ]; then
+ echo "reverted symlink still exists on disk" >&2
+ test_done "$testroot" "1"
+ return 1
+ fi
+
+ (cd $testroot/wt && got status > $testroot/stdout)
+
+ echo -n "" > $testroot/stdout.expected
+ cmp -s $testroot/stdout.expected $testroot/stdout
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/stdout.expected $testroot/stdout
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ (cd $testroot/wt && got log -l3 | grep ^commit > $testroot/stdout)
+ echo "commit $master_commit (master)" > $testroot/stdout.expected
+ echo "commit $commit0" >> $testroot/stdout.expected
+ cmp -s $testroot/stdout.expected $testroot/stdout
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/stdout.expected $testroot/stdout
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ (cd $testroot/wt && got update > $testroot/stdout)
+
+ echo 'Already up-to-date' > $testroot/stdout.expected
+ cmp -s $testroot/stdout.expected $testroot/stdout
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/stdout.expected $testroot/stdout
+ fi
+ test_done "$testroot" "$ret"
+}
+
+test_merge_in_progress() {
+ local testroot=`test_init merge_in_progress`
+ local commit0=`git_show_head $testroot/repo`
+ local commit0_author_time=`git_show_author_time $testroot/repo`
+
+ (cd $testroot/repo && git checkout -q -b newbranch)
+ echo "modified alpha on branch" > $testroot/repo/alpha
+ git_commit $testroot/repo -m "committing to alpha on newbranch"
+ local branch_commit0=`git_show_branch_head $testroot/repo newbranch`
+
+ got checkout -b master $testroot/repo $testroot/wt > /dev/null
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ echo "got checkout failed unexpectedly" >&2
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ # create a conflicting commit
+ (cd $testroot/repo && git checkout -q master)
+ echo "modified alpha on master" > $testroot/repo/alpha
+ git_commit $testroot/repo -m "committing to alpha on master"
+ local master_commit=`git_show_head $testroot/repo`
+
+ # need an up-to-date work tree for 'got merge'
+ (cd $testroot/wt && got update > /dev/null)
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ echo "got update failed unexpectedly" >&2
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ (cd $testroot/wt && got merge newbranch \
+ > $testroot/stdout 2> $testroot/stderr)
+ ret="$?"
+ if [ "$ret" == "0" ]; then
+ echo "got merge succeeded unexpectedly" >&2
+ test_done "$testroot" "1"
+ return 1
+ fi
+
+ echo "C alpha" >> $testroot/stdout.expected
+ echo "Files with new merge conflicts: 1" >> $testroot/stdout.expected
+ cmp -s $testroot/stdout.expected $testroot/stdout
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/stdout.expected $testroot/stdout
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ echo "got: conflicts must be resolved before merging can continue" \
+ > $testroot/stderr.expected
+ cmp -s $testroot/stderr.expected $testroot/stderr
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/stderr.expected $testroot/stderr
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ (cd $testroot/wt && got status > $testroot/stdout)
+
+ echo "C alpha" > $testroot/stdout.expected
+ cmp -s $testroot/stdout.expected $testroot/stdout
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/stdout.expected $testroot/stdout
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ for cmd in update commit histedit "rebase newbranch" \
+ "integrate newbranch" "stage alpha"; do
+ (cd $testroot/wt && got $cmd > $testroot/stdout \
+ 2> $testroot/stderr)
+
+ echo -n > $testroot/stdout.expected
+ cmp -s $testroot/stdout.expected $testroot/stdout
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/stdout.expected $testroot/stdout
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ echo -n "got: a merge operation is in progress in this " \
+ > $testroot/stderr.expected
+ echo "work tree and must be continued or aborted first" \
+ >> $testroot/stderr.expected
+ cmp -s $testroot/stderr.expected $testroot/stderr
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/stderr.expected $testroot/stderr
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+ done
+
+ test_done "$testroot" "$ret"
+}
+
+test_merge_path_prefix() {
+ local testroot=`test_init merge_path_prefix`
+ local commit0=`git_show_head $testroot/repo`
+ local commit0_author_time=`git_show_author_time $testroot/repo`
+
+ (cd $testroot/repo && git checkout -q -b newbranch)
+ echo "modified alpha on branch" > $testroot/repo/alpha
+ git_commit $testroot/repo -m "committing to alpha on newbranch"
+ local branch_commit0=`git_show_branch_head $testroot/repo newbranch`
+
+ got checkout -p epsilon -b master $testroot/repo $testroot/wt \
+ > /dev/null
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ echo "got checkout failed unexpectedly" >&2
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ # create a conflicting commit
+ (cd $testroot/repo && git checkout -q master)
+ echo "modified alpha on master" > $testroot/repo/alpha
+ git_commit $testroot/repo -m "committing to alpha on master"
+ local master_commit=`git_show_head $testroot/repo`
+
+ # need an up-to-date work tree for 'got merge'
+ (cd $testroot/wt && got update > /dev/null)
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ echo "got update failed unexpectedly" >&2
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ (cd $testroot/wt && got merge newbranch \
+ > $testroot/stdout 2> $testroot/stderr)
+ ret="$?"
+ if [ "$ret" == "0" ]; then
+ echo "got merge succeeded unexpectedly" >&2
+ test_done "$testroot" "1"
+ return 1
+ fi
+
+ echo -n "got: cannot merge branch which contains changes outside " \
+ > $testroot/stderr.expected
+ echo "of this work tree's path prefix" >> $testroot/stderr.expected
+ cmp -s $testroot/stderr.expected $testroot/stderr
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/stderr.expected $testroot/stderr
+ fi
+ test_done "$testroot" "$ret"
+}
+
+test_merge_missing_file() {
+ local testroot=`test_init merge_missing_file`
+ local commit0=`git_show_head $testroot/repo`
+ local commit0_author_time=`git_show_author_time $testroot/repo`
+
+ (cd $testroot/repo && git checkout -q -b newbranch)
+ echo "modified alpha on branch" > $testroot/repo/alpha
+ echo "modified delta on branch" > $testroot/repo/gamma/delta
+ git_commit $testroot/repo -m "committing to alpha and delta"
+ local branch_commit0=`git_show_branch_head $testroot/repo newbranch`
+
+ got checkout -b master $testroot/repo $testroot/wt > /dev/null
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ echo "got checkout failed unexpectedly" >&2
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ # create a conflicting commit which renames alpha
+ (cd $testroot/repo && git checkout -q master)
+ (cd $testroot/repo && git mv alpha epsilon/alpha-moved)
+ git_commit $testroot/repo -m "moving alpha on master"
+ local master_commit=`git_show_head $testroot/repo`
+
+ # need an up-to-date work tree for 'got merge'
+ (cd $testroot/wt && got update > /dev/null)
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ echo "got update failed unexpectedly" >&2
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ (cd $testroot/wt && got merge newbranch \
+ > $testroot/stdout 2> $testroot/stderr)
+ ret="$?"
+ if [ "$ret" == "0" ]; then
+ echo "got merge succeeded unexpectedly" >&2
+ test_done "$testroot" "1"
+ return 1
+ fi
+
+ echo "! alpha" > $testroot/stdout.expected
+ echo "G gamma/delta" >> $testroot/stdout.expected
+ cmp -s $testroot/stdout.expected $testroot/stdout
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/stdout.expected $testroot/stdout
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ echo -n "got: changes destined for missing or obstructed files " \
+ > $testroot/stderr.expected
+ echo -n "were not yet merged and should be merged manually if " \
+ >> $testroot/stderr.expected
+ echo "required before the merge operation is continued" \
+ >> $testroot/stderr.expected
+ cmp -s $testroot/stderr.expected $testroot/stderr
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/stderr.expected $testroot/stderr
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ (cd $testroot/wt && got status > $testroot/stdout)
+
+ echo "M gamma/delta" > $testroot/stdout.expected
+ cmp -s $testroot/stdout.expected $testroot/stdout
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/stdout.expected $testroot/stdout
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ test_done "$testroot" "$ret"
+}
+
+test_parseargs "$@"
+run_test test_merge_basic
+run_test test_merge_continue
+run_test test_merge_abort
+run_test test_merge_in_progress
+run_test test_merge_path_prefix
+run_test test_merge_missing_file