Commit Diff


commit - d3bcc3d18ab565811e1da723157c85657570b010
commit + 33aa809d4fd2e43328dd3ad4897a7394c28c79ca
blob - 7ed26ac56b31f616e7c25df822075c33721cfff9
blob + 54e42060280f796e019efb052d487169b04cd972
--- got/got.1
+++ got/got.1
@@ -509,7 +509,7 @@ Perform the operation even if a file contains uncommit
 .It Cm rm
 Short alias for
 .Cm remove .
-.It Cm revert [ Fl R ] Ar path ...
+.It Cm revert [ Fl p ] [ Fl F Ar response-script ] [ Fl R ] Ar path ...
 Revert any uncommited changes in files at the specified paths.
 File contents will be overwritten with those contained in the
 work tree's base commit. There is no way to bring discarded
@@ -527,6 +527,29 @@ The options for
 .Cm got revert
 are as follows:
 .Bl -tag -width Ds
+.It Fl p
+Instead of reverting all changes in files, interactively select or reject
+changes to revert based on
+.Dq y
+(revert change),
+.Dq n
+(keep change), and
+.Dq q
+(quit reverting this file) responses.
+If a file is in modified status, individual patches derived from the
+modified file content can be reverted.
+Files in added or deleted status may only be reverted in their entirety.
+.It Fl F Ar response-script
+With the
+.Fl p
+option, read
+.Dq y ,
+.Dq n ,
+and
+.Dq q
+responses line-by-line from the specified
+.Ar response-script
+file instead of prompting interactively.
 .It Fl R
 Permit recursion into directories.
 If this option is not specified,
@@ -1074,7 +1097,11 @@ View local changes in a work tree directory:
 .Pp
 .Dl $ got status
 .Dl $ got diff | less
+.Pp
+Interactively revert selected local changes in a work tree directory:
 .Pp
+.Dl $ got revert -p -R\ .
+.Pp
 In a work tree or a git repository directory, list all branch references:
 .Pp
 .Dl $ got branch -l
blob - 079cc2a7e0ea6073669d3006c65ae8cdf10eb1ec
blob + 2d491b86591c59df3a408119c74dfa30aa5287e8
--- got/got.c
+++ got/got.c
@@ -3104,7 +3104,8 @@ done:
 __dead static void
 usage_revert(void)
 {
-	fprintf(stderr, "usage: %s revert [-R] path ...\n", getprogname());
+	fprintf(stderr, "usage: %s revert [-p] [-F response-script] [-R] "
+	    "path ...\n", getprogname());
 	exit(1);
 }
 
@@ -3114,9 +3115,122 @@ revert_progress(void *arg, unsigned char status, const
 	while (path[0] == '/')
 		path++;
 	printf("%c  %s\n", status, path);
+	return NULL;
+}
+
+struct choose_patch_arg {
+	FILE *patch_script_file;
+	const char *action;
+};
+
+static const struct got_error *
+show_change(unsigned char status, const char *path, FILE *patch_file, int n,
+    int nchanges, const char *action)
+{
+	char *line = NULL;
+	size_t linesize = 0;
+	ssize_t linelen;
+
+	switch (status) {
+	case GOT_STATUS_ADD:
+		printf("A  %s\n%s this addition? [y/n] ", path, action);
+		break;
+	case GOT_STATUS_DELETE:
+		printf("D  %s\n%s this deletion? [y/n] ", path, action);
+		break;
+	case GOT_STATUS_MODIFY:
+		if (fseek(patch_file, 0L, SEEK_SET) == -1)
+			return got_error_from_errno("fseek");
+		printf(GOT_COMMIT_SEP_STR);
+		while ((linelen = getline(&line, &linesize, patch_file) != -1))
+			printf("%s", line);
+		if (ferror(patch_file))
+			return got_error_from_errno("getline");
+		printf(GOT_COMMIT_SEP_STR);
+		printf("M  %s (change %d of %d)\n%s this change? [y/n/q] ",
+		    path, n, nchanges, action);
+		break;
+	default:
+		return got_error_path(path, GOT_ERR_FILE_STATUS);
+	}
+
+	return NULL;
+}
+
+static const struct got_error *
+choose_patch(int *choice, void *arg, unsigned char status, const char *path,
+    FILE *patch_file, int n, int nchanges)
+{
+	const struct got_error *err = NULL;
+	char *line = NULL;
+	size_t linesize = 0;
+	ssize_t linelen;
+	int resp = ' ';
+	struct choose_patch_arg *a = arg;
+
+	*choice = GOT_PATCH_CHOICE_NONE;
+
+	if (a->patch_script_file) {
+		char *nl;
+		err = show_change(status, path, patch_file, n, nchanges,
+		    a->action);
+		if (err)
+			return err;
+		linelen = getline(&line, &linesize, a->patch_script_file);
+		if (linelen == -1) {
+			if (ferror(a->patch_script_file))
+				return got_error_from_errno("getline");
+			return NULL;
+		}
+		nl = strchr(line, '\n');
+		if (nl)
+			*nl = '\0';
+		if (strcmp(line, "y") == 0) {
+			*choice = GOT_PATCH_CHOICE_YES;
+			printf("y\n");
+		} else if (strcmp(line, "n") == 0) {
+			*choice = GOT_PATCH_CHOICE_NO;
+			printf("n\n");
+		} else if (strcmp(line, "q") == 0 &&
+		    status == GOT_STATUS_MODIFY) {
+			*choice = GOT_PATCH_CHOICE_QUIT;
+			printf("q\n");
+		} else
+			printf("invalid response '%s'\n", line);
+		free(line);
+		return NULL;
+	}
+
+	while (resp != 'y' && resp != 'n' && resp != 'q') {
+		err = show_change(status, path, patch_file, n, nchanges,
+		    a->action);
+		if (err)
+			return err;
+		resp = getchar();
+		if (resp == '\n')
+			resp = getchar();
+		if (status == GOT_STATUS_MODIFY) {
+			if (resp != 'y' && resp != 'n' && resp != 'q') {
+				printf("invalid response '%c'\n", resp);
+				resp = ' ';
+			}
+		} else if (resp != 'y' && resp != 'n') {
+				printf("invalid response '%c'\n", resp);
+				resp = ' ';
+		}
+	}
+
+	if (resp == 'y')
+		*choice = GOT_PATCH_CHOICE_YES;
+	else if (resp == 'n')
+		*choice = GOT_PATCH_CHOICE_NO;
+	else if (resp == 'q' && status == GOT_STATUS_MODIFY)
+		*choice = GOT_PATCH_CHOICE_QUIT;
+
 	return NULL;
 }
 
