Commit Diff


commit - ad7de6a5165442ec89b9daaadb18901a981befee
commit + 2e1f37b02955aef4d5bd0d0307e98da1e4b95463
blob - 180e1f604e161320c60fb88596033366830d1a36
blob + a279b6139588894ca1f2cb2ef36a76a68ab140a1
--- got/got.1
+++ got/got.1
@@ -971,7 +971,7 @@ and may then be staged again if necessary.
 .It Cm sg
 Short alias for
 .Cm stage .
-.It Cm unstage [ Ar path ... ]
+.It Cm unstage [ Fl p ] [ Fl F Ar response-script ] [ Ar path ... ]
 Merge staged changes back into the work tree and put affected paths
 back into non-staged status.
 If no
@@ -988,6 +988,35 @@ Show the status of each affected file, using the follo
 .It d Ta file's deletion was obstructed by local modifications
 .It ~ Ta changes destined for a non-regular file were not merged
 .El
+.Pp
+The options for
+.Cm got unstage
+are as follows:
+.Bl -tag -width Ds
+.It Fl p
+Instead of unstaging the entire content of a changed file, interactively
+select or reject changes for unstaging based on
+.Dq y
+(unstage change),
+.Dq n
+(keep change staged), and
+.Dq q
+(quit unstaging this file) responses.
+If a file is staged in modified status, individual patches derived from the
+staged file content can be unstaged.
+Files staged in added or deleted status may only be unstaged 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.
+.El
 .It Cm ug
 Short alias for
 .Cm unstage .
