Commit Diff


commit - 7d69d862a07866680ea64fcf8c30500f1f510243
commit + 0e039681388d7ec1f832edcbb108ac28db95ae1e
blob - e1becc061137b20d5e2b85ad0fb8830565d9e2db
blob + 78a5edb08ad1bbb3feb172d3b16c537a9fe1c3cb
--- include/got_error.h
+++ include/got_error.h
@@ -161,6 +161,7 @@
 #define GOT_ERR_MERGE_COMMIT_OUT_OF_DATE 143
 #define GOT_ERR_MERGE_BUSY	144
 #define GOT_ERR_MERGE_PATH	145
+#define GOT_ERR_FILE_BINARY	146
 
 static const struct got_error {
 	int code;
@@ -337,6 +338,7 @@ static const struct got_error {
 	    "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" },
+	{ GOT_ERR_FILE_BINARY, "found a binary file instead of text" },
 };
 
 /*
blob - 8ec39e3af09c02989e1df349d533b1442550f19d
blob + 95fdb0726bce1615a82c90e53763702fa49bc139
--- lib/diff3.c
+++ lib/diff3.c
@@ -223,9 +223,19 @@ diffreg(BUF **d, const char *path1, const char *path2,
 	if (err)
 		goto done;
 
-	err = got_diffreg(&diffreg_result, f1, f2, diff_algo, 0, 1);
+	err = got_diffreg(&diffreg_result, f1, f2, diff_algo, 0, 0);
 	if (err)
 		goto done;
+
+	if (diffreg_result) {
+		struct diff_result *diff_result = diffreg_result->result;
+		int atomizer_flags = (diff_result->left->atomizer_flags |
+		    diff_result->right->atomizer_flags);
+		if ((atomizer_flags & DIFF_ATOMIZER_FOUND_BINARY_DATA)) {
+			err = got_error(GOT_ERR_FILE_BINARY);
+			goto done;
+		}
+	}
 
 	err = got_diffreg_output(NULL, NULL, diffreg_result, 1, 1, "", "",
 	    GOT_DIFF_OUTPUT_EDSCRIPT, 0, outfile);
blob - 31c6214885278c6568978931a3f60491140afca9
blob + 08a1d596cc47c30cfb0e5dd2c6118178530bdd72
--- lib/worktree.c
+++ lib/worktree.c
@@ -474,6 +474,159 @@ check_files_equal(int *same, FILE *f1, FILE *f2)
 		return got_ferror(f2, GOT_ERR_IO);
 
 	return check_file_contents_equal(same, f1, f2);
+}
+
+static const struct got_error *
+copy_file_to_fd(off_t *outsize, FILE *f, int outfd)
+{
+	uint8_t fbuf[65536];
+	size_t flen;
+	ssize_t outlen;
+
+	*outsize = 0;
+
+	if (fseek(f, 0L, SEEK_SET) == -1)
+		return got_ferror(f, GOT_ERR_IO);
+
+	for (;;) {
+		flen = fread(fbuf, 1, sizeof(fbuf), f);
+		if (flen == 0) {
+			if (ferror(f))
+				return got_error_from_errno("fread");
+			if (feof(f))
+				break;
+		}
+		outlen = write(outfd, fbuf, flen);
+		if (outlen == -1)
+			return got_error_from_errno("write");
+		if (outlen != flen)
+			return got_error(GOT_ERR_IO);
+		*outsize += outlen;
+	}
+
+	return NULL;
+}
+
+static const struct got_error *
+merge_binary_file(int *overlapcnt, int merged_fd,
+    FILE *f_deriv, FILE *f_orig, FILE *f_deriv2,
+    const char *label_deriv, const char *label_orig, const char *label_deriv2,
+    const char *ondisk_path)
+{
+	const struct got_error *err = NULL;
+	int same_content, changed_deriv, changed_deriv2;
+	int fd_orig = -1, fd_deriv = -1, fd_deriv2 = -1;
+	off_t size_orig = 0, size_deriv = 0, size_deriv2 = 0;
+	char *path_orig = NULL, *path_deriv = NULL, *path_deriv2 = NULL;
+	char *base_path_orig = NULL, *base_path_deriv = NULL;
+	char *base_path_deriv2 = NULL;
+
+	*overlapcnt = 0;
+
+	err = check_files_equal(&same_content, f_deriv, f_deriv2);
+	if (err)
+		return err;
+
+	if (same_content)
+		return copy_file_to_fd(&size_deriv, f_deriv, merged_fd);
+
+	err = check_files_equal(&same_content, f_deriv, f_orig);
+	if (err)
+		return err;
+	changed_deriv = !same_content;
+	err = check_files_equal(&same_content, f_deriv2, f_orig);
+	if (err)
+		return err;
+	changed_deriv2 = !same_content;
+
+	if (changed_deriv && changed_deriv2) {
+		*overlapcnt = 1;
+		if (asprintf(&base_path_orig, "%s-orig", ondisk_path) == -1) {
+			err = got_error_from_errno("asprintf");
+			goto done;
+		}
+		if (asprintf(&base_path_deriv, "%s-1", ondisk_path) == -1) {
+			err = got_error_from_errno("asprintf");
+			goto done;
+		}
+		if (asprintf(&base_path_deriv2, "%s-2", ondisk_path) == -1) {
+			err = got_error_from_errno("asprintf");
+			goto done;
+		}
+		err = got_opentemp_named_fd(&path_orig, &fd_orig,
+		    base_path_orig);
+		if (err)
+			goto done;
+		err = got_opentemp_named_fd(&path_deriv, &fd_deriv,
+		    base_path_deriv);
+		if (err)
+			goto done;
+		err = got_opentemp_named_fd(&path_deriv2, &fd_deriv2,
+		    base_path_deriv2);
+		if (err)
+			goto done;
+		err = copy_file_to_fd(&size_orig, f_orig, fd_orig);
+		if (err)
+			goto done;
+		err = copy_file_to_fd(&size_deriv, f_deriv, fd_deriv);
+		if (err)
+			goto done;
+		err = copy_file_to_fd(&size_deriv2, f_deriv2, fd_deriv2);
+		if (err)
+			goto done;
+		if (dprintf(merged_fd, "Binary files differ and cannot be "
+		    "merged automatically:\n") < 0) {
+			err = got_error_from_errno("dprintf");
+			goto done;
+		}
+		if (dprintf(merged_fd, "%s%s%s\nfile %s\n",
+		    GOT_DIFF_CONFLICT_MARKER_BEGIN,
+		    label_deriv ? " " : "",
+		    label_deriv ? label_deriv : "",
+		    path_deriv) < 0) {
+			err = got_error_from_errno("dprintf");
+			goto done;
+		}
+		if (size_orig > 0) {
+			if (dprintf(merged_fd, "%s%s%s\nfile %s\n",
+			    GOT_DIFF_CONFLICT_MARKER_ORIG,
+			    label_orig ? " " : "",
+			    label_orig ? label_orig : "",
+			    path_orig) < 0) {
+				err = got_error_from_errno("dprintf");
+				goto done;
+			}
+		}
+		if (dprintf(merged_fd, "%s\nfile %s\n%s%s%s\n",
+		    GOT_DIFF_CONFLICT_MARKER_SEP,
+		    path_deriv2,
+		    GOT_DIFF_CONFLICT_MARKER_END,
+		    label_deriv2 ?  " " : "",
+		    label_deriv2 ? label_deriv2 : "") < 0) {
+			err = got_error_from_errno("dprintf");
+			goto done;
+		}
+	} else if (changed_deriv)
+		err = copy_file_to_fd(&size_deriv, f_deriv, merged_fd);
+	else if (changed_deriv2)
+		err = copy_file_to_fd(&size_deriv2, f_deriv2, merged_fd);
+done:
+	if (size_orig == 0 && path_orig && unlink(path_orig) == -1 &&
+	    err == NULL)
+		err = got_error_from_errno2("unlink", path_orig);
+	if (fd_orig != -1 && close(fd_orig) == -1 && err == NULL)
+		err = got_error_from_errno2("close", path_orig);
+	if (fd_deriv != -1 && close(fd_deriv) == -1 && err == NULL)
+		err = got_error_from_errno2("close", path_deriv);
+	if (fd_deriv2 != -1 && close(fd_deriv2) == -1 && err == NULL)
+		err = got_error_from_errno2("close", path_deriv2);
+	free(path_orig);
+	free(path_deriv);
+	free(path_deriv2);
+	free(base_path_orig);
+	free(base_path_deriv);
+	free(base_path_deriv2);
+	return err;
 }
 
 /*
@@ -515,8 +668,15 @@ merge_file(int *local_changes_subsumed, struct got_wor
 
 	err = got_merge_diff3(&overlapcnt, merged_fd, f_deriv, f_orig,
 	    f_deriv2, label_deriv, label_orig, label_deriv2, diff_algo);
-	if (err)
-		goto done;
+	if (err) {
+		if (err->code != GOT_ERR_FILE_BINARY)
+			goto done;
+		err = merge_binary_file(&overlapcnt, merged_fd, f_deriv,
+		    f_orig, f_deriv2, label_deriv, label_orig, label_deriv2,
+		    ondisk_path);
+		if (err)
+			goto done;
+	}
 
 	err = (*progress_cb)(progress_arg,
 	    overlapcnt > 0 ? GOT_STATUS_CONFLICT : GOT_STATUS_MERGE, path);
blob - e03fd43fd1cb428c958535c9201a1f65fe2366ad
blob + bb31a4ff0f60f6eae6b54b90abdbf7ce98b11618
--- regress/cmdline/cherrypick.sh
+++ regress/cmdline/cherrypick.sh
@@ -1460,6 +1460,233 @@ test_cherrypick_dot_on_a_line_by_itself() {
 		diff -u $testroot/content.expected $testroot/content
 	fi
 	test_done "$testroot" "$ret"
+}
+
+test_cherrypick_binary_file() {
+	local testroot=`test_init cherrypick_binary_file`
+	local commit_id0=`git_show_head $testroot/repo`
+
+	got checkout $testroot/repo $testroot/wt > /dev/null
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	cp /bin/ls $testroot/wt/foo
+	chmod 755 $testroot/wt/foo
+	(cd $testroot/wt && got add foo >/dev/null)
+	(cd $testroot/wt && got commit -m 'add binary file' > /dev/null)
+	local commit_id1=`git_show_head $testroot/repo`
+
+	cp /bin/cat $testroot/wt/foo
+	chmod 755 $testroot/wt/foo
+	(cd $testroot/wt && got commit -m 'change binary file' > /dev/null)
+	local commit_id2=`git_show_head $testroot/repo`
+
+	cp /bin/cp $testroot/wt/foo
+	chmod 755 $testroot/wt/foo
+	(cd $testroot/wt && got commit -m 'change binary file' > /dev/null)
+	local commit_id3=`git_show_head $testroot/repo`
+
+	(cd $testroot/wt && got rm foo >/dev/null)
+	(cd $testroot/wt && got commit -m 'remove binary file' > /dev/null)
+	local commit_id4=`git_show_head $testroot/repo`
+
+	# backdate the work tree to make it usable for cherry-picking
+	(cd $testroot/wt && got up -c $commit_id0 > /dev/null)
+
+	# cherry-pick addition of a binary file
+	(cd $testroot/wt && got cy $commit_id1 > $testroot/stdout)
+
+	echo "A  foo" > $testroot/stdout.expected
+	echo "Merged commit $commit_id1" >> $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
+
+	cp /bin/ls $testroot/content.expected
+	chmod 755 $testroot/content.expected
+	cp $testroot/wt/foo $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
+
+	# cherry-pick modification of a binary file
+	(cd $testroot/wt && got cy $commit_id2 > $testroot/stdout)
+
+	echo "G  foo" > $testroot/stdout.expected
+	echo "Merged commit $commit_id2" >> $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
+
+	cp /bin/cat $testroot/content.expected
+	chmod 755 $testroot/content.expected
+	cp $testroot/wt/foo $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
+
+	# cherry-pick conflicting addition of a binary file
+	(cd $testroot/wt && got cy $commit_id1 > $testroot/stdout)
+
+	echo "C  foo" > $testroot/stdout.expected
+	echo "Merged commit $commit_id1" >> $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 "Binary files differ and cannot be merged automatically:" \
+		> $testroot/content.expected
+	echo "<<<<<<< merged change: commit $commit_id1" \
+		>> $testroot/content.expected
+	echo -n "file " >> $testroot/content.expected
+	ls $testroot/wt/foo-1-* >> $testroot/content.expected
+	echo '=======' >> $testroot/content.expected
+	echo -n "file " >> $testroot/content.expected
+	ls $testroot/wt/foo-2-* >> $testroot/content.expected
+	echo '>>>>>>>' >> $testroot/content.expected
+	cp $testroot/wt/foo $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
+
+	# revert local changes to allow further testing
+	(cd $testroot/wt && got revert -R . >/dev/null)
+
+	(cd $testroot/wt && got status > $testroot/stdout)
+	echo '?  foo' > $testroot/stdout.expected
+	echo -n '?  ' >> $testroot/stdout.expected
+	(cd $testroot/wt && ls foo-1-* >> $testroot/stdout.expected)
+	echo -n '?  ' >> $testroot/stdout.expected
+	(cd $testroot/wt && ls foo-2-* >> $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
+
+	# tidy up
+	rm $testroot/wt/foo $testroot/wt/foo-1-* $testroot/wt/foo-2-*
+	(cd $testroot/wt && got up -c $commit_id1 > /dev/null)
+
+	# cherry-pick conflicting modification of a binary file
+	(cd $testroot/wt && got cy $commit_id3 > $testroot/stdout)
+
+	echo "C  foo" > $testroot/stdout.expected
+	echo "Merged commit $commit_id3" >> $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 "Binary files differ and cannot be merged automatically:" \
+		> $testroot/content.expected
+	echo '<<<<<<<' >> $testroot/content.expected
+	echo -n "file " >> $testroot/content.expected
+	ls $testroot/wt/foo-1-* >> $testroot/content.expected
+	echo "||||||| 3-way merge base: commit $commit_id2" \
+		>> $testroot/content.expected
+	echo -n "file " >> $testroot/content.expected
+	ls $testroot/wt/foo-orig-* >> $testroot/content.expected
+	echo '=======' >> $testroot/content.expected
+	echo -n "file " >> $testroot/content.expected
+	ls $testroot/wt/foo-2-* >> $testroot/content.expected
+	echo ">>>>>>> merged change: commit $commit_id3" \
+		>> $testroot/content.expected
+	cp $testroot/wt/foo $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
+
+	cp /bin/ls $testroot/content.expected
+	chmod 755 $testroot/content.expected
+	cat $testroot/wt/foo-1-* > $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
+
+	cp /bin/cp $testroot/content.expected
+	chmod 755 $testroot/content.expected
+	cat $testroot/wt/foo-2-* > $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
+
+	# revert local changes to allow further testing
+	(cd $testroot/wt && got revert -R . > /dev/null)
+	rm $testroot/wt/foo-1-*
+	rm $testroot/wt/foo-2-*
+	(cd $testroot/wt && got up -c $commit_id3 > /dev/null)
+
+	# cherry-pick deletion of a binary file
+	(cd $testroot/wt && got cy $commit_id4 > $testroot/stdout)
+
+	echo "D  foo" > $testroot/stdout.expected
+	echo "Merged commit $commit_id4" >> $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
+
+	if [ -e $testroot/wt/foo ]; then
+		echo "removed file foo still exists on disk" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+	test_done "$testroot" "0"
 }
 
 test_parseargs "$@"
@@ -1478,3 +1705,4 @@ run_test test_cherrypick_conflict_no_eol2
 run_test test_cherrypick_unrelated_changes
 run_test test_cherrypick_same_branch
 run_test test_cherrypick_dot_on_a_line_by_itself
+run_test test_cherrypick_binary_file
blob - d064dee9ea07f4c1490ffd37ad9aed281f7f6cb0
blob + 51b2e89261f318fde6e149922ab6d66ef3629aac
--- regress/cmdline/update.sh
+++ regress/cmdline/update.sh
@@ -2741,6 +2741,267 @@ test_update_quiet() {
 		diff -u $testroot/content.expected $testroot/content
 	fi
 	test_done "$testroot" "$ret"
+}
+
+test_update_binary_file() {
+	local testroot=`test_init update_binary_file`
+	local commit_id0=`git_show_head $testroot/repo`
+
+	got checkout $testroot/repo $testroot/wt > /dev/null
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	cp /bin/ls $testroot/wt/foo
+	chmod 755 $testroot/wt/foo
+	(cd $testroot/wt && got add foo >/dev/null)
+	(cd $testroot/wt && got commit -m 'add binary file' > /dev/null)
+	local commit_id1=`git_show_head $testroot/repo`
+
+	cp /bin/cat $testroot/wt/foo
+	chmod 755 $testroot/wt/foo
+	(cd $testroot/wt && got commit -m 'change binary file' > /dev/null)
+	local commit_id2=`git_show_head $testroot/repo`
+
+	cp /bin/cp $testroot/wt/foo
+	chmod 755 $testroot/wt/foo
+	(cd $testroot/wt && got commit -m 'change binary file' > /dev/null)
+	local commit_id3=`git_show_head $testroot/repo`
+
+	(cd $testroot/wt && got rm foo >/dev/null)
+	(cd $testroot/wt && got commit -m 'remove binary file' > /dev/null)
+	local commit_id4=`git_show_head $testroot/repo`
+
+	# backdate the work tree to make it usable for updating
+	(cd $testroot/wt && got up -c $commit_id0 > /dev/null)
+
+	# update which adds a binary file
+	(cd $testroot/wt && got up -c $commit_id1 > $testroot/stdout)
+
+	echo "A  foo" > $testroot/stdout.expected
+	echo -n "Updated to refs/heads/master: $commit_id1" \
+		>> $testroot/stdout.expected
+	echo >> $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
+
+	cp /bin/ls $testroot/content.expected
+	chmod 755 $testroot/content.expected
+	cat $testroot/wt/foo > $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
+
+	# update which adds a conflicting binary file
+	(cd $testroot/wt && got up -c $commit_id0 > /dev/null)
+	cp /bin/cat $testroot/wt/foo
+	chmod 755 $testroot/wt/foo
+	(cd $testroot/wt && got add foo > /dev/null)
+	(cd $testroot/wt && got up -c $commit_id1 > $testroot/stdout)
+
+	echo "C  foo" > $testroot/stdout.expected
+	echo "Updated to refs/heads/master: $commit_id1" \
+		>> $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 "Binary files differ and cannot be merged automatically:" \
+		> $testroot/content.expected
+	echo "<<<<<<< merged change: commit $commit_id1" \
+		>> $testroot/content.expected
+	echo -n "file " >> $testroot/content.expected
+	ls $testroot/wt/foo-1-* >> $testroot/content.expected
+	echo '=======' >> $testroot/content.expected
+	echo -n "file " >> $testroot/content.expected
+	ls $testroot/wt/foo-2-* >> $testroot/content.expected
+	echo ">>>>>>>" >> $testroot/content.expected
+	cat $testroot/wt/foo > $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
+
+	cp /bin/ls $testroot/content.expected
+	chmod 755 $testroot/content.expected
+	cat $testroot/wt/foo-1-* > $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
+
+	cp /bin/cat $testroot/content.expected
+	chmod 755 $testroot/content.expected
+	cat $testroot/wt/foo-2-* > $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
+
+	# tidy up
+	(cd $testroot/wt && got revert -R . >/dev/null)
+	rm $testroot/wt/foo-1-* $testroot/wt/foo-2-*
+	(cd $testroot/wt && got up -c $commit_id1 > /dev/null)
+
+	# update which changes a binary file
+	(cd $testroot/wt && got up -c $commit_id2 > $testroot/stdout)
+
+	echo "U  foo" > $testroot/stdout.expected
+	echo -n "Updated to refs/heads/master: $commit_id2" \
+		>> $testroot/stdout.expected
+	echo >> $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
+
+	cp /bin/cat $testroot/content.expected
+	chmod 755 $testroot/content.expected
+	cat $testroot/wt/foo > $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
+
+	# update which changes a locally modified binary file
+	cp /bin/ls $testroot/wt/foo
+	chmod 755 $testroot/wt/foo
+	(cd $testroot/wt && got up -c $commit_id3 > $testroot/stdout)
+
+	echo "C  foo" > $testroot/stdout.expected
+	echo -n "Updated to refs/heads/master: $commit_id3" \
+		>> $testroot/stdout.expected
+	echo >> $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 "Binary files differ and cannot be merged automatically:" \
+		> $testroot/content.expected
+	echo "<<<<<<< merged change: commit $commit_id3" \
+		>> $testroot/content.expected
+	echo -n "file " >> $testroot/content.expected
+	ls $testroot/wt/foo-1-* >> $testroot/content.expected
+	echo "||||||| 3-way merge base: commit $commit_id2" \
+		>> $testroot/content.expected
+	echo -n "file " >> $testroot/content.expected
+	ls $testroot/wt/foo-orig-* >> $testroot/content.expected
+	echo '=======' >> $testroot/content.expected
+	echo -n "file " >> $testroot/content.expected
+	ls $testroot/wt/foo-2-* >> $testroot/content.expected
+	echo ">>>>>>>" >> $testroot/content.expected
+	cat $testroot/wt/foo > $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
+
+	cp /bin/cp $testroot/content.expected
+	chmod 755 $testroot/content.expected
+	cp $testroot/wt/foo-1-* $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
+
+	cp /bin/ls $testroot/content.expected
+	chmod 755 $testroot/content.expected
+	cp $testroot/wt/foo-2-* $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 'C  foo' > $testroot/stdout.expected
+	echo -n '?  ' >> $testroot/stdout.expected
+	(cd $testroot/wt && ls foo-1-* >> $testroot/stdout.expected)
+	echo -n '?  ' >> $testroot/stdout.expected
+	(cd $testroot/wt && ls foo-2-* >> $testroot/stdout.expected)
+	echo -n '?  ' >> $testroot/stdout.expected
+	(cd $testroot/wt && ls foo-orig-* >> $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
+
+	# tidy up
+	(cd $testroot/wt && got revert -R . > /dev/null)
+	rm $testroot/wt/foo-orig-* $testroot/wt/foo-1-* $testroot/wt/foo-2-*
+
+	# update which deletes a binary file
+	(cd $testroot/wt && got up -c $commit_id4 > $testroot/stdout)
+	echo "D  foo" > $testroot/stdout.expected
+	echo -n "Updated to refs/heads/master: $commit_id4" \
+		>> $testroot/stdout.expected
+	echo >> $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"
+	fi
+
+	if [ -e $testroot/wt/foo ]; then
+		echo "removed file foo still exists on disk" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+	test_done "$testroot" "0"
 }
 
 test_parseargs "$@"
@@ -2786,3 +3047,4 @@ run_test test_update_single_file
 run_test test_update_file_skipped_due_to_conflict
 run_test test_update_file_skipped_due_to_obstruction
 run_test test_update_quiet
+run_test test_update_binary_file