+
 static const struct got_error *
 cmd_revert(int argc, char *argv[])
 {
@@ -3126,12 +3240,21 @@ cmd_revert(int argc, char *argv[])
 	char *cwd = NULL, *path = NULL;
 	struct got_pathlist_head paths;
 	struct got_pathlist_entry *pe;
-	int ch, can_recurse = 0;
+	int ch, can_recurse = 0, pflag = 0;
+	FILE *patch_script_file = NULL;
+	const char *patch_script_path = NULL;
+	struct choose_patch_arg cpa;
 
 	TAILQ_INIT(&paths);
 
-	while ((ch = getopt(argc, argv, "R")) != -1) {
+	while ((ch = getopt(argc, argv, "pF:R")) != -1) {
 		switch (ch) {
+		case 'p':
+			pflag = 1;
+			break;
+		case 'F':
+			patch_script_path = optarg;
+			break;
 		case 'R':
 			can_recurse = 1;
 			break;
@@ -3151,6 +3274,8 @@ cmd_revert(int argc, char *argv[])
 #endif
 	if (argc < 1)
 		usage_revert();
+	if (patch_script_path && !pflag)
+		errx(1, "-F option can only be used together with -p option");
 
 	cwd = getcwd(NULL, 0);
 	if (cwd == NULL) {
@@ -3165,6 +3290,14 @@ cmd_revert(int argc, char *argv[])
 	if (error != NULL)
 		goto done;
 
+	if (patch_script_path) {
+		patch_script_file = fopen(patch_script_path, "r");
+		if (patch_script_file == NULL) {
+			error = got_error_from_errno2("fopen",
+			    patch_script_path);
+			goto done;
+		}
+	}
 	error = apply_unveil(got_repo_get_path(repo), 1,
 	    got_worktree_get_root_path(worktree));
 	if (error)
@@ -3203,11 +3336,16 @@ cmd_revert(int argc, char *argv[])
 		}
 	}
 
-	error = got_worktree_revert(worktree, &paths,
-	    revert_progress, NULL, repo);
+	cpa.patch_script_file = patch_script_file;
+	cpa.action = "revert";
+	error = got_worktree_revert(worktree, &paths, revert_progress, NULL,
+	    pflag ? choose_patch : NULL, &cpa, repo);
 	if (error)
 		goto done;
 done:
+	if (patch_script_file && fclose(patch_script_file) == EOF &&
+	    error == NULL)
+		error = got_error_from_errno2("fclose", patch_script_path);
 	if (repo)
 		got_repo_close(repo);
 	if (worktree)
@@ -5235,122 +5373,10 @@ print_stage(void *arg, unsigned char status, unsigned 
 
 	printf("%s %c %s\n", id_str, staged_status, path);
 	free(id_str);
-	return NULL;
-}
-
-static const struct got_error *
-show_change(unsigned char status, const char *path, FILE *patch_file, int n,
-    int nchanges, const char *action)
-{
-	char *line = NULL;
-	size_t linesize = 0;
-	ssize_t linelen;
-
-	switch (status) {
-	case GOT_STATUS_ADD:
-		printf("A  %s\n%s this addition? [y/n] ", path, action);
-		break;
-	case GOT_STATUS_DELETE:
-		printf("D  %s\n%s this deletion? [y/n] ", path, action);
-		break;
-	case GOT_STATUS_MODIFY:
-		if (fseek(patch_file, 0L, SEEK_SET) == -1)
-			return got_error_from_errno("fseek");
-		printf(GOT_COMMIT_SEP_STR);
-		while ((linelen = getline(&line, &linesize, patch_file) != -1))
-			printf("%s", line);
-		if (ferror(patch_file))
-			return got_error_from_errno("getline");
-		printf(GOT_COMMIT_SEP_STR);
-		printf("M  %s (change %d of %d)\n%s this change? [y/n/q] ",
-		    path, n, nchanges, action);
-		break;
-	default:
-		return got_error_path(path, GOT_ERR_FILE_STATUS);
-	}
-
 	return NULL;
 }
 
-struct choose_patch_arg {
-	FILE *patch_script_file;
-	const char *action;
-};
-
 static const struct got_error *
-choose_patch(int *choice, void *arg, unsigned char status, const char *path,
-    FILE *patch_file, int n, int nchanges)
-{
-	const struct got_error *err = NULL;
-	char *line = NULL;
-	size_t linesize = 0;
-	ssize_t linelen;
-	int resp = ' ';
-	struct choose_patch_arg *a = arg;
-
-	*choice = GOT_PATCH_CHOICE_NONE;
-
-	if (a->patch_script_file) {
-		char *nl;
-		err = show_change(status, path, patch_file, n, nchanges,
-		    a->action);
-		if (err)
-			return err;
-		linelen = getline(&line, &linesize, a->patch_script_file);
-		if (linelen == -1) {
-			if (ferror(a->patch_script_file))
-				return got_error_from_errno("getline");
-			return NULL;
-		}
-		nl = strchr(line, '\n');
-		if (nl)
-			*nl = '\0';
-		if (strcmp(line, "y") == 0) {
-			*choice = GOT_PATCH_CHOICE_YES;
-			printf("y\n");
-		} else if (strcmp(line, "n") == 0) {
-			*choice = GOT_PATCH_CHOICE_NO;
-			printf("n\n");
-		} else if (strcmp(line, "q") == 0 &&
-		    status == GOT_STATUS_MODIFY) {
-			*choice = GOT_PATCH_CHOICE_QUIT;
-			printf("q\n");
-		} else
-			printf("invalid response '%s'\n", line);
-		free(line);
-		return NULL;
-	}
-
-	while (resp != 'y' && resp != 'n' && resp != 'q') {
-		err = show_change(status, path, patch_file, n, nchanges,
-		    a->action);
-		if (err)
-			return err;
-		resp = getchar();
-		if (resp == '\n')
-			resp = getchar();
-		if (status == GOT_STATUS_MODIFY) {
-			if (resp != 'y' && resp != 'n' && resp != 'q') {
-				printf("invalid response '%c'\n", resp);
-				resp = ' ';
-			}
-		} else if (resp != 'y' && resp != 'n') {
-				printf("invalid response '%c'\n", resp);
-				resp = ' ';
-		}
-	}
-
-	if (resp == 'y')
-		*choice = GOT_PATCH_CHOICE_YES;
-	else if (resp == 'n')
-		*choice = GOT_PATCH_CHOICE_NO;
-	else if (resp == 'q' && status == GOT_STATUS_MODIFY)
-		*choice = GOT_PATCH_CHOICE_QUIT;
-
-	return NULL;
-}
-
-static const struct got_error *
 cmd_stage(int argc, char *argv[])
 {
 	const struct got_error *error = NULL;
blob - ff51861f2e2d1f238ff5c2e4b183ecf93de910ae
blob + 1a7f2a24bda27f5261331d7f5788b4a0b5ccdf91
--- include/got_worktree.h
+++ include/got_worktree.h
@@ -173,12 +173,25 @@ got_worktree_schedule_delete(struct got_worktree *,
     struct got_pathlist_head *, int, got_worktree_status_cb, void *,
     struct got_repository *);
 
+/* A callback function which is used to select or reject a patch. */
+typedef const struct got_error *(*got_worktree_patch_cb)(int *, void *,
+    unsigned char, const char *, FILE *, int, int);
+
+/* Values for result output parameter of got_wortree_patch_cb. */
+#define GOT_PATCH_CHOICE_NONE	0
+#define GOT_PATCH_CHOICE_YES	1
+#define GOT_PATCH_CHOICE_NO	2
+#define GOT_PATCH_CHOICE_QUIT	3
+
 /*
  * Revert a file at the specified path such that it matches its
  * original state in the worktree's base commit.
+ * If the patch callback is not NULL, call it to select patch hunks to
+ * revert. Otherwise, revert the whole file found at each path.
  */
 const struct got_error *got_worktree_revert(struct got_worktree *,
     struct got_pathlist_head *, got_worktree_checkout_cb, void *,
+    got_worktree_patch_cb patch_cb, void *patch_arg,
     struct got_repository *);
 
 /*
@@ -373,16 +386,6 @@ const struct got_error *got_worktree_get_histedit_scri
     struct got_worktree *);
 
 
-/* A callback function which is used to select or reject a patch. */
-typedef const struct got_error *(*got_worktree_patch_cb)(int *, void *,
-    unsigned char, const char *, FILE *, int, int);
-
-/* Values for result output parameter of got_wortree_patch_cb. */
-#define GOT_PATCH_CHOICE_NONE	0
-#define GOT_PATCH_CHOICE_YES	1
-#define GOT_PATCH_CHOICE_NO	2
-#define GOT_PATCH_CHOICE_QUIT	3
-
 /*
  * Stage the specified paths for commit.
  * If the patch callback is not NULL, call it to select patch hunks for
blob - 1fdda693ee6ec447c666e69535d782fcfb137e00
blob + daff6b4fb00bc0770f29dfa3992be6e84c3b13e7
--- lib/worktree.c
+++ lib/worktree.c
@@ -2680,18 +2680,308 @@ done:
 	unlockerr = lock_worktree(worktree, LOCK_SH);
 	if (unlockerr && err == NULL)
 		err = unlockerr;
+	return err;
+}
+
+static const struct got_error *
+copy_one_line(FILE *infile, FILE *outfile, FILE *rejectfile)
+{
+	const struct got_error *err = NULL;
+	char *line = NULL;
+	size_t linesize = 0, n;
+	ssize_t linelen;
+
+	linelen = getline(&line, &linesize, infile);
+	if (linelen == -1) {
+		if (ferror(infile)) {
+			err = got_error_from_errno("getline");
+			goto done;
+		}
+		return NULL;
+	}
+	if (outfile) {
+		n = fwrite(line, 1, linelen, outfile);
+		if (n != linelen) {
+			err = got_ferror(outfile, GOT_ERR_IO);
+			goto done;
+		}
+	}
+	if (rejectfile) {
+		n = fwrite(line, 1, linelen, rejectfile);
+		if (n != linelen)
+			err = got_ferror(outfile, GOT_ERR_IO);
+	}
+done:
+	free(line);
 	return err;
 }
 
+static const struct got_error *
+skip_one_line(FILE *f)
+{
+	char *line = NULL;
+	size_t linesize = 0;
+	ssize_t linelen;
+
+	linelen = getline(&line, &linesize, f);
+	free(line);
+	if (linelen == -1 && ferror(f))
+		return got_error_from_errno("getline");
+	return NULL;
+}
+
+static const struct got_error *
+copy_change(FILE *f1, FILE *f2, int *line_cur1, int *line_cur2,
+    int start_old, int end_old, int start_new, int end_new,
+    FILE *outfile, FILE *rejectfile)
+ {
+	const struct got_error *err;
+
+	/* Copy old file's lines leading up to patch. */
+	while (!feof(f1) && *line_cur1 < start_old) {
+		err = copy_one_line(f1, outfile, NULL);
+		if (err)
+			return err;
+		(*line_cur1)++;
+	}
+	/* Skip new file's lines leading up to patch. */
+	while (!feof(f2) && *line_cur2 < start_new) {
+		if (rejectfile)
+			err = copy_one_line(f2, NULL, rejectfile);
+		else
+			err = skip_one_line(f2);
+		if (err)
+			return err;
+		(*line_cur2)++;
+	}
+	/* Copy patched lines. */
+	while (!feof(f2) && *line_cur2 <= end_new) {
+		err = copy_one_line(f2, outfile, NULL);
+		if (err)
+			return err;
+		(*line_cur2)++;
+	}
+	/* Skip over old file's replaced lines. */
+	while (!feof(f1) && *line_cur1 <= end_new) {
+		if (rejectfile)
+			err = copy_one_line(f1, NULL, rejectfile);
+		else
+			err = skip_one_line(f1);
+		if (err)
+			return err;
+		(*line_cur1)++;
+	}
+	/* Copy old file's lines after patch. */
+	while (!feof(f1) && *line_cur1 <= end_old) {
+		err = copy_one_line(f1, outfile, rejectfile);
+		if (err)
+			return err;
+		(*line_cur1)++;
+	}
+
+	return NULL;
+}
+
+static const struct got_error *
+apply_or_reject_change(int *choice, struct got_diff_change *change, int n,
+    int nchanges, struct got_diff_state *ds, struct got_diff_args *args,
+    int diff_flags, const char *relpath, FILE *f1, FILE *f2, int *line_cur1,
+    int *line_cur2, FILE *outfile, FILE *rejectfile,
+    got_worktree_patch_cb patch_cb, void *patch_arg)
+{
+	const struct got_error *err = NULL;
+	int start_old = change->cv.a;
+	int end_old = change->cv.b;
+	int start_new = change->cv.c;
+	int end_new = change->cv.d;
+	long pos1, pos2;
+	FILE *hunkfile;
+
+	*choice = GOT_PATCH_CHOICE_NONE;
+
+	hunkfile = got_opentemp();
+	if (hunkfile == NULL)
+		return got_error_from_errno("got_opentemp");
+
+	pos1 = ftell(f1);
+	pos2 = ftell(f2);
+
+	/* XXX TODO needs error checking */
+	got_diff_dump_change(hunkfile, change, ds, args, f1, f2, diff_flags);
+
+	if (fseek(f1, pos1, SEEK_SET) == -1) {
+		err = got_ferror(f1, GOT_ERR_IO);
+		goto done;
+	}
+	if (fseek(f2, pos2, SEEK_SET) == -1) {
+		err = got_ferror(f1, GOT_ERR_IO);
+		goto done;
+	}
+	if (fseek(hunkfile, 0L, SEEK_SET) == -1) {
+		err = got_ferror(hunkfile, GOT_ERR_IO);
+		goto done;
+	}
+
+	err = (*patch_cb)(choice, patch_arg, GOT_STATUS_MODIFY, relpath,
+	    hunkfile, n, nchanges);
+	if (err)
+		goto done;
+
+	switch (*choice) {
+	case GOT_PATCH_CHOICE_YES:
+		err = copy_change(f1, f2, line_cur1, line_cur2, start_old,
+		    end_old, start_new, end_new, outfile, rejectfile);
+		break;
+	case GOT_PATCH_CHOICE_NO:
+		err = copy_change(f1, f2, line_cur1, line_cur2, start_old,
+		    end_old, start_new, end_new, rejectfile, outfile);
+		break;
+	case GOT_PATCH_CHOICE_QUIT:
+		if (outfile) {
+			/* Copy old file's lines until EOF. */
+			while (!feof(f1)) {
+				err = copy_one_line(f1, outfile, NULL);
+				if (err)
+					goto done;
+				(*line_cur1)++;
+			}
+		}
+		if (rejectfile) {
+			/* Copy new file's lines until EOF. */
+			while (!feof(f2)) {
+				err = copy_one_line(f2, NULL, rejectfile);
+				if (err)
+					goto done;
+				(*line_cur2)++;
+			}
+		}
+		break;
+	default:
+		err = got_error(GOT_ERR_PATCH_CHOICE);
+		break;
+	}
+done:
+	if (hunkfile && fclose(hunkfile) == EOF && err == NULL)
+		err = got_error_from_errno("fclose");
+	return err;
+}
+
 struct revert_file_args {
 	struct got_worktree *worktree;
 	struct got_fileindex *fileindex;
 	got_worktree_checkout_cb progress_cb;
 	void *progress_arg;
+	got_worktree_patch_cb patch_cb;
+	void *patch_arg;
 	struct got_repository *repo;
 };
 
 static const struct got_error *
+create_reverted_content(char **path_outfile, struct got_object_id *blob_id,
+    const char *path2, const char *relpath, struct got_repository *repo,
+    got_worktree_patch_cb patch_cb, void *patch_arg)
+{
+	const struct got_error *err;
+	struct got_blob_object *blob = NULL;
+	FILE *f1 = NULL, *f2 = NULL, *outfile = NULL;
+	char *path1 = NULL, *id_str = NULL;
+	struct stat sb1, sb2;
+	struct got_diff_changes *changes = NULL;
+	struct got_diff_state *ds = NULL;
+	struct got_diff_args *args = NULL;
+	struct got_diff_change *change;
+	int diff_flags = 0, line_cur1 = 1, line_cur2 = 1, have_content = 0;
+	int n = 0;
+
+	*path_outfile = NULL;
+
+	err = got_object_id_str(&id_str, blob_id);
+	if (err)
+		return err;
+
+	f2 = fopen(path2, "r");
+	if (f2 == NULL) {
+		err = got_error_from_errno2("fopen", path2);
+		goto done;
+	}
+
+	err = got_object_open_as_blob(&blob, repo, blob_id, 8192);
+	if (err)
+		goto done;
+
+	err = got_opentemp_named(&path1, &f1, "got-stage-blob");
+	if (err)
+		goto done;
+
+	err = got_object_blob_dump_to_file(NULL, NULL, NULL, f1, blob);
+	if (err)
+		goto done;
+
+	if (stat(path1, &sb1) == -1) {
+		err = got_error_from_errno2("stat", path1);
+		goto done;
+	}
+	if (stat(path2, &sb2) == -1) {
+		err = got_error_from_errno2("stat", path2);
+		goto done;
+	}
+
+	err = got_diff_files(&changes, &ds, &args, &diff_flags,
+	    f1, sb1.st_size, id_str, f2, sb2.st_size, path2, 3, NULL);
+	if (err)
+		goto done;
+
+	err = got_opentemp_named(path_outfile, &outfile, "got-stage-content");
+	if (err)
+		goto done;
+
+	if (fseek(f1, 0L, SEEK_SET) == -1)
+		return got_ferror(f1, GOT_ERR_IO);
+	if (fseek(f2, 0L, SEEK_SET) == -1)
+		return got_ferror(f2, GOT_ERR_IO);
+	SIMPLEQ_FOREACH(change, &changes->entries, entry) {
+		int choice;
+		err = apply_or_reject_change(&choice, change, ++n,
+		    changes->nchanges, ds, args, diff_flags, relpath,
+		    f1, f2, &line_cur1, &line_cur2,
+		    NULL, outfile, patch_cb, patch_arg);
+		if (err)
+			goto done;
+		if (choice == GOT_PATCH_CHOICE_YES)
+			have_content = 1;
+		else if (choice == GOT_PATCH_CHOICE_QUIT)
+			break;
+	}
+done:
+	free(id_str);
+	if (blob)
+		got_object_blob_close(blob);
+	if (f1 && fclose(f1) == EOF && err == NULL)
+		err = got_error_from_errno2("fclose", path1);
+	if (f2 && fclose(f2) == EOF && err == NULL)
+		err = got_error_from_errno2("fclose", path2);
+	if (outfile && fclose(outfile) == EOF && err == NULL)
+		err = got_error_from_errno2("fclose", *path_outfile);
+	if (path1 && unlink(path1) == -1 && err == NULL)
+		err = got_error_from_errno2("unlink", path1);
+	if (err || !have_content) {
+		if (*path_outfile && unlink(*path_outfile) == -1 && err == NULL)
+			err = got_error_from_errno2("unlink", *path_outfile);
+		free(*path_outfile);
+		*path_outfile = NULL;
+	}
+	free(args);
+	if (ds) {
+		got_diff_state_free(ds);
+		free(ds);
+	}
+	if (changes)
+		got_diff_free_changes(changes);
+	free(path1);
+	return err;
+}
+
+static const struct got_error *
 revert_file(void *arg, unsigned char status, unsigned char staged_status,
     const char *relpath, struct got_object_id *blob_id,
     struct got_object_id *staged_blob_id, struct got_object_id *commit_id)
@@ -2704,7 +2994,7 @@ revert_file(void *arg, unsigned char status, unsigned 
 	struct got_object_id *tree_id = NULL;
 	const struct got_tree_entry *te = NULL;
 	char *tree_path = NULL, *te_name;
-	char *ondisk_path = NULL;
+	char *ondisk_path = NULL, *path_content = NULL;
 	struct got_blob_object *blob = NULL;
 
 	/* Reverting a staged deletion is a no-op. */
@@ -2777,6 +3067,15 @@ revert_file(void *arg, unsigned char status, unsigned 
 
 	switch (status) {
 	case GOT_STATUS_ADD:
+		if (a->patch_cb) {
+			int choice = GOT_PATCH_CHOICE_NONE;
+			err = (*a->patch_cb)(&choice, a->patch_arg,
+			    status, ie->path, NULL, 1, 1);
+			if (err)
+				goto done;
+			if (choice != GOT_PATCH_CHOICE_YES)
+				break;
+		}
 		err = (*a->progress_cb)(a->progress_arg, GOT_STATUS_REVERT,
 		    ie->path);
 		if (err)
@@ -2784,6 +3083,16 @@ revert_file(void *arg, unsigned char status, unsigned 
 		got_fileindex_entry_remove(a->fileindex, ie);
 		break;
 	case GOT_STATUS_DELETE:
+		if (a->patch_cb) {
+			int choice = GOT_PATCH_CHOICE_NONE;
+			err = (*a->patch_cb)(&choice, a->patch_arg,
+			    status, ie->path, NULL, 1, 1);
+			if (err)
+				goto done;
+			if (choice != GOT_PATCH_CHOICE_YES)
+				break;
+		}
+		/* fall through */
 	case GOT_STATUS_MODIFY:
 	case GOT_STATUS_CONFLICT:
 	case GOT_STATUS_MISSING: {
@@ -2805,17 +3114,32 @@ revert_file(void *arg, unsigned char status, unsigned 
 			goto done;
 		}
 
-		err = install_blob(a->worktree, ondisk_path, ie->path,
-		    te ? te->mode : GOT_DEFAULT_FILE_MODE,
-		    got_fileindex_perms_to_st(ie), blob, 0, 1,
-		    a->repo, a->progress_cb, a->progress_arg);
-		if (err)
-			goto done;
-		if (status == GOT_STATUS_DELETE) {
-			err = update_blob_fileindex_entry(a->worktree,
-			    a->fileindex, ie, ondisk_path, ie->path, blob, 1);
+		if (a->patch_cb && (status == GOT_STATUS_MODIFY ||
+		    status == GOT_STATUS_CONFLICT)) {
+			err = create_reverted_content(&path_content, &id,
+			    ondisk_path, ie->path, a->repo,
+			    a->patch_cb, a->patch_arg);
+			if (err || path_content == NULL)
+				break;
+			if (rename(path_content, ondisk_path) == -1) {
+				err = got_error_from_errno3("rename",
+				    path_content, ondisk_path);
+				goto done;
+			}
+		} else {
+			err = install_blob(a->worktree, ondisk_path, ie->path,
+			    te ? te->mode : GOT_DEFAULT_FILE_MODE,
+			    got_fileindex_perms_to_st(ie), blob, 0, 1,
+			    a->repo, a->progress_cb, a->progress_arg);
 			if (err)
 				goto done;
+			if (status == GOT_STATUS_DELETE) {
+				err = update_blob_fileindex_entry(a->worktree,
+				    a->fileindex, ie, ondisk_path, ie->path,
+				    blob, 1);
+				if (err)
+					goto done;
+			}
 		}
 		break;
 	}
@@ -2824,6 +3148,7 @@ revert_file(void *arg, unsigned char status, unsigned 
 	}
 done:
 	free(ondisk_path);
+	free(path_content);
 	free(parent_path);
 	free(tree_path);
 	if (blob)
@@ -2838,6 +3163,7 @@ const struct got_error *
 got_worktree_revert(struct got_worktree *worktree,
     struct got_pathlist_head *paths,
     got_worktree_checkout_cb progress_cb, void *progress_arg,
+    got_worktree_patch_cb patch_cb, void *patch_arg,
     struct got_repository *repo)
 {
 	struct got_fileindex *fileindex = NULL;
@@ -2859,6 +3185,8 @@ got_worktree_revert(struct got_worktree *worktree,
 	rfa.fileindex = fileindex;
 	rfa.progress_cb = progress_cb;
 	rfa.progress_arg = progress_arg;
+	rfa.patch_cb = patch_cb;
+	rfa.patch_arg = patch_arg;
 	rfa.repo = repo;
 	TAILQ_FOREACH(pe, paths, entry) {
 		err = worktree_status(worktree, pe->path, fileindex, repo,
@@ -4617,6 +4945,8 @@ got_worktree_rebase_abort(struct got_worktree *worktre
 	rfa.fileindex = fileindex;
 	rfa.progress_cb = progress_cb;
 	rfa.progress_arg = progress_arg;
+	rfa.patch_cb = NULL;
+	rfa.patch_arg = NULL;
 	rfa.repo = repo;
 	err = worktree_status(worktree, "", fileindex, repo,
 	    revert_file, &rfa, NULL, NULL);
@@ -4968,6 +5298,8 @@ got_worktree_histedit_abort(struct got_worktree *workt
 	rfa.fileindex = fileindex;
 	rfa.progress_cb = progress_cb;
 	rfa.progress_arg = progress_arg;
+	rfa.patch_cb = NULL;
+	rfa.patch_arg = NULL;
 	rfa.repo = repo;
 	err = worktree_status(worktree, "", fileindex, repo,
 	    revert_file, &rfa, NULL, NULL);
@@ -5115,193 +5447,10 @@ check_stage_ok(void *arg, unsigned char status,
 	    GOT_ERR_STAGE_OUT_OF_DATE);
 done:
 	free(in_repo_path);
-	return err;
-}
-
-static const struct got_error *
-copy_one_line(FILE *infile, FILE *outfile, FILE *rejectfile)
-{
-	const struct got_error *err = NULL;
-	char *line = NULL;
-	size_t linesize = 0, n;
-	ssize_t linelen;
-
-	linelen = getline(&line, &linesize, infile);
-	if (linelen == -1) {
-		if (ferror(infile)) {
-			err = got_error_from_errno("getline");
-			goto done;
-		}
-		return NULL;
-	}
-	if (outfile) {
-		n = fwrite(line, 1, linelen, outfile);
-		if (n != linelen) {
-			err = got_ferror(outfile, GOT_ERR_IO);
-			goto done;
-		}
-	}
-	if (rejectfile) {
-		n = fwrite(line, 1, linelen, rejectfile);
-		if (n != linelen)
-			err = got_ferror(outfile, GOT_ERR_IO);
-	}
-done:
-	free(line);
 	return err;
 }
 
 static const struct got_error *
-skip_one_line(FILE *f)
-{
-	char *line = NULL;
-	size_t linesize = 0;
-	ssize_t linelen;
-
-	linelen = getline(&line, &linesize, f);
-	free(line);
-	if (linelen == -1 && ferror(f))
-		return got_error_from_errno("getline");
-	return NULL;
-}
-
-static const struct got_error *
-copy_change(FILE *f1, FILE *f2, int *line_cur1, int *line_cur2,
-    int start_old, int end_old, int start_new, int end_new,
-    FILE *outfile, FILE *rejectfile)
- {
-	const struct got_error *err;
-
-	/* Copy old file's lines leading up to patch. */
-	while (!feof(f1) && *line_cur1 < start_old) {
-		err = copy_one_line(f1, outfile, NULL);
-		if (err)
-			return err;
-		(*line_cur1)++;
-	}
-	/* Skip new file's lines leading up to patch. */
-	while (!feof(f2) && *line_cur2 < start_new) {
-		if (rejectfile)
-			err = copy_one_line(f2, NULL, rejectfile);
-		else
-			err = skip_one_line(f2);
-		if (err)
-			return err;
-		(*line_cur2)++;
-	}
-	/* Copy patched lines. */
-	while (!feof(f2) && *line_cur2 <= end_new) {
-		err = copy_one_line(f2, outfile, NULL);
-		if (err)
-			return err;
-		(*line_cur2)++;
-	}
-	/* Skip over old file's replaced lines. */
-	while (!feof(f1) && *line_cur1 <= end_new) {
-		if (rejectfile)
-			err = copy_one_line(f1, NULL, rejectfile);
-		else
-			err = skip_one_line(f1);
-		if (err)
-			return err;
-		(*line_cur1)++;
-	}
-	/* Copy old file's lines after patch. */
-	while (!feof(f1) && *line_cur1 <= end_old) {
-		err = copy_one_line(f1, outfile, rejectfile);
-		if (err)
-			return err;
-		(*line_cur1)++;
-	}
-
-	return NULL;
-}
-
-static const struct got_error *
-apply_or_reject_change(int *choice, struct got_diff_change *change, int n,
-    int nchanges, struct got_diff_state *ds, struct got_diff_args *args,
-    int diff_flags, const char *relpath, FILE *f1, FILE *f2, int *line_cur1,
-    int *line_cur2, FILE *outfile, FILE *rejectfile,
-    got_worktree_patch_cb patch_cb, void *patch_arg)
-{
-	const struct got_error *err = NULL;
-	int start_old = change->cv.a;
-	int end_old = change->cv.b;
-	int start_new = change->cv.c;
-	int end_new = change->cv.d;
-	long pos1, pos2;
-	FILE *hunkfile;
-
-	*choice = GOT_PATCH_CHOICE_NONE;
-
-	hunkfile = got_opentemp();
-	if (hunkfile == NULL)
-		return got_error_from_errno("got_opentemp");
-
-	pos1 = ftell(f1);
-	pos2 = ftell(f2);
-
-	/* XXX TODO needs error checking */
-	got_diff_dump_change(hunkfile, change, ds, args, f1, f2, diff_flags);
-
-	if (fseek(f1, pos1, SEEK_SET) == -1) {
-		err = got_ferror(f1, GOT_ERR_IO);
-		goto done;
-	}
-	if (fseek(f2, pos2, SEEK_SET) == -1) {
-		err = got_ferror(f1, GOT_ERR_IO);
-		goto done;
-	}
-	if (fseek(hunkfile, 0L, SEEK_SET) == -1) {
-		err = got_ferror(hunkfile, GOT_ERR_IO);
-		goto done;
-	}
-
-	err = (*patch_cb)(choice, patch_arg, GOT_STATUS_MODIFY, relpath,
-	    hunkfile, n, nchanges);
-	if (err)
-		goto done;
-
-	switch (*choice) {
-	case GOT_PATCH_CHOICE_YES:
-		err = copy_change(f1, f2, line_cur1, line_cur2, start_old,
-		    end_old, start_new, end_new, outfile, rejectfile);
-		break;
-	case GOT_PATCH_CHOICE_NO:
-		err = copy_change(f1, f2, line_cur1, line_cur2, start_old,
-		    end_old, start_new, end_new, rejectfile, outfile);
-		break;
-	case GOT_PATCH_CHOICE_QUIT:
-		if (outfile) {
-			/* Copy old file's lines until EOF. */
-			while (!feof(f1)) {
-				err = copy_one_line(f1, outfile, NULL);
-				if (err)
-					goto done;
-				(*line_cur1)++;
-			}
-		}
-		if (rejectfile) {
-			/* Copy new file's lines until EOF. */
-			while (!feof(f2)) {
-				err = copy_one_line(f2, NULL, rejectfile);
-				if (err)
-					goto done;
-				(*line_cur2)++;
-			}
-		}
-		break;
-	default:
-		err = got_error(GOT_ERR_PATCH_CHOICE);
-		break;
-	}
-done:
-	if (hunkfile && fclose(hunkfile) == EOF && err == NULL)
-		err = got_error_from_errno("fclose");
-	return err;
-}
-
-static const struct got_error *
 create_staged_content(char **path_outfile, struct got_object_id *blob_id,
     const char *path2, const char *relpath, struct got_repository *repo,
     got_worktree_patch_cb patch_cb, void *patch_arg)
blob - 67677ab2ab97f83a684ffa2a5abaaf6c5e078d38
blob + 4ef46ac70695dfee3f56957de47bc0f1a7b14a23
--- regress/cmdline/revert.sh
+++ regress/cmdline/revert.sh
@@ -250,7 +250,8 @@ function test_revert_no_arguments {
 		return 1
 	fi
 
-	echo "usage: got revert [-R] path ..." > $testroot/stderr.expected
+	echo "usage: got revert [-p] [-F response-script] [-R] path ..." \
+		> $testroot/stderr.expected
 	cmp -s $testroot/stderr.expected $testroot/stderr
 	ret="$?"
 	if [ "$ret" != "0" ]; then
@@ -315,11 +316,383 @@ function test_revert_directory {
 	ret="$?"
 	if [ "$ret" != "0" ]; then
 		diff -u $testroot/content.expected $testroot/content
+	fi
+	test_done "$testroot" "$ret"
+
+}
+
+function test_revert_patch {
+	local testroot=`test_init revert_patch`
+
+	jot 16 > $testroot/repo/numbers
+	(cd $testroot/repo && git add numbers)
+	git_commit $testroot/repo -m "added numbers file"
+	local commit_id=`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
+
+	sed -i -e 's/^2$/a/' $testroot/wt/numbers
+	sed -i -e 's/^7$/b/' $testroot/wt/numbers
+	sed -i -e 's/^16$/c/' $testroot/wt/numbers
+
+	(cd $testroot/wt && got diff > $testroot/numbers.diff)
+
+	# don't revert any hunks
+	printf "n\nn\nn\n" > $testroot/patchscript
+	(cd $testroot/wt && got revert -F $testroot/patchscript -p \
+		numbers > $testroot/stdout)
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		echo "got revert command failed unexpectedly" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+	cat > $testroot/stdout.expected <<EOF
+-----------------------------------------------
+@@ -1,5 +1,5 @@
+ 1
+-2
++a
+ 3
+ 4
+ 5
+-----------------------------------------------
+M  numbers (change 1 of 3)
+revert this change? [y/n/q] n
+-----------------------------------------------
+@@ -4,7 +4,7 @@
+ 4
+ 5
+ 6
+-7
++b
+ 8
+ 9
+ 10
+-----------------------------------------------
+M  numbers (change 2 of 3)
+revert this change? [y/n/q] n
+-----------------------------------------------
+@@ -13,4 +13,4 @@
+ 13
+ 14
+ 15
+-16
++c
+-----------------------------------------------
+M  numbers (change 3 of 3)
+revert this change? [y/n/q] n
+EOF
+	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 "M  numbers" > $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 diff > $testroot/stdout)
+	cmp -s $testroot/numbers.diff $testroot/stdout
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/numbers.diff $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	# revert middle hunk
+	printf "n\ny\nn\n" > $testroot/patchscript
+	(cd $testroot/wt && got revert -F $testroot/patchscript -p \
+		numbers > $testroot/stdout)
+
+	cat > $testroot/stdout.expected <<EOF
+-----------------------------------------------
+@@ -1,5 +1,5 @@
+ 1
+-2
++a
+ 3
+ 4
+ 5
+-----------------------------------------------
+M  numbers (change 1 of 3)
+revert this change? [y/n/q] n
+-----------------------------------------------
+@@ -4,7 +4,7 @@
+ 4
+ 5
+ 6
+-7
++b
+ 8
+ 9
+ 10
+-----------------------------------------------
+M  numbers (change 2 of 3)
+revert this change? [y/n/q] y
+-----------------------------------------------
+@@ -13,4 +13,4 @@
+ 13
+ 14
+ 15
+-16
++c
+-----------------------------------------------
+M  numbers (change 3 of 3)
+revert this change? [y/n/q] n
+EOF
+	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 "M  numbers" > $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 diff > $testroot/stdout)
+
+	echo "diff $commit_id $testroot/wt" > $testroot/stdout.expected
+	echo -n 'blob - ' >> $testroot/stdout.expected
+	got tree -r $testroot/repo -i -c $commit_id \
+		| grep 'numbers$' | cut -d' ' -f 1 \
+		>> $testroot/stdout.expected
+	echo 'file + numbers' >> $testroot/stdout.expected
+	cat >> $testroot/stdout.expected <<EOF
+--- numbers
++++ numbers
+@@ -1,5 +1,5 @@
+ 1
+-2
++a
+ 3
+ 4
+ 5
+@@ -13,4 +13,4 @@
+ 13
+ 14
+ 15
+-16
++c
+EOF
+	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
+
+	# revert last hunk
+	printf "n\ny\n" > $testroot/patchscript
+	(cd $testroot/wt && got revert -F $testroot/patchscript -p \
+		numbers > $testroot/stdout)
+	cat > $testroot/stdout.expected <<EOF
+-----------------------------------------------
+@@ -1,5 +1,5 @@
+ 1
+-2
++a
+ 3
+ 4
+ 5
+-----------------------------------------------
+M  numbers (change 1 of 2)
+revert this change? [y/n/q] n
+-----------------------------------------------
+@@ -13,4 +13,4 @@
+ 13
+ 14
+ 15
+-16
++c
+-----------------------------------------------
+M  numbers (change 2 of 2)
+revert this change? [y/n/q] y
+EOF
+	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 diff > $testroot/stdout)
+
+	echo "diff $commit_id $testroot/wt" > $testroot/stdout.expected
+	echo -n 'blob - ' >> $testroot/stdout.expected
+	got tree -r $testroot/repo -i -c $commit_id \
+		| grep 'numbers$' | cut -d' ' -f 1 \
+		>> $testroot/stdout.expected
+	echo 'file + numbers' >> $testroot/stdout.expected
+	cat >> $testroot/stdout.expected <<EOF
+--- numbers
++++ numbers
+@@ -1,5 +1,5 @@
+ 1
+-2
++a
+ 3
+ 4
+ 5
+EOF
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+	fi
 	test_done "$testroot" "$ret"
+}
 
+function test_revert_patch_added {
+	local testroot=`test_init revert_patch_added`
+	local commit_id=`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
+
+	echo "new" > $testroot/wt/epsilon/new
+	(cd $testroot/wt && got add epsilon/new > /dev/null)
+
+	printf "n\n" > $testroot/patchscript
+	(cd $testroot/wt && got revert -F $testroot/patchscript -p \
+		epsilon/new > $testroot/stdout)
+
+	echo "A  epsilon/new" > $testroot/stdout.expected
+	echo "revert this addition? [y/n] 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 status > $testroot/stdout)
+	echo "A  epsilon/new" > $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
+
+	printf "y\n" > $testroot/patchscript
+	(cd $testroot/wt && got revert -F $testroot/patchscript -p \
+		epsilon/new > $testroot/stdout)
+
+	echo "A  epsilon/new" > $testroot/stdout.expected
+	echo "revert this addition? [y/n] y" >> $testroot/stdout.expected
+	echo "R  epsilon/new" >> $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 "?  epsilon/new" > $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"
 }
 
+function test_revert_patch_removed {
+	local testroot=`test_init revert_patch_removed`
+	local commit_id=`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
+
+	(cd $testroot/wt && got rm beta > /dev/null)
+
+	printf "n\n" > $testroot/patchscript
+	(cd $testroot/wt && got revert -F $testroot/patchscript -p \
+		beta > $testroot/stdout)
+	echo "D  beta" > $testroot/stdout.expected
+	echo "revert this deletion? [y/n] 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 status > $testroot/stdout)
+	echo "D  beta" > $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
+
+	printf "y\n" > $testroot/patchscript
+	(cd $testroot/wt && got revert -F $testroot/patchscript -p \
+		beta > $testroot/stdout)
+
+	echo "D  beta" > $testroot/stdout.expected
+	echo "revert this deletion? [y/n] y" >> $testroot/stdout.expected
+	echo "R  beta" >> $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
+	fi
+	test_done "$testroot" "$ret"
+}
+
 run_test test_revert_basic
 run_test test_revert_rm
 run_test test_revert_add
@@ -327,3 +700,6 @@ run_test test_revert_multiple
 run_test test_revert_file_in_new_subdir
 run_test test_revert_no_arguments
 run_test test_revert_directory
+run_test test_revert_patch
+run_test test_revert_patch_added
+run_test test_revert_patch_removed