commit 234035bc7943e32aa92668438f4c0ba9c85e2f83 from: Stefan Sperling date: Sat Jun 01 16:23:07 2019 UTC add 'got cherrypick' command 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 blob - /dev/null blob + 8dca3386160a47f20341cf47880b79c3f8c2692c (mode 755) --- /dev/null +++ regress/cmdline/cherrypick.sh @@ -0,0 +1,85 @@ +#!/bin/sh +# +# Copyright (c) 2019 Stefan Sperling +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +. ./common.sh + +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