blob - af3c2e76e6aa26242c5c90e65a3a35249dc0f83e
blob + c140d3621c853710a48a9125b23fc43d77e61798
--- got/got.c
+++ got/got.c
@@ -5194,7 +5194,7 @@ print_stage(void *arg, unsigned char status, unsigned 
 
 static const struct got_error *
 show_change(unsigned char status, const char *path, FILE *patch_file, int n,
-    int nchanges)
+    int nchanges, const char *action)
 {
 	char *line = NULL;
 	size_t linesize = 0;
@@ -5202,10 +5202,10 @@ show_change(unsigned char status, const char *path, FI
 
 	switch (status) {
 	case GOT_STATUS_ADD:
-		printf("A  %s\nstage this addition? [y/n] ", path);
+		printf("A  %s\n%s this addition? [y/n] ", path, action);
 		break;
 	case GOT_STATUS_DELETE:
-		printf("D  %s\nstage this deletion? [y/n] ", path);
+		printf("D  %s\n%s this deletion? [y/n] ", path, action);
 		break;
 	case GOT_STATUS_MODIFY:
 		if (fseek(patch_file, 0L, SEEK_SET) == -1)
@@ -5216,8 +5216,8 @@ show_change(unsigned char status, const char *path, FI
 		if (ferror(patch_file))
 			return got_error_from_errno("getline");
 		printf(GOT_COMMIT_SEP_STR);
-		printf("M  %s (change %d of %d)\nstage this change? [y/n/q] ",
-		    path, n, nchanges);
+		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);
@@ -5225,6 +5225,11 @@ show_change(unsigned char status, const char *path, FI
 
 	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,
@@ -5235,18 +5240,19 @@ choose_patch(int *choice, void *arg, unsigned char sta
 	size_t linesize = 0;
 	ssize_t linelen;
 	int resp = ' ';
-	FILE *patch_script_file = arg;
+	struct choose_patch_arg *a = arg;
 
 	*choice = GOT_PATCH_CHOICE_NONE;
 
-	if (patch_script_file) {
+	if (a->patch_script_file) {
 		char *nl;
-		err = show_change(status, path, patch_file, n, nchanges);
+		err = show_change(status, path, patch_file, n, nchanges,
+		    a->action);
 		if (err)
 			return err;
-		linelen = getline(&line, &linesize, patch_script_file);
+		linelen = getline(&line, &linesize, a->patch_script_file);
 		if (linelen == -1) {
-			if (ferror(patch_script_file))
+			if (ferror(a->patch_script_file))
 				return got_error_from_errno("getline");
 			return NULL;
 		}
@@ -5270,7 +5276,8 @@ choose_patch(int *choice, void *arg, unsigned char sta
 	}
 
 	while (resp != 'y' && resp != 'n' && resp != 'q') {
-		err = show_change(status, path, patch_file, n, nchanges);
+		err = show_change(status, path, patch_file, n, nchanges,
+		    a->action);
 		if (err)
 			return err;
 		resp = getchar();
@@ -5307,8 +5314,9 @@ cmd_stage(int argc, char *argv[])
 	struct got_pathlist_head paths;
 	struct got_pathlist_entry *pe;
 	int ch, list_stage = 0, pflag = 0;
-	const char *patch_script_path = NULL;
 	FILE *patch_script_file = NULL;
+	const char *patch_script_path = NULL;
+	struct choose_patch_arg cpa;
 
 	TAILQ_INIT(&paths);
 
@@ -5376,11 +5384,17 @@ cmd_stage(int argc, char *argv[])
 	if (list_stage)
 		error = got_worktree_status(worktree, &paths, repo,
 		    print_stage, NULL, check_cancelled, NULL);
-	else
+	else {
+		cpa.patch_script_file = patch_script_file;
+		cpa.action = "stage";
 		error = got_worktree_stage(worktree, &paths,
 		    pflag ? NULL : print_status, NULL,
-		    pflag ? choose_patch : NULL, patch_script_file, repo);
+		    pflag ? choose_patch : NULL, &cpa, repo);
+	}
 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)
@@ -5395,7 +5409,8 @@ done:
 __dead static void
 usage_unstage(void)
 {
-	fprintf(stderr, "usage: %s unstage [file-path ...]\n",
+	fprintf(stderr, "usage: %s unstage [-p] [-F response-script] "
+	    "[file-path ...]\n",
 	    getprogname());
 	exit(1);
 }
@@ -5410,12 +5425,21 @@ cmd_unstage(int argc, char *argv[])
 	char *cwd = NULL;
 	struct got_pathlist_head paths;
 	struct got_pathlist_entry *pe;
-	int ch, did_something = 0;
+	int ch, did_something = 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, "")) != -1) {
+	while ((ch = getopt(argc, argv, "pF:")) != -1) {
 		switch (ch) {
+		case 'p':
+			pflag = 1;
+			break;
+		case 'F':
+			patch_script_path = optarg;
+			break;
 		default:
 			usage_unstage();
 			/* NOTREACHED */
@@ -5430,6 +5454,9 @@ cmd_unstage(int argc, char *argv[])
 	    "unveil", NULL) == -1)
 		err(1, "pledge");
 #endif
+	if (patch_script_path && !pflag)
+		errx(1, "-F option can only be used together with -p option");
+
 	cwd = getcwd(NULL, 0);
 	if (cwd == NULL) {
 		error = got_error_from_errno("getcwd");
@@ -5444,6 +5471,15 @@ cmd_unstage(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)
@@ -5453,9 +5489,14 @@ cmd_unstage(int argc, char *argv[])
 	if (error)
 		goto done;
 
+	cpa.patch_script_file = patch_script_file;
+	cpa.action = "unstage";
 	error = got_worktree_unstage(worktree, &paths, update_progress,
-	    &did_something, repo);
+	    &did_something, pflag ? choose_patch : NULL, &cpa, repo);
 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)
blob - b07be0075e7e1ea62cda811facea1131cf1463c0
blob + ff51861f2e2d1f238ff5c2e4b183ecf93de910ae
--- include/got_worktree.h
+++ include/got_worktree.h
@@ -398,4 +398,4 @@ const struct got_error *got_worktree_stage(struct got_
  */
 const struct got_error *got_worktree_unstage(struct got_worktree *,
     struct got_pathlist_head *, got_worktree_checkout_cb, void *,
-    struct got_repository *);
+    got_worktree_patch_cb, void *, struct got_repository *);
blob - 04dedb5234881a9d6726e1e8da27d6eb707ceaaa
blob + bd8465018eb5fe3c7e15d2992d4119c5a58f1089
--- lib/worktree.c
+++ lib/worktree.c
@@ -5168,7 +5168,7 @@ done:
 }
 
 static const struct got_error *
-copy_one_line(FILE *infile, FILE *outfile)
+copy_one_line(FILE *infile, FILE *outfile, FILE *rejectfile)
 {
 	const struct got_error *err = NULL;
 	char *line = NULL;
@@ -5183,9 +5183,18 @@ copy_one_line(FILE *infile, FILE *outfile)
 		}
 		return NULL;
 	}
-	n = fwrite(line, 1, linelen, outfile);
-	if (n != linelen)
-		err = got_ferror(outfile, GOT_ERR_IO);
+	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;
@@ -5206,11 +5215,63 @@ skip_one_line(FILE *f)
 }
 
 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, got_worktree_patch_cb patch_cb,
-    void *patch_arg)
+    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;
@@ -5252,62 +5313,17 @@ apply_or_reject_change(int *choice, struct got_diff_ch
 
 	switch (*choice) {
 	case GOT_PATCH_CHOICE_YES:
-		/* Copy old file's lines leading up to patch. */
-		while (!feof(f1) && *line_cur1 < start_old) {
-			err = copy_one_line(f1, outfile);
-			if (err)
-				goto done;
-			(*line_cur1)++;
-		}
-		/* Skip new file's lines leading up to patch. */
-		while (!feof(f2) && *line_cur2 < start_new) {
-			err = skip_one_line(f2);
-			if (err)
-				goto done;
-			(*line_cur2)++;
-		}
-		/* Copy patched lines. */
-		while (!feof(f2) && *line_cur2 <= end_new) {
-			err = copy_one_line(f2, outfile);
-			if (err)
-				goto done;
-			(*line_cur2)++;
-		}
-		/* Skip over old file's replaced lines. */
-		while (!feof(f1) && *line_cur1 <= end_new) {
-			err = skip_one_line(f1);
-			if (err)
-				goto done;
-			(*line_cur1)++;
-		}
-		/* Copy old file's lines after patch. */
-		while (!feof(f1) && *line_cur1 <= end_old) {
-			err = skip_one_line(f1);
-			if (err)
-				goto done;
-			(*line_cur1)++;
-		}
+		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:
-		/* Copy old file's lines. */
-		while (!feof(f1) && *line_cur1 <= end_old) {
-			err = copy_one_line(f1, outfile);
-			if (err)
-				goto done;
-			(*line_cur1)++;
-		}
-		/* Skip over new file's lines. */
-		while (!feof(f2) && *line_cur2 <= end_new) {
-			err = skip_one_line(f2);
-			if (err)
-				goto done;
-			(*line_cur2)++;
-		}
+		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:
 		/* Copy old file's lines until EOF. */
 		while (!feof(f1)) {
-			err = copy_one_line(f1, outfile);
+			err = copy_one_line(f1, outfile, rejectfile);
 			if (err)
 				goto done;
 			(*line_cur1)++;
@@ -5391,7 +5407,7 @@ create_staged_content(char **path_outfile, struct got_
 		err = apply_or_reject_change(&choice, change, ++n,
 		    changes->nchanges, ds, args, diff_flags, relpath,
 		    f1, f2, &line_cur1, &line_cur2,
-		    outfile, patch_cb, patch_arg);
+		    outfile, NULL, patch_cb, patch_arg);
 		if (err)
 			goto done;
 		if (choice == GOT_PATCH_CHOICE_YES)
@@ -5620,8 +5636,152 @@ struct unstage_path_arg {
 	struct got_repository *repo;
 	got_worktree_checkout_cb progress_cb;
 	void *progress_arg;
+	got_worktree_patch_cb patch_cb;
+	void *patch_arg;
 };
+
+static const struct got_error *
+create_unstaged_content(char **path_unstaged_content,
+    char **path_new_staged_content, struct got_object_id *blob_id,
+    struct got_object_id *staged_blob_id, 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, *staged_blob = NULL;
+	FILE *f1 = NULL, *f2 = NULL, *outfile = NULL, *rejectfile = NULL;
+	char *path1 = NULL, *path2 = NULL, *label1 = 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, n = 0;
+	int have_content = 0, have_rejected_content = 0;
+
+	*path_unstaged_content = NULL;
+	*path_new_staged_content = NULL;
+
+	err = got_object_id_str(&label1, blob_id);
+	if (err)
+		return err;
+	err = got_object_open_as_blob(&blob, repo, blob_id, 8192);
+	if (err)
+		goto done;
+
+	err = got_opentemp_named(&path1, &f1, "got-unstage-blob-base");
+	if (err)
+		goto done;
+
+	err = got_object_blob_dump_to_file(NULL, NULL, NULL, f1, blob);
+	if (err)
+		goto done;
+
+	err = got_object_open_as_blob(&staged_blob, repo, staged_blob_id, 8192);
+	if (err)
+		goto done;
+
+	err = got_opentemp_named(&path2, &f2, "got-unstage-blob-staged");
+	if (err)
+		goto done;
 
+	err = got_object_blob_dump_to_file(NULL, NULL, NULL, f2, staged_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, label1, f2, sb2.st_size, path2, 3, NULL);
+	if (err)
+		goto done;
+
+	err = got_opentemp_named(path_unstaged_content, &outfile,
+	    "got-unstaged-content");
+	if (err)
+		goto done;
+	err = got_opentemp_named(path_new_staged_content, &rejectfile,
+	    "got-new-staged-content");
+	if (err)
+		goto done;
+
+	if (fseek(f1, 0L, SEEK_SET) == -1) {
+		err = got_ferror(f1, GOT_ERR_IO);
+		goto done;
+	}
+	if (fseek(f2, 0L, SEEK_SET) == -1) {
+		err = got_ferror(f2, GOT_ERR_IO);
+		goto done;
+	}
+	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,
+		    outfile, rejectfile, patch_cb, patch_arg);
+		if (err)
+			goto done;
+		if (choice == GOT_PATCH_CHOICE_YES)
+			have_content = 1;
+		if (choice == GOT_PATCH_CHOICE_NO)
+			have_rejected_content = 1;
+		if (choice == GOT_PATCH_CHOICE_QUIT)
+			break;
+	}
+done:
+	free(label1);
+	if (blob)
+		got_object_blob_close(blob);
+	if (staged_blob)
+		got_object_blob_close(staged_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_unstaged_content);
+	if (rejectfile && fclose(rejectfile) == EOF && err == NULL)
+		err = got_error_from_errno2("fclose", *path_new_staged_content);
+	if (path1 && unlink(path1) == -1 && err == NULL)
+		err = got_error_from_errno2("unlink", path1);
+	if (path2 && unlink(path2) == -1 && err == NULL)
+		err = got_error_from_errno2("unlink", path2);
+	if (err || !have_content) {
+		if (*path_unstaged_content &&
+		    unlink(*path_unstaged_content) == -1 && err == NULL)
+			err = got_error_from_errno2("unlink",
+			    *path_unstaged_content);
+		free(*path_unstaged_content);
+		*path_unstaged_content = NULL;
+	}
+	if (err || !have_rejected_content) {
+		if (*path_new_staged_content &&
+		    unlink(*path_new_staged_content) == -1 && err == NULL)
+			err = got_error_from_errno2("unlink",
+			    *path_new_staged_content);
+		free(*path_new_staged_content);
+		*path_new_staged_content = NULL;
+	}
+	free(args);
+	if (ds) {
+		got_diff_state_free(ds);
+		free(ds);
+	}
+	if (changes)
+		got_diff_free_changes(changes);
+	free(path1);
+	free(path2);
+	return err;
+}
+
 static const struct got_error *
 unstage_path(void *arg, unsigned char status,
     unsigned char staged_status, const char *relpath,
@@ -5632,10 +5792,16 @@ unstage_path(void *arg, unsigned char status,
 	struct unstage_path_arg *a = arg;
 	struct got_fileindex_entry *ie;
 	struct got_blob_object *blob_base = NULL, *blob_staged = NULL;
-	char *ondisk_path = NULL;
+	char *ondisk_path = NULL, *path_unstaged_content = NULL;
+	char *path_new_staged_content = NULL;
 	int local_changes_subsumed;
 	struct stat sb;
 
+	if (staged_status != GOT_STATUS_ADD &&
+	    staged_status != GOT_STATUS_MODIFY &&
+	    staged_status != GOT_STATUS_DELETE)
+		return NULL;
+
 	ie = got_fileindex_entry_get(a->fileindex, relpath, strlen(relpath));
 	if (ie == NULL)
 		return got_error_path(relpath, GOT_ERR_BAD_PATH);
@@ -5652,12 +5818,50 @@ unstage_path(void *arg, unsigned char status,
 			break;
 		/* fall through */
 	case GOT_STATUS_ADD:
+		if (a->patch_cb) {
+			if (staged_status == GOT_STATUS_ADD) {
+				int choice = GOT_PATCH_CHOICE_NONE;
+				err = (*a->patch_cb)(&choice, a->patch_arg,
+				    staged_status, ie->path, NULL, 1, 1);
+				if (err)
+					break;
+				if (choice != GOT_PATCH_CHOICE_YES)
+					break;
+			} else {
+				err = create_unstaged_content(
+				    &path_unstaged_content,
+				    &path_new_staged_content, blob_id,
+				    staged_blob_id, ie->path, a->repo,
+				    a->patch_cb, a->patch_arg);
+				if (err || path_unstaged_content == NULL)
+					break;
+				if (path_new_staged_content) {
+					err = got_object_blob_create(
+					    &staged_blob_id,
+					    path_new_staged_content,
+					    a->repo);
+					if (err)
+						break;
+					memcpy(ie->staged_blob_sha1,
+					    staged_blob_id->sha1,
+					    SHA1_DIGEST_LENGTH);
+				}
+				err = merge_file(&local_changes_subsumed,
+				    a->worktree, blob_base, ondisk_path,
+				    relpath, got_fileindex_perms_to_st(ie),
+				    path_unstaged_content, "unstaged",
+				    a->repo, a->progress_cb, a->progress_arg);
+				if (err == NULL &&
+				    path_new_staged_content == NULL)
+					got_fileindex_entry_stage_set(ie,
+					    GOT_FILEIDX_STAGE_NONE);
+				break; /* Done with this file. */
+			}
+		}
 		err = got_object_open_as_blob(&blob_staged, a->repo,
 		    staged_blob_id, 8192);
 		if (err)
 			break;
-
-
 		err = merge_blob(&local_changes_subsumed, a->worktree,
 		    blob_base, ondisk_path, relpath,
 		    got_fileindex_perms_to_st(ie), blob_staged,
@@ -5668,6 +5872,19 @@ unstage_path(void *arg, unsigned char status,
 			    GOT_FILEIDX_STAGE_NONE);
 		break;
 	case GOT_STATUS_DELETE:
+		if (a->patch_cb) {
+			int choice = GOT_PATCH_CHOICE_NONE;
+			err = (*a->patch_cb)(&choice, a->patch_arg,
+			    staged_status, ie->path, NULL, 1, 1);
+			if (err)
+				break;
+			if (choice == GOT_PATCH_CHOICE_NO)
+				break;
+			if (choice != GOT_PATCH_CHOICE_YES) {
+				err = got_error(GOT_ERR_PATCH_CHOICE);
+				break;
+			}
+		}
 		got_fileindex_entry_stage_set(ie, GOT_FILEIDX_STAGE_NONE);
 		err = get_file_status(&status, &sb, ie, ondisk_path, a->repo);
 		if (err)
@@ -5677,6 +5894,14 @@ unstage_path(void *arg, unsigned char status,
 	}
 
 	free(ondisk_path);
+	if (path_unstaged_content &&
+	    unlink(path_unstaged_content) == -1 && err == NULL)
+		err = got_error_from_errno2("unlink", path_unstaged_content);
+	if (path_new_staged_content &&
+	    unlink(path_new_staged_content) == -1 && err == NULL)
+		err = got_error_from_errno2("unlink", path_new_staged_content);
+	free(path_unstaged_content);
+	free(path_new_staged_content);
 	if (blob_base)
 		got_object_blob_close(blob_base);
 	if (blob_staged)
@@ -5688,6 +5913,7 @@ const struct got_error *
 got_worktree_unstage(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)
 {
 	const struct got_error *err = NULL, *sync_err, *unlockerr;
@@ -5709,6 +5935,8 @@ got_worktree_unstage(struct got_worktree *worktree,
 	upa.repo = repo;
 	upa.progress_cb = progress_cb;
 	upa.progress_arg = progress_arg;
+	upa.patch_cb = patch_cb;
+	upa.patch_arg = patch_arg;
 	TAILQ_FOREACH(pe, paths, entry) {
 		err = worktree_status(worktree, pe->path, fileindex, repo,
 		    unstage_path, &upa, NULL, NULL);
blob - 1377492b96f7e6c3e31976b4e40366a8f951e7e7
blob + aa8c840bc317b2439e162d8a7510982c44639132
--- regress/cmdline/unstage.sh
+++ regress/cmdline/unstage.sh
@@ -58,13 +58,633 @@ function test_unstage_basic {
 	echo 'M  alpha' > $testroot/stdout.expected
 	echo 'D  beta' >> $testroot/stdout.expected
 	echo 'A  foo' >> $testroot/stdout.expected
+	(cd $testroot/wt && got status > $testroot/stdout)
+	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_unstage_patch {
+	local testroot=`test_init unstage_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 stage > /dev/null)
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		echo "got stage command failed unexpectedly" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	# don't unstage any hunks
+	printf "n\nn\nn\n" > $testroot/patchscript
+	(cd $testroot/wt && got unstage -F $testroot/patchscript -p \
+		numbers > $testroot/stdout)
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		echo "got stage 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)
+unstage this change? [y/n/q] n
+-----------------------------------------------
+@@ -4,7 +4,7 @@
+ 4
+ 5
+ 6
+-7
++b
+ 8
+ 9
+ 10
+-----------------------------------------------
+M  numbers (change 2 of 3)
+unstage this change? [y/n/q] n
+-----------------------------------------------
+@@ -13,4 +13,4 @@
+ 13
+ 14
+ 15
+-16
++c
+-----------------------------------------------
+M  numbers (change 3 of 3)
+unstage 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
+
+	# unstage middle hunk
+	printf "n\ny\nn\n" > $testroot/patchscript
+	(cd $testroot/wt && got unstage -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)
+unstage this change? [y/n/q] n
+-----------------------------------------------
+@@ -4,7 +4,7 @@
+ 4
+ 5
+ 6
+-7
++b
+ 8
+ 9
+ 10
+-----------------------------------------------
+M  numbers (change 2 of 3)
+unstage this change? [y/n/q] y
+-----------------------------------------------
+@@ -13,4 +13,4 @@
+ 13
+ 14
+ 15
+-16
++c
+-----------------------------------------------
+M  numbers (change 3 of 3)
+unstage this change? [y/n/q] n
+G  numbers
+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 "MM 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 -s > $testroot/stdout)
+
+	echo "diff $commit_id $testroot/wt (staged changes)" \
+		> $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 -n 'blob + ' >> $testroot/stdout.expected
+	(cd $testroot/wt && got stage -l numbers) | cut -d' ' -f 1 \
+		>> $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
+
+	(cd $testroot/wt && got diff > $testroot/stdout)
+	echo "diff $commit_id $testroot/wt" > $testroot/stdout.expected
+	echo -n 'blob - ' >> $testroot/stdout.expected
+	(cd $testroot/wt && got stage -l numbers) | cut -d' ' -f 1 \
+		>> $testroot/stdout.expected
+	echo "file + numbers" >> $testroot/stdout.expected
+	cat >> $testroot/stdout.expected <<EOF
+--- numbers
++++ numbers
+@@ -4,7 +4,7 @@ a
+ 4
+ 5
+ 6
+-7
++b
+ 8
+ 9
+ 10
+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 stage >/dev/null)
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		echo "got stage command failed unexpectedly" >&2
+		test_done "$testroot" "1"
+		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
+
+	# unstage last hunk
+	printf "n\nn\ny\n" > $testroot/patchscript
+	(cd $testroot/wt && got unstage -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)
+unstage this change? [y/n/q] n
+-----------------------------------------------
+@@ -4,7 +4,7 @@
+ 4
+ 5
+ 6
+-7
++b
+ 8
+ 9
+ 10
+-----------------------------------------------
+M  numbers (change 2 of 3)
+unstage this change? [y/n/q] n
+-----------------------------------------------
+@@ -13,4 +13,4 @@
+ 13
+ 14
+ 15
+-16
++c
+-----------------------------------------------
+M  numbers (change 3 of 3)
+unstage this change? [y/n/q] y
+G  numbers
+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 "MM 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 -s > $testroot/stdout)
+
+	echo "diff $commit_id $testroot/wt (staged changes)" \
+		> $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 -n 'blob + ' >> $testroot/stdout.expected
+	(cd $testroot/wt && got stage -l numbers) | cut -d' ' -f 1 \
+		>> $testroot/stdout.expected
+	cat >> $testroot/stdout.expected <<EOF
+--- numbers
++++ numbers
+@@ -1,10 +1,10 @@
+ 1
+-2
++a
+ 3
+ 4
+ 5
+ 6
+-7
++b
+ 8
+ 9
+ 10
+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
+	(cd $testroot/wt && got stage -l numbers) | cut -d' ' -f 1 \
+		>> $testroot/stdout.expected
+	echo "file + numbers" >> $testroot/stdout.expected
+	cat >> $testroot/stdout.expected <<EOF
+--- numbers
++++ numbers
+@@ -13,4 +13,4 @@ b
+ 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
+
+	(cd $testroot/wt && got stage >/dev/null)
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		echo "got stage command failed unexpectedly" >&2
+		test_done "$testroot" "1"
+		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
+
+	# unstage all hunks
+	printf "y\ny\ny\n" > $testroot/patchscript
+	(cd $testroot/wt && got unstage -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)
+unstage this change? [y/n/q] y
+-----------------------------------------------
+@@ -4,7 +4,7 @@
+ 4
+ 5
+ 6
+-7
++b
+ 8
+ 9
+ 10
+-----------------------------------------------
+M  numbers (change 2 of 3)
+unstage this change? [y/n/q] y
+-----------------------------------------------
+@@ -13,4 +13,4 @@
+ 13
+ 14
+ 15
+-16
++c
+-----------------------------------------------
+M  numbers (change 3 of 3)
+unstage this change? [y/n/q] y
+G  numbers
+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 -s > $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
+		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,10 +1,10 @@
+ 1
+-2
++a
+ 3
+ 4
+ 5
+ 6
+-7
++b
+ 8
+ 9
+ 10
+@@ -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
+	fi
 	test_done "$testroot" "$ret"
+
 }
 
+function test_unstage_patch_added {
+	local testroot=`test_init unstage_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)
+
+	(cd $testroot/wt && got stage > /dev/null)
+
+	printf "y\n" > $testroot/patchscript
+	(cd $testroot/wt && got unstage -F $testroot/patchscript -p \
+		epsilon/new > $testroot/stdout)
+
+	echo "A  epsilon/new" > $testroot/stdout.expected
+	echo "unstage this addition? [y/n] y" >> $testroot/stdout.expected
+	echo "G  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 "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
+
+	(cd $testroot/wt && got diff -s > $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
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	(cd $testroot/wt && got diff > $testroot/stdout)
+
+	echo "diff $commit_id $testroot/wt" > $testroot/stdout.expected
+	echo 'blob - /dev/null' >> $testroot/stdout.expected
+	echo 'file + epsilon/new' >> $testroot/stdout.expected
+	echo "--- epsilon/new" >> $testroot/stdout.expected
+	echo "+++ epsilon/new" >> $testroot/stdout.expected
+	echo "@@ -0,0 +1 @@" >> $testroot/stdout.expected
+	echo "+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_unstage_patch_removed {
+	local testroot=`test_init unstage_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)
+	(cd $testroot/wt && got stage > /dev/null)
+
+	printf "y\n" > $testroot/patchscript
+	(cd $testroot/wt && got unstage -F $testroot/patchscript -p \
+		beta > $testroot/stdout)
+
+	echo "D  beta" > $testroot/stdout.expected
+	echo "unstage this deletion? [y/n] y" >> $testroot/stdout.expected
+	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
+
+	(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
+
+	(cd $testroot/wt && got diff -s > $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
+		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 | grep 'beta$' | cut -d' ' -f 1 \
+		>> $testroot/stdout.expected
+	echo 'file + /dev/null' >> $testroot/stdout.expected
+	echo "--- beta" >> $testroot/stdout.expected
+	echo "+++ beta" >> $testroot/stdout.expected
+	echo "@@ -1 +0,0 @@" >> $testroot/stdout.expected
+	echo "-beta" >> $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_unstage_basic
+run_test test_unstage_patch
+run_test test_unstage_patch_added
+run_test test_unstage_patch_removed