Commit Diff


commit - aaa1358905e35eaa19a177bd11797d1a38d6cc03
commit + 234035bc7943e32aa92668438f4c0ba9c85e2f83
blob - 400c8b4665c0401ac0780eac38c93acbd16e73fa
blob + b8cd1a35b2873cdfc9028574e37bfc42cb5eb67a
--- got/got.1
+++ got/got.1
@@ -373,7 +373,44 @@ option,
 .Cm got commit
 opens a temporary file in an editor where a log message can be written.
 .El
+.It Cm cherrypick Ar commit
+Merge changes from a single
+.Ar commit
+into the work tree.
+The specified
+.Ar commit
+must be on a different branch than the work tree's base commit.
+The expected argument is a reference or a SHA1 hash which corresponds to
+a commit object.
+.Pp
+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 A Ta new file was added
 .El
+.Pp
+The merged changes will appear as local changes in the work tree, which
+may be viewed with
+.Cm got diff ,
+amended manually or with further
+.Cm got cherrypick
+comands,
+committed with
+.Cm got commit ,
+or discarded again with
+.Cm got revert .
+.Pp
+.Cm got cherrypick
+will refuse to run if certain preconditions are not met.
+If the work tree contains multiple base commits it must first be updated
+to a single base commit with
+.Cm got update .
+If the work tree already contains files with merge conflicts, these
+conflicts must be resolved first.
+.El
 .Sh ENVIRONMENT
 .Bl -tag -width GOT_AUTHOR
 .It Ev GOT_AUTHOR
blob - 0cb4b58a9016b7bc0462275d8ddff416e5848b05
blob + bf12b60a1d19c91b549c6e2ef57d10957d4ca0f6
--- got/got.c
+++ got/got.c
@@ -86,6 +86,7 @@ __dead static void	usage_add(void);
 __dead static void	usage_rm(void);
 __dead static void	usage_revert(void);
 __dead static void	usage_commit(void);
+__dead static void	usage_cherrypick(void);
 
 static const struct got_error*		cmd_checkout(int, char *[]);
 static const struct got_error*		cmd_update(int, char *[]);
@@ -99,6 +100,7 @@ static const struct got_error*		cmd_add(int, char *[])
 static const struct got_error*		cmd_rm(int, char *[]);
 static const struct got_error*		cmd_revert(int, char *[]);
 static const struct got_error*		cmd_commit(int, char *[]);
