commit - aaa1358905e35eaa19a177bd11797d1a38d6cc03
commit + 234035bc7943e32aa92668438f4c0ba9c85e2f83
blob - 400c8b4665c0401ac0780eac38c93acbd16e73fa
blob + b8cd1a35b2873cdfc9028574e37bfc42cb5eb67a
--- got/got.1
+++ got/got.1
.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
__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 *[]);
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,
"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
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
#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;
{ 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
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
#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"
}
/*
- * 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);
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;
/* 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;
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");
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) {
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);
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;
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);
}
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
-REGRESS_TARGETS=checkout update status log add rm diff commit
+REGRESS_TARGETS=checkout update status log add rm diff commit cherrypick
NOOBJ=Yes
checkout:
commit:
./commit.sh
+cherrypick:
+ ./cherrypick.sh
+
.include <bsd.regress.mk>
blob - /dev/null
blob + 8dca3386160a47f20341cf47880b79c3f8c2692c (mode 755)
--- /dev/null
+++ regress/cmdline/cherrypick.sh
+#!/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