+static const struct got_error*		cmd_cherrypick(int, char *[]);
 
 static struct cmd got_commands[] = {
 	{ "checkout",	cmd_checkout,	usage_checkout,
@@ -125,6 +127,8 @@ static struct cmd got_commands[] = {
 	    "revert uncommitted changes" },
 	{ "commit",	cmd_commit,	usage_commit,
 	    "write changes from work tree to repository" },
+	{ "cherrypick",	cmd_cherrypick,	usage_cherrypick,
+	    "merge a single commit from another branch into a work tree" },
 };
 
 int
@@ -2604,5 +2608,117 @@ done:
 	free(cwd);
 	free(id_str);
 	free(editor);
+	return error;
+}
+
+__dead static void
+usage_cherrypick(void)
+{
+	fprintf(stderr, "usage: %s cherrypick commit-id\n", getprogname());
+	exit(1);
+}
+
+static const struct got_error *
+cmd_cherrypick(int argc, char *argv[])
+{
+	const struct got_error *error = NULL;
+	struct got_worktree *worktree = NULL;
+	struct got_repository *repo = NULL;
+	char *cwd = NULL, *commit_id_str = NULL;
+	struct got_object_id *commit_id = NULL;
+	struct got_commit_object *commit = NULL;
+	struct got_object_qid *pid;
+	struct got_reference *head_ref = NULL;
+	int ch, did_something = 0;
+
+	while ((ch = getopt(argc, argv, "")) != -1) {
+		switch (ch) {
+		default:
+			usage_cherrypick();
+			/* NOTREACHED */
+		}
+	}
+
+	argc -= optind;
+	argv += optind;
+
+	if (argc != 1)
+		usage_cherrypick();
+
+	cwd = getcwd(NULL, 0);
+	if (cwd == NULL) {
+		error = got_error_from_errno("getcwd");
+		goto done;
+	}
+	error = got_worktree_open(&worktree, cwd);
+	if (error)
+		goto done;
+
+	error = got_repo_open(&repo, got_worktree_get_repo_path(worktree));
+	if (error != NULL)
+		goto done;
+
+	error = apply_unveil(got_repo_get_path(repo), 0,
+	    got_worktree_get_root_path(worktree), 0);
+	if (error)
+		goto done;
+
+	error = got_object_resolve_id_str(&commit_id, repo, argv[0]);
+	if (error != NULL) {
+		struct got_reference *ref;
+		if (error->code != GOT_ERR_BAD_OBJ_ID_STR)
+			goto done;
+		error = got_ref_open(&ref, repo, argv[0], 0);
+		if (error != NULL)
+			goto done;
+		error = got_ref_resolve(&commit_id, repo, ref);
+		got_ref_close(ref);
+		if (error != NULL)
+			goto done;
+	}
+	error = got_object_id_str(&commit_id_str, commit_id);
+	if (error)
+		goto done;
+
+	error = got_ref_open(&head_ref, repo,
+	    got_worktree_get_head_ref_name(worktree), 0);
+	if (error != NULL)
+		goto done;
+
+	error = check_same_branch(commit_id, head_ref, repo);
+	if (error) {
+		if (error->code != GOT_ERR_ANCESTRY)
+			goto done;
+		error = NULL;
+	} else {
+		error = got_error(GOT_ERR_SAME_BRANCH);
+		goto done;
+	}
+
+	error = got_object_open_as_commit(&commit, repo, commit_id);
+	if (error)
+		goto done;
+	pid = SIMPLEQ_FIRST(got_object_commit_get_parent_ids(commit));
+	if (pid == NULL) {
+		error = got_error(GOT_ERR_ROOT_COMMIT);
+		goto done;
+	}
+	error = got_worktree_merge_files(worktree, pid->id, commit_id,
+	    repo, update_progress, &did_something, check_cancelled, NULL);
+	if (error != NULL)
+		goto done;
+
+	if (did_something)
+		printf("merged commit %s\n", commit_id_str);
+done:
+	if (commit)
+		got_object_commit_close(commit);
+	free(commit_id_str);
+	if (head_ref)
+		got_ref_close(head_ref);
+	if (worktree)
+		got_worktree_close(worktree);
+	if (repo)
+		got_repo_close(repo);
 	return error;
 }
blob - b5d808017aca17881058fa3c0276cc3ec7ed3381
blob + 0186e720548b6da8fde30b6fd002a9076c3adc16
--- include/got_error.h
+++ include/got_error.h
@@ -92,6 +92,10 @@
 #define GOT_ERR_COMMIT_NO_CHANGES 76
 #define GOT_ERR_BRANCH_MOVED	77
 #define GOT_ERR_OBJ_TOO_LARGE	78
+#define GOT_ERR_SAME_BRANCH	79
+#define GOT_ERR_ROOT_COMMIT	80
+#define GOT_ERR_MIXED_COMMITS	81
+#define GOT_ERR_CONFLICTS	82
 
 static const struct got_error {
 	int code;
@@ -177,6 +181,12 @@ static const struct got_error {
 	{ GOT_ERR_BRANCH_MOVED,	"work tree's head reference now points to a "
 	    "different branch; new head reference and/or update -b required" },
 	{ GOT_ERR_OBJ_TOO_LARGE,	"object too large" },
+	{ GOT_ERR_SAME_BRANCH,	"commit is already contained in this branch" },
+	{ GOT_ERR_ROOT_COMMIT,	"specified commit has no parent commit" },
+	{ GOT_ERR_MIXED_COMMITS,"work tree contains files from multiple "
+	    "base commits; the entire work tree must be updated first" },
+	{ GOT_ERR_CONFLICTS,	"work tree contains conflicted files; these "
+	    "conflicts must be resolved first" },
 };
 
 /*
blob - 2c68c6de849933fd543de60c68b71da71c71d802
blob + 1174e671c3ad58acfbc94422a2a09de5b3e23229
--- include/got_worktree.h
+++ include/got_worktree.h
@@ -125,6 +125,13 @@ const struct got_error *got_worktree_checkout_files(st
     const char *, struct got_repository *, got_worktree_checkout_cb, void *,
     got_worktree_cancel_cb, void *);
 
+/* Merge the differences between two commits into a work tree. */
+const struct got_error *
+got_worktree_merge_files(struct got_worktree *,
+    struct got_object_id *, struct got_object_id *,
+    struct got_repository *, got_worktree_checkout_cb, void *,
+    got_worktree_cancel_cb, void *);
+
 /* A callback function which is invoked to report a path's status. */
 typedef const struct got_error *(*got_worktree_status_cb)(void *,
     unsigned char, const char *, struct got_object_id *,
blob - f8c67ee6313435224cec826561679a4f712b822e
blob + 91723c534485c31a0ef0cf0edbbfcf1805d77afb
--- lib/worktree.c
+++ lib/worktree.c
@@ -41,6 +41,7 @@
 #include "got_path.h"
 #include "got_worktree.h"
 #include "got_opentemp.h"
+#include "got_diff.h"
 
 #include "got_lib_worktree.h"
 #include "got_lib_sha1.h"
@@ -737,28 +738,29 @@ done:
 }
 
 /*
- * Perform a 3-way merge where the file's version in the file index (blob2)
- * acts as the common ancestor, the incoming blob (blob1) acts as the first
- * derived version, and the file on disk acts as the second derived version.
+ * Perform a 3-way merge where blob2 acts as the common ancestor,
+ * blob1 acts as the first derived version, and the file on disk
+ * acts as the second derived version.
  */
 static const struct got_error *
-merge_blob(struct got_worktree *worktree, struct got_fileindex *fileindex,
-   struct got_fileindex_entry *ie, const char *ondisk_path, const char *path,
-   uint16_t te_mode, uint16_t st_mode, struct got_blob_object *blob1,
-   struct got_repository *repo,
-   got_worktree_checkout_cb progress_cb, void *progress_arg)
+merge_blob(int *local_changes_subsumed, struct got_worktree *worktree,
+    struct got_blob_object *blob2, const char *ondisk_path,
+    const char *path, uint16_t st_mode, struct got_blob_object *blob1,
+    struct got_repository *repo, got_worktree_checkout_cb progress_cb,
+    void *progress_arg)
 {
 	const struct got_error *err = NULL;
 	int merged_fd = -1;
-	struct got_blob_object *blob2 = NULL;
 	FILE *f1 = NULL, *f2 = NULL;
 	char *blob1_path = NULL, *blob2_path = NULL;
 	char *merged_path = NULL, *base_path = NULL;
 	char *id_str = NULL;
 	char *label1 = NULL;
-	int overlapcnt = 0, update_timestamps = 0;
+	int overlapcnt = 0;
 	char *parent;
 
+	*local_changes_subsumed = 0;
+
 	parent = dirname(ondisk_path);
 	if (parent == NULL)
 		return got_error_from_errno2("dirname", ondisk_path);
@@ -794,12 +796,7 @@ merge_blob(struct got_worktree *worktree, struct got_f
 	err = got_opentemp_named(&blob2_path, &f2, base_path);
 	if (err)
 		goto done;
-	if (got_fileindex_entry_has_blob(ie)) {
-		struct got_object_id id2;
-		memcpy(id2.sha1, ie->blob_sha1, SHA1_DIGEST_LENGTH);
-		err = got_object_open_as_blob(&blob2, repo, &id2, 8192);
-		if (err)
-			goto done;
+	if (blob2) {
 		err = got_object_blob_dump_to_file(NULL, NULL, f2, blob2);
 		if (err)
 			goto done;
@@ -834,7 +831,7 @@ merge_blob(struct got_worktree *worktree, struct got_f
 
 	/* Check if a clean merge has subsumed all local changes. */
 	if (overlapcnt == 0) {
-		err = check_files_equal(&update_timestamps, blob1_path,
+		err = check_files_equal(local_changes_subsumed, blob1_path,
 		    merged_path);
 		if (err)
 			goto done;
@@ -852,12 +849,6 @@ merge_blob(struct got_worktree *worktree, struct got_f
 		goto done;
 	}
 
-	/*
-	 * Do not update timestamps of already modified files. Otherwise,
-	 * a future status walk would treat them as unmodified files again.
-	 */
-	err = got_fileindex_entry_update(ie, ondisk_path,
-	    blob1->id.sha1, worktree->base_commit_id->sha1, update_timestamps);
 done:
 	if (merged_fd != -1 && close(merged_fd) != 0 && err == NULL)
 		err = got_error_from_errno("close");
@@ -865,8 +856,6 @@ done:
 		err = got_error_from_errno("fclose");
 	if (f2 && fclose(f2) != 0 && err == NULL)
 		err = got_error_from_errno("fclose");
-	if (blob2)
-		got_object_blob_close(blob2);
 	free(merged_path);
 	free(base_path);
 	if (blob1_path) {
@@ -1202,11 +1191,30 @@ update_blob(struct got_worktree *worktree,
 	if (err)
 		goto done;
 
-	if (status == GOT_STATUS_MODIFY || status == GOT_STATUS_ADD)
-		err = merge_blob(worktree, fileindex, ie, ondisk_path, path,
-		    te->mode, sb.st_mode, blob, repo, progress_cb,
-		    progress_arg);
-	else if (status == GOT_STATUS_DELETE) {
+	if (status == GOT_STATUS_MODIFY || status == GOT_STATUS_ADD) {
+		int update_timestamps;
+		struct got_blob_object *blob2 = NULL;
+		if (got_fileindex_entry_has_blob(ie)) {
+			struct got_object_id id2;
+			memcpy(id2.sha1, ie->blob_sha1, SHA1_DIGEST_LENGTH);
+			err = got_object_open_as_blob(&blob2, repo, &id2, 8192);
+			if (err)
+				goto done;
+		}
+		err = merge_blob(&update_timestamps, worktree, blob2,
+		    ondisk_path, path, sb.st_mode, blob, repo,
+		    progress_cb, progress_arg);
+		if (blob2)
+			got_object_blob_close(blob2);
+		/*
+		 * Do not update timestamps of files with local changes.
+		 * Otherwise, a future status walk would treat them as
+		 * unmodified files again.
+		 */
+		err = got_fileindex_entry_update(ie, ondisk_path,
+		    blob->id.sha1, worktree->base_commit_id->sha1,
+		    update_timestamps);
+	} else if (status == GOT_STATUS_DELETE) {
 		(*progress_cb)(progress_arg, GOT_STATUS_MERGE, path);
 		err = update_blob_fileindex_entry(worktree, fileindex, ie,
 		    ondisk_path, path, blob, 0);
@@ -1259,8 +1267,7 @@ remove_ondisk_file(const char *root_path, const char *
 
 static const struct got_error *
 delete_blob(struct got_worktree *worktree, struct got_fileindex *fileindex,
-    struct got_fileindex_entry *ie, const char *parent_path,
-    struct got_repository *repo,
+    struct got_fileindex_entry *ie, struct got_repository *repo,
     got_worktree_checkout_cb progress_cb, void *progress_arg)
 {
 	const struct got_error *err = NULL;
@@ -1331,7 +1338,7 @@ diff_old(void *arg, struct got_fileindex_entry *ie, co
 	if (a->cancel_cb && a->cancel_cb(a->cancel_arg))
 		return got_error(GOT_ERR_CANCELLED);
 
-	return delete_blob(a->worktree, a->fileindex, ie, parent_path,
+	return delete_blob(a->worktree, a->fileindex, ie,
 	    a->repo, a->progress_cb, a->progress_arg);
 }
 
@@ -1655,6 +1662,224 @@ done:
 	return err;
 }
 
+struct merge_file_cb_arg {
+    struct got_worktree *worktree;
+    struct got_fileindex *fileindex;
+    struct got_object_id *commit_id1;
+    struct got_object_id *commit_id2;
+    got_worktree_checkout_cb progress_cb;
+    void *progress_arg;
+    got_worktree_cancel_cb cancel_cb;
+    void *cancel_arg;
+};
+
+static const struct got_error *
+merge_file_cb(void *arg, struct got_blob_object *blob1,
+    struct got_blob_object *blob2, struct got_object_id *id1,
+    struct got_object_id *id2, const char *path1, const char *path2,
+    struct got_repository *repo)
+{
+	static const struct got_error *err = NULL;
+	struct merge_file_cb_arg *a = arg;
+	struct got_fileindex_entry *ie;
+	char *ondisk_path = NULL;
+	struct stat sb;
+	unsigned char status;
+	int local_changes_subsumed;
+
+	if (blob1 && blob2) {
+		ie = got_fileindex_entry_get(a->fileindex, path2);
+		if (ie == NULL) {
+			(*a->progress_cb)(a->progress_arg, GOT_STATUS_MISSING,
+			    path2);
+			return NULL;
+		}
+
+		if (asprintf(&ondisk_path, "%s/%s", a->worktree->root_path,
+		    path2) == -1)
+			return got_error_from_errno("asprintf");
+
+		err = get_file_status(&status, &sb, ie, ondisk_path, repo);
+		if (err)
+			goto done;
+
+		if (status == GOT_STATUS_DELETE) {
+			(*a->progress_cb)(a->progress_arg, GOT_STATUS_MERGE,
+			    path2);
+			goto done;
+		}
+		if (status != GOT_STATUS_NO_CHANGE &&
+		    status != GOT_STATUS_MODIFY &&
+		    status != GOT_STATUS_CONFLICT &&
+		    status != GOT_STATUS_ADD) {
+			(*a->progress_cb)(a->progress_arg, status, path2);
+			goto done;
+		}
+
+		err = merge_blob(&local_changes_subsumed, a->worktree, blob1,
+		    ondisk_path, path2, sb.st_mode, blob2, repo,
+		    a->progress_cb, a->progress_arg);
+	} else if (blob1) {
+		ie = got_fileindex_entry_get(a->fileindex, path1);
+		if (ie == NULL) {
+			(*a->progress_cb)(a->progress_arg, GOT_STATUS_MISSING,
+			    path2);
+			return NULL;
+		}
+		err = delete_blob(a->worktree, a->fileindex, ie, repo,
+		    a->progress_cb, a->progress_arg);
+	} else if (blob2) {
+		if (asprintf(&ondisk_path, "%s/%s", a->worktree->root_path,
+		    path2) == -1)
+			return got_error_from_errno("asprintf");
+		ie = got_fileindex_entry_get(a->fileindex, path2);
+		if (ie) {
+			err = get_file_status(&status, &sb, ie, ondisk_path,
+			    repo);
+			if (err)
+				goto done;
+			if (status != GOT_STATUS_NO_CHANGE &&
+			    status != GOT_STATUS_MODIFY &&
+			    status != GOT_STATUS_CONFLICT &&
+			    status != GOT_STATUS_ADD) {
+				(*a->progress_cb)(a->progress_arg, status,
+				    path2);
+				goto done;
+			}
+			err = merge_blob(&local_changes_subsumed, a->worktree,
+			    NULL, ondisk_path, path2, sb.st_mode, blob2, repo,
+			    a->progress_cb, a->progress_arg);
+			if (status == GOT_STATUS_DELETE) {
+				err = update_blob_fileindex_entry(a->worktree,
+				    a->fileindex, ie, ondisk_path, ie->path,
+				    blob2, 0);
+				if (err)
+					goto done;
+			}
+		} else {
+			sb.st_mode = GOT_DEFAULT_FILE_MODE;
+			err = install_blob(a->worktree, ondisk_path, path2,
+			    /* XXX get this from parent tree! */
+			    GOT_DEFAULT_FILE_MODE,
+			    sb.st_mode, blob2, 0, 0, repo,
+			    a->progress_cb, a->progress_arg);
+			if (err)
+				goto done;
+
+			err = update_blob_fileindex_entry(a->worktree,
+			    a->fileindex, NULL, ondisk_path, path2, blob2, 0);
+			if (err)
+				goto done;
+		}
+	}
+done:
+	free(ondisk_path);
+	return err;
+}
+
+struct check_merge_ok_arg {
+	struct got_worktree *worktree;
+	struct got_repository *repo;
+};
+
+static const struct got_error *
+check_merge_ok(void *arg, struct got_fileindex_entry *ie)
+{
+	const struct got_error *err = NULL;
+	struct check_merge_ok_arg *a = arg;
+	unsigned char status;
+	struct stat sb;
+	char *ondisk_path;
+
+	/* Reject merges into a work tree with mixed base commits. */
+	if (memcmp(ie->commit_sha1, a->worktree->base_commit_id->sha1,
+	    SHA1_DIGEST_LENGTH))
+		return got_error(GOT_ERR_MIXED_COMMITS);
+
+	if (asprintf(&ondisk_path, "%s/%s", a->worktree->root_path, ie->path)
+	    == -1)
+		return got_error_from_errno("asprintf");
+
+	/* Reject merges into a work tree with conflicted files. */
+	err = get_file_status(&status, &sb, ie, ondisk_path, a->repo);
+	if (err)
+		return err;
+	if (status == GOT_STATUS_CONFLICT)
+		return got_error(GOT_ERR_CONFLICTS);
+
+	return NULL;
+}
+
+const struct got_error *
+got_worktree_merge_files(struct got_worktree *worktree,
+    struct got_object_id *commit_id1, struct got_object_id *commit_id2,
+    struct got_repository *repo, got_worktree_checkout_cb progress_cb,
+    void *progress_arg, got_worktree_cancel_cb cancel_cb, void *cancel_arg)
+{
+	const struct got_error *err = NULL, *unlockerr;
+	struct got_object_id *tree_id1 = NULL, *tree_id2 = NULL;
+	struct got_tree_object *tree1 = NULL, *tree2 = NULL;
+	struct merge_file_cb_arg arg;
+	char *fileindex_path = NULL;
+	struct got_fileindex *fileindex = NULL;
+	struct check_merge_ok_arg mok_arg;
+
+	err = lock_worktree(worktree, LOCK_EX);
+	if (err)
+		return err;
+
+	err = open_fileindex(&fileindex, &fileindex_path, worktree);
+	if (err)
+		goto done;
+
+	mok_arg.worktree = worktree;
+	mok_arg.repo = repo;
+	err = got_fileindex_for_each_entry_safe(fileindex, check_merge_ok,
+	    &mok_arg);
+	if (err)
+		goto done;
+
+	err = got_object_id_by_path(&tree_id1, repo, commit_id1,
+	    worktree->path_prefix);
+	if (err)
+		goto done;
+
+	err = got_object_id_by_path(&tree_id2, repo, commit_id2,
+	    worktree->path_prefix);
+	if (err)
+		goto done;
+
+	err = got_object_open_as_tree(&tree1, repo, tree_id1);
+	if (err)
+		goto done;
+
+	err = got_object_open_as_tree(&tree2, repo, tree_id2);
+	if (err)
+		goto done;
+
+	arg.worktree = worktree;
+	arg.fileindex = fileindex;
+	arg.commit_id1 = commit_id1;
+	arg.commit_id2 = commit_id2;
+	arg.progress_cb = progress_cb;
+	arg.progress_arg = progress_arg;
+	arg.cancel_cb = cancel_cb;
+	arg.cancel_arg = cancel_arg;
+	err = got_diff_tree(tree1, tree2, "", "", repo, merge_file_cb, &arg);
+done:
+	free(fileindex_path);
+	got_fileindex_free(fileindex);
+	if (tree1)
+		got_object_tree_close(tree1);
+	if (tree2)
+		got_object_tree_close(tree2);
+
+	unlockerr = lock_worktree(worktree, LOCK_SH);
+	if (unlockerr && err == NULL)
+		err = unlockerr;
+	return err;
+}
+
 struct diff_dir_cb_arg {
     struct got_fileindex *fileindex;
     struct got_worktree *worktree;
blob - ff9bbc68ea0af0bc35fcd025a982ec9136e9a7f5
blob + 5776af5d999314a63da955a325813c1315eb8bfa
--- regress/cmdline/Makefile
+++ regress/cmdline/Makefile
@@ -1,4 +1,4 @@
-REGRESS_TARGETS=checkout update status log add rm diff commit
+REGRESS_TARGETS=checkout update status log add rm diff commit cherrypick
 NOOBJ=Yes
 
 checkout:
@@ -25,4 +25,7 @@ diff:
 commit:
 	./commit.sh
 
+cherrypick:
+	./cherrypick.sh
+
 .include <bsd.regress.mk>
blob - /dev/null
blob + 8dca3386160a47f20341cf47880b79c3f8c2692c (mode 755)
--- /dev/null
+++ regress/cmdline/cherrypick.sh
@@ -0,0 +1,85 @@
+#!/bin/sh
+#
+# Copyright (c) 2019 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
+
+function test_cherrypick_basic {
+	local testroot=`test_init cherrypick_basic`
+
+	got checkout $testroot/repo $testroot/wt > /dev/null
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	(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"
+
+	echo "modified alpha on branch" > $testroot/repo/alpha
+	(cd $testroot/repo && git rm -q beta)
+	echo "new file on branch" > $testroot/repo/epsilon/new
+	(cd $testroot/repo && git add epsilon/new)
+	git_commit $testroot/repo -m "committing more changes on newbranch"
+
+	local branch_rev=`git_show_head $testroot/repo`
+
+	(cd $testroot/wt && got cherrypick $branch_rev > $testroot/stdout)
+
+	echo "G  alpha" > $testroot/stdout.expected
+	echo "D  beta" >> $testroot/stdout.expected
+	echo "A  epsilon/new" >> $testroot/stdout.expected
+	echo "merged commit $branch_rev" >> $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 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
+
+	test_done "$testroot" "$ret"
+}
+
+run_test test_cherrypick_basic