Commit Diff


commit - 3043f6a3950d04db865f7c663fcbee68deec37e9
commit + dc424a06a9c1f0f664cee320cc1b34752f33ec97
blob - 3bf741184d417aa20f5a808f0601de38b89e450f
blob + 601a65eadb4dcaff86ccb43efd94fb6ff9183663
--- got/got.1
+++ got/got.1
@@ -870,7 +870,7 @@ If this option is used, no further command-line argume
 .It Cm he
 Short alias for
 .Cm histedit .
-.It Cm stage [ Fl l ] Ar file-path ...
+.It Cm stage [ Fl l ] [ Fl p ] [ Fl F ] Ar file-path ...
 Stage local changes at the specified paths for inclusion in the next commit.
 Paths may be staged if they are added, modified, or deleted according to
 .Cm got status .
@@ -917,6 +917,22 @@ Instead of staging new changes, list paths which are a
 along with the IDs of staged blob objects and stage status codes.
 If paths were provided in the command line show the staged paths
 among the specified paths. Otherwise, show all staged paths.
+.It Fl p
+Instead of staging the entire content of a changed file, interactively
+select or reject changes for staging based on
+.Dq y
+and
+.Dq n
+responses.
+If a file is in modified status, individual patches derived from the
+modified file content can be staged.
+Files in added or deleted status may be staged or rejected in their entirety.
+.It Fl F Ar path
+With the
+.Fl p
+option, read responses line-by-line from a script file at the specified
+.Ar path
+instead of prompting interactively.
 .El
 .Pp
 .Cm got stage
blob - 87f59f1e2beb0ab8b8186562d5f88ed930dcee13
blob + cf034841cbaef30f1aa862ce4fcd6c9a1dbf8f37
--- got/got.c
+++ got/got.c
@@ -1321,6 +1321,8 @@ get_datestr(time_t *time, char *datebuf)
 		*p = '\0';
 	return s;
 }
+
+#define GOT_COMMIT_SEP_STR "-----------------------------------------------\n"
 
 static const struct got_error *
 print_commit(struct got_commit_object *commit, struct got_object_id *id,
@@ -1364,7 +1366,7 @@ print_commit(struct got_commit_object *commit, struct 
 	if (err)
 		return err;
 
-	printf("-----------------------------------------------\n");
+	printf(GOT_COMMIT_SEP_STR);
 	printf("commit %s%s%s%s\n", id_str, refs_str ? " (" : "",
 	    refs_str ? refs_str : "", refs_str ? ")" : "");
 	free(id_str);
@@ -5156,7 +5158,7 @@ done:
 __dead static void
 usage_stage(void)
 {
-	fprintf(stderr, "usage: %s stage [-l] | file-path ...\n",
+	fprintf(stderr, "usage: %s stage [-l] | [-p] [-F] file-path ...\n",
 	    getprogname());
 	exit(1);
 }
@@ -5188,6 +5190,96 @@ print_stage(void *arg, unsigned char status, unsigned 
 }
 
 static const struct got_error *
+show_change(unsigned char status, const char *path, FILE *patch_file)
+{
+	char *line = NULL;
+	size_t linesize = 0;
+	ssize_t linelen;
+
+	switch (status) {
+	case GOT_STATUS_ADD:
+		printf("A  %s\nstage this addition? [y/n] ", path);
+		break;
+	case GOT_STATUS_DELETE:
+		printf("D  %s\nstage deletion? [y/n] ", path);
+		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\nstage this change? [y/n] ", path);
+		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)
+{
+	const struct got_error *err = NULL;
+	char *line = NULL;
+	size_t linesize = 0;
+	ssize_t linelen;
+	int resp = ' ';
+	FILE *patch_script_file = arg;
+
+	*choice = GOT_PATCH_CHOICE_NONE;
+
+	if (patch_script_file) {
+		char *nl;
+		linelen = getline(&line, &linesize, patch_script_file);
+		if (linelen == -1) {
+			if (ferror(patch_script_file))
+				return got_error_from_errno("getline");
+			return NULL;
+		}
+		nl = strchr(line, '\n');
+		if (nl)
+			*nl = '\0';
+		err = show_change(status, path, patch_file);
+		if (err)
+			return err;
+		if (strcmp(line, "y") == 0) {
+			*choice = GOT_PATCH_CHOICE_YES;
+			printf("y\n");
+		}
+		if (strcmp(line, "n") == 0) {
+			*choice = GOT_PATCH_CHOICE_NO;
+			printf("n\n");
+		}
+		free(line);
+		return NULL;
+	}
+
+	while (resp != 'y' && resp != 'n') {
+		err = show_change(status, path, patch_file);
+		if (err)
+			return err;
+		resp = getchar();
+		if (resp == '\n')
+			resp = getchar();
+		if (resp != 'y' && resp != 'n')
+			printf("invalid response '%c'\n", resp);
+	}
+
+	if (resp == 'y')
+		*choice = GOT_PATCH_CHOICE_YES;
+	else if (resp == 'n')
+		*choice = GOT_PATCH_CHOICE_NO;
+
+	return NULL;
+}
+
+static const struct got_error *
 cmd_stage(int argc, char *argv[])
 {
 	const struct got_error *error = NULL;
@@ -5196,15 +5288,23 @@ cmd_stage(int argc, char *argv[])
 	char *cwd = NULL;
 	struct got_pathlist_head paths;
 	struct got_pathlist_entry *pe;
-	int ch, list_stage = 0;
+	int ch, list_stage = 0, pflag = 0;
+	const char *patch_script_path = NULL;
+	FILE *patch_script_file = NULL;
 
 	TAILQ_INIT(&paths);
 
-	while ((ch = getopt(argc, argv, "l")) != -1) {
+	while ((ch = getopt(argc, argv, "lpF:")) != -1) {
 		switch (ch) {
 		case 'l':
 			list_stage = 1;
 			break;
+		case 'p':
+			pflag = 1;
+			break;
+		case 'F':
+			patch_script_path = optarg;
+			break;
 		default:
 			usage_stage();
 			/* NOTREACHED */
@@ -5219,8 +5319,10 @@ cmd_stage(int argc, char *argv[])
 	    "unveil", NULL) == -1)
 		err(1, "pledge");
 #endif
-	if (!list_stage && argc < 1)
+	if ((list_stage && pflag) || (!list_stage && argc < 1))
 		usage_stage();
+	if (patch_script_path && !pflag)
+		errx(1, "-F option can only be used together with -p option");
 
 	cwd = getcwd(NULL, 0);
 	if (cwd == NULL) {
@@ -5236,6 +5338,14 @@ cmd_stage(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)
@@ -5249,8 +5359,9 @@ cmd_stage(int argc, char *argv[])
 		error = got_worktree_status(worktree, &paths, repo,
 		    print_stage, NULL, check_cancelled, NULL);
 	else
-		error = got_worktree_stage(worktree, &paths, print_status,
-		    NULL, repo);
+		error = got_worktree_stage(worktree, &paths,
+		    pflag ? NULL : print_status, NULL,
+		    pflag ? choose_patch : NULL, patch_script_file, repo);
 done:
 	if (repo)
 		got_repo_close(repo);
blob - bd4cd933c9ccebfb9a50286f6bde4d29779e8e59
blob + cd6c014f7b55f4b7b291761685e20f8788e90559
--- include/got_error.h
+++ include/got_error.h
@@ -120,6 +120,7 @@
 #define GOT_ERR_STAGE_OUT_OF_DATE 104
 #define GOT_ERR_FILE_NOT_STAGED 105
 #define GOT_ERR_STAGED_PATHS	106
+#define GOT_ERR_PATCH_CHOICE	107
 
 static const struct got_error {
 	int code;
@@ -244,6 +245,7 @@ static const struct got_error {
 	{ GOT_ERR_FILE_NOT_STAGED, "file is not staged" },
 	{ GOT_ERR_STAGED_PATHS, "work tree contains files with staged "
 	    "changes; these changes must be committed or unstaged first" },
+	{ GOT_ERR_PATCH_CHOICE, "invalid patch choice" },
 };
 
 /*
blob - 0b3c446ddee4de1ba085b83ea1dad0a327895ce1
blob + 0edf139bd3d1ced5c9179e07ffed8843f424b0ea
--- include/got_worktree.h
+++ include/got_worktree.h
@@ -371,17 +371,25 @@ const struct got_error *got_worktree_histedit_abort(st
 /* Get the path to this work tree's histedit script file. */
 const struct got_error *got_worktree_get_histedit_script_path(char **,
     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 *);
+
+/* 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
 
 /*
  * Stage the specified paths for commit.
- * If the 'data' pointer of a pathlist element on the path list is NULL then
- * stage the content of the entire file at this path. Otherwise, the 'data'
- * pointer is expected to point at a const char * path of a file which
- * contains alternative content to be staged instead.
+ * If the patch callback is not NULL, call it to select patch hunks for
+ * staging. Otherwise, stage the full file content found at each path.
 */
 const struct got_error *got_worktree_stage(struct got_worktree *,
     struct got_pathlist_head *, got_worktree_status_cb, void *,
-    struct got_repository *);
+    got_worktree_patch_cb, void *, struct got_repository *);
 
 /*
  * Merge staged changes for the specified paths back into the work tree
blob - cab65a3f7e04a20a205739e81a819921eea06c5a
blob + 4270950a51e56cff79acaeff89d7eab40667f9b0
--- lib/diff.c
+++ lib/diff.c
@@ -112,6 +112,7 @@ diff_blobs(struct got_blob_object *blob1, struct got_b
 		fprintf(outfile, "blob + %s\n", idstr2);
 	}
 	err = got_diffreg(&res, f1, f2, flags, &args, &ds, outfile, changes);
+	got_diff_state_free(&ds);
 done:
 	if (f1 && fclose(f1) != 0 && err == NULL)
 		err = got_error_from_errno("fclose");
@@ -215,6 +216,7 @@ diff_blob_file(struct got_diff_changes **changes,
 	}
 	err = got_diffreg(&res, f1, f2, flags, &args, &ds, outfile,
 	    changes ? *changes : NULL);
+	got_diff_state_free(&ds);
 done:
 	if (f1 && fclose(f1) != 0 && err == NULL)
 		err = got_error_from_errno("fclose");
@@ -743,3 +745,82 @@ done:
 		got_object_commit_close(commit2);
 	return err;
 }
+
+const struct got_error *
+got_diff_files(struct got_diff_changes **changes,
+    struct got_diff_state **ds,
+    struct got_diff_args **args,
+    int *flags,
+    FILE *f1, size_t size1, const char *label1,
+    FILE *f2, size_t size2, const char *label2,
+    int diff_context, FILE *outfile)
+{
+	const struct got_error *err = NULL;
+	int res;
+
+	*flags = 0;
+	*ds = calloc(1, sizeof(**ds));
+	if (*ds == NULL)
+		return got_error_from_errno("calloc");
+	*args = calloc(1, sizeof(**args));
+	if (*args == NULL) {
+		err = got_error_from_errno("calloc");
+		goto done;
+	}
+
+	if (changes)
+		*changes = NULL;
+
+	if (f1 == NULL)
+		*flags |= D_EMPTY1;
+
+	if (f2 == NULL)
+		*flags |= D_EMPTY2;
+
+	/* XXX should stat buffers be passed in args instead of ds? */
+	(*ds)->stb1.st_mode = S_IFREG;
+	(*ds)->stb1.st_size = size1;
+	(*ds)->stb1.st_mtime = 0; /* XXX */
+
+	(*ds)->stb2.st_mode = S_IFREG;
+	(*ds)->stb2.st_size = size2;
+	(*ds)->stb2.st_mtime = 0; /* XXX */
+
+	(*args)->diff_format = D_UNIFIED;
+	(*args)->label[0] = label1;
+	(*args)->label[1] = label2;
+	(*args)->diff_context = diff_context;
+	*flags |= D_PROTOTYPE;
+
+	if (outfile) {
+		fprintf(outfile, "file - %s\n",
+		    f1 == NULL ? "/dev/null" : label1);
+		fprintf(outfile, "file + %s\n",
+		    f2 == NULL ? "/dev/null" : label2);
+	}
+	if (changes) {
+		err = alloc_changes(changes);
+		if (err)
+			goto done;
+	}
+	err = got_diffreg(&res, f1, f2, *flags, *args, *ds, outfile,
+	    changes ? *changes : NULL);
+done:
+	if (err) {
+		if (*ds) {
+			got_diff_state_free(*ds);
+			free(*ds);
+			*ds = NULL;
+		}
+		if (*args) {
+			free(*args);
+			*args = NULL;
+		}
+		if (changes) {
+			if (*changes)
+				got_diff_free_changes(*changes);
+			*changes = NULL;
+		}
+	}
+	return err;
+}
blob - be2aa2069ee9a5c93d8352842b9d0fc1618de477
blob + 5cceae5c27beafc107fba5e896b2aa3297eaebaf
--- lib/diffreg.c
+++ lib/diffreg.c
@@ -265,6 +265,18 @@ diff_output(FILE *outfile, const char *fmt, ...)
 	va_start(ap, fmt);
 	vfprintf(outfile, fmt, ap);
 	va_end(ap);
+}
+
+void
+got_diff_state_free(struct got_diff_state *ds)
+{
+	free(ds->J);
+	free(ds->member);
+	free(ds->class);
+	free(ds->clist);
+	free(ds->klist);
+	free(ds->ixold);
+	free(ds->ixnew);
 }
 
 const struct got_error *
@@ -407,13 +419,6 @@ got_diffreg(int *rval, FILE *f1, FILE *f2, int flags,
 	    args->label[1], f2, flags))
 		err = got_error_from_errno("output");
 closem:
-	free(ds->J);
-	free(ds->member);
-	free(ds->class);
-	free(ds->clist);
-	free(ds->klist);
-	free(ds->ixold);
-	free(ds->ixnew);
 	if (ds->anychange) {
 		args->status |= 1;
 		if (*rval == D_SAME)
@@ -1234,6 +1239,19 @@ dump_unified_vec(FILE *outfile, struct got_diff_change
 	ds->context_vec_ptr = ds->context_vec_start - 1;
 }
 
+void
+got_diff_dump_change(FILE *outfile, struct got_diff_change *change,
+    struct got_diff_state *ds, struct got_diff_args *args,
+    FILE *f1, FILE *f2, int diff_flags)
+{
+	ds->context_vec_ptr = &change->cv;
+	ds->context_vec_start = &change->cv;
+	ds->context_vec_end = &change->cv;
+
+	/* XXX TODO needs error checking */
+	dump_unified_vec(outfile, NULL, ds, args, f1, f2, diff_flags);
+}
+
 static void
 print_header(FILE *outfile, struct got_diff_state *ds, struct got_diff_args *args,
     const char *file1, const char *file2)
blob - c9a2662dc807dc6277cf292b6f634361856790ad
blob + 496525cdbab66eb5140f3e444ebb242457424957
--- lib/got_lib_diff.h
+++ lib/got_lib_diff.h
@@ -125,6 +125,8 @@ struct got_diff_state {
 	size_t max_context;
 };
 
+void got_diff_state_free(struct got_diff_state *);
+
 struct got_diff_args {
 	int	 Tflag;
 	int	 diff_format, diff_context, status;
@@ -148,3 +150,10 @@ void got_diff_free_changes(struct got_diff_changes *);
 
 const struct got_error *got_merge_diff3(int *, int, const char *, const char *,
     const char *, const char *, const char *);
+
+const struct got_error *got_diff_files(struct got_diff_changes **,
+    struct got_diff_state **, struct got_diff_args **, int *, FILE *, size_t,
+    const char *, FILE *, size_t, const char *, int, FILE *);
+
+void got_diff_dump_change(FILE *, struct got_diff_change *,
+    struct got_diff_state *, struct got_diff_args *, FILE *, FILE *, int);
blob - 39d55837d729a44cf52961df8dfae70850fe9bd2
blob + 3c1ffb4a2a5de47f3d61e5f8f8fded152ed6b987
--- lib/worktree.c
+++ lib/worktree.c
@@ -5136,16 +5136,265 @@ check_stage_ok(const char *relpath, const char *ondisk
 done:
 	free(in_repo_path);
 	return err;
+}
+
+static const struct got_error *
+copy_one_line(FILE *infile, FILE *outfile)
+{
+	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;
+	}
+	n = fwrite(line, 1, linelen, outfile);
+	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 *
+apply_or_reject_change(int *choice, struct got_diff_change *change,
+    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)
+{
+	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);
+	if (err)
+		goto done;
+
+	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)++;
+		}
+		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)++;
+		}
+		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)
+{
+	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;
+
+	*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, ds, args,
+		    diff_flags, relpath, f1, f2, &line_cur1, &line_cur2,
+		    outfile, patch_cb, patch_arg);
+		if (err)
+			goto done;
+		if (choice == GOT_PATCH_CHOICE_YES)
+			have_content = 1;
+	}
+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 *
 stage_path(const char *relpath, const char *ondisk_path,
-    const char *path_content, struct got_worktree *worktree,
-    struct got_fileindex *fileindex, struct got_repository *repo,
-    got_worktree_status_cb status_cb, void *status_arg)
+    struct got_worktree *worktree, struct got_fileindex *fileindex,
+    struct got_repository *repo,
+    got_worktree_status_cb status_cb, void *status_arg,
+    got_worktree_patch_cb patch_cb, void *patch_arg)
 {
 	const struct got_error *err = NULL;
 	struct got_fileindex_entry *ie;
+	char *path_content = NULL;
 	unsigned char status, staged_status;
 	struct stat sb;
 	struct got_object_id blob_id, *staged_blob_id = NULL;
@@ -5163,11 +5412,28 @@ stage_path(const char *relpath, const char *ondisk_pat
 	switch (status) {
 	case GOT_STATUS_ADD:
 	case GOT_STATUS_MODIFY:
+		memcpy(&blob_id.sha1, ie->blob_sha1, SHA1_DIGEST_LENGTH);
+		if (patch_cb) {
+			if (status == GOT_STATUS_ADD) {
+				int choice = GOT_PATCH_CHOICE_NONE;
+				err = (*patch_cb)(&choice, patch_arg, status,
+				    ie->path, NULL);
+				if (err)
+					break;
+				if (choice != GOT_PATCH_CHOICE_YES)
+					break;
+			} else {
+				err = create_staged_content(&path_content,
+				    &blob_id, ondisk_path, ie->path, repo,
+				    patch_cb, patch_arg);
+				if (err || path_content == NULL)
+					break;
+			}
+		}
 		err = got_object_blob_create(&staged_blob_id,
 		    path_content ? path_content : ondisk_path, repo);
 		if (err)
-			goto done;
-		memcpy(&blob_id.sha1, ie->blob_sha1, SHA1_DIGEST_LENGTH);
+			break;
 		memcpy(ie->staged_blob_sha1, staged_blob_id->sha1,
 		    SHA1_DIGEST_LENGTH);
 		if (status == GOT_STATUS_ADD || staged_status == GOT_STATUS_ADD)
@@ -5175,6 +5441,8 @@ stage_path(const char *relpath, const char *ondisk_pat
 		else
 			stage = GOT_FILEIDX_STAGE_MODIFY;
 		got_fileindex_entry_stage_set(ie, stage);
+		if (status_cb == NULL)
+			break;
 		err = (*status_cb)(status_arg, GOT_STATUS_NO_CHANGE,
 		    get_staged_status(ie), relpath, &blob_id,
 		    staged_blob_id, NULL);
@@ -5182,8 +5450,19 @@ stage_path(const char *relpath, const char *ondisk_pat
 	case GOT_STATUS_DELETE:
 		if (staged_status == GOT_STATUS_DELETE)
 			break;
+		if (patch_cb) {
+			int choice = GOT_PATCH_CHOICE_NONE;
+			err = (*patch_cb)(&choice, patch_arg, status,
+			    ie->path, NULL);
+			if (err)
+				break;
+			if (choice != GOT_PATCH_CHOICE_YES)
+				break;
+		}
 		stage = GOT_FILEIDX_STAGE_DELETE;
 		got_fileindex_entry_stage_set(ie, stage);
+		if (status_cb == NULL)
+			break;
 		err = (*status_cb)(status_arg, GOT_STATUS_NO_CHANGE,
 		    get_staged_status(ie), relpath, NULL, NULL, NULL);
 		break;
@@ -5197,7 +5476,10 @@ stage_path(const char *relpath, const char *ondisk_pat
 		err = got_error_path(relpath, GOT_ERR_FILE_STATUS);
 		break;
 	}
-done:
+
+	if (path_content && unlink(path_content) == -1 && err == NULL)
+		err = got_error_from_errno2("unlink", path_content);
+	free(path_content);
 	free(staged_blob_id);
 	return err;
 }
@@ -5206,6 +5488,7 @@ const struct got_error *
 got_worktree_stage(struct got_worktree *worktree,
     struct got_pathlist_head *paths,
     got_worktree_status_cb status_cb, void *status_arg,
+    got_worktree_patch_cb patch_cb, void *patch_arg,
     struct got_repository *repo)
 {
 	const struct got_error *err = NULL, *sync_err, *unlockerr;
@@ -5249,8 +5532,8 @@ got_worktree_stage(struct got_worktree *worktree,
 		    pe->path) == -1)
 			return got_error_from_errno("asprintf");
 		err = stage_path(pe->path, ondisk_path,
-		    (const char *)pe->data, worktree, fileindex, repo,
-		    status_cb, status_arg);
+		    worktree, fileindex, repo, status_cb, status_arg,
+		    patch_cb, patch_arg);
 		free(ondisk_path);
 		if (err)
 			break;
blob - 4eb0bdb57555928c3ce80c22a90a278497349b0a
blob + 4bc1f4252276a19fd35ea26cf76d444bfaabe503
--- regress/cmdline/stage.sh
+++ regress/cmdline/stage.sh
@@ -1140,7 +1140,399 @@ function test_stage_commit {
 	fi
 	test_done "$testroot" "$ret"
 }
+
+function test_stage_patch {
+	local testroot=`test_init stage_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
+
+	# don't stage any hunks
+	printf "n\nn\nn\n" > $testroot/patchscript
+	(cd $testroot/wt && got stage -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
+stage this change? [y/n] n
+-----------------------------------------------
+@@ -4,7 +4,7 @@
+ 4
+ 5
+ 6
+-7
++b
+ 8
+ 9
+ 10
+-----------------------------------------------
+M  numbers
+stage this change? [y/n] n
+-----------------------------------------------
+@@ -13,4 +13,4 @@
+ 13
+ 14
+ 15
+-16
++c
+-----------------------------------------------
+M  numbers
+stage this change? [y/n] 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
+
+	# stage middle hunk
+	printf "n\ny\nn\n" > $testroot/patchscript
+	(cd $testroot/wt && got stage -F $testroot/patchscript -p \
+		numbers > $testroot/stdout)
+
+	cat > $testroot/stdout.expected <<EOF
+-----------------------------------------------
+@@ -1,5 +1,5 @@
+ 1
+-2
++a
+ 3
+ 4
+ 5
+-----------------------------------------------
+M  numbers
+stage this change? [y/n] n
+-----------------------------------------------
+@@ -4,7 +4,7 @@
+ 4
+ 5
+ 6
+-7
++b
+ 8
+ 9
+ 10
+-----------------------------------------------
+M  numbers
+stage this change? [y/n] y
+-----------------------------------------------
+@@ -13,4 +13,4 @@
+ 13
+ 14
+ 15
+-16
++c
+-----------------------------------------------
+M  numbers
+stage this change? [y/n] 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 "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
+	echo "--- numbers" >> $testroot/stdout.expected
+	echo "+++ numbers" >> $testroot/stdout.expected
+	echo "@@ -4,7 +4,7 @@" >> $testroot/stdout.expected
+	echo " 4" >> $testroot/stdout.expected
+	echo " 5" >> $testroot/stdout.expected
+	echo " 6" >> $testroot/stdout.expected
+	echo "-7" >> $testroot/stdout.expected
+	echo "+b" >> $testroot/stdout.expected
+	echo " 8" >> $testroot/stdout.expected
+	echo " 9" >> $testroot/stdout.expected
+	echo " 10" >> $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 unstage >/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
+
+	# stage last hunk
+	printf "n\nn\ny\n" > $testroot/patchscript
+	(cd $testroot/wt && got stage -F $testroot/patchscript -p \
+		numbers > $testroot/stdout)
+
+	cat > $testroot/stdout.expected <<EOF
+-----------------------------------------------
+@@ -1,5 +1,5 @@
+ 1
+-2
++a
+ 3
+ 4
+ 5
+-----------------------------------------------
+M  numbers
+stage this change? [y/n] n
+-----------------------------------------------
+@@ -4,7 +4,7 @@
+ 4
+ 5
+ 6
+-7
++b
+ 8
+ 9
+ 10
+-----------------------------------------------
+M  numbers
+stage this change? [y/n] n
+-----------------------------------------------
+@@ -13,4 +13,4 @@
+ 13
+ 14
+ 15
+-16
++c
+-----------------------------------------------
+M  numbers
+stage this change? [y/n] 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 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
+	echo "--- numbers" >> $testroot/stdout.expected
+	echo "+++ numbers" >> $testroot/stdout.expected
+	echo "@@ -13,4 +13,4 @@" >> $testroot/stdout.expected
+	echo " 13" >> $testroot/stdout.expected
+	echo " 14" >> $testroot/stdout.expected
+	echo " 15" >> $testroot/stdout.expected
+	echo "-16" >> $testroot/stdout.expected
+	echo "+c" >> $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_stage_patch_added {
+	local testroot=`test_init stage_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 "y\n" > $testroot/patchscript
+	(cd $testroot/wt && got stage -F $testroot/patchscript -p \
+		epsilon/new > $testroot/stdout)
+
+	echo "A  epsilon/new" > $testroot/stdout.expected
+	echo "stage this addition? [y/n] y" >> $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 "diff $commit_id $testroot/wt (staged changes)" \
+		> $testroot/stdout.expected
+	echo 'blob - /dev/null' >> $testroot/stdout.expected
+	echo -n 'blob + ' >> $testroot/stdout.expected
+	(cd $testroot/wt && got stage -l epsilon/new) | cut -d' ' -f 1 \
+		>> $testroot/stdout.expected
+	echo "--- /dev/null" >> $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_stage_patch_removed {
+	local testroot=`test_init stage_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 "y\n" > $testroot/patchscript
+	(cd $testroot/wt && got stage -F $testroot/patchscript -p \
+		beta > $testroot/stdout)
+
+	echo -n > $testroot/stdout.expected
+
+	echo "D  beta" > $testroot/stdout.expected
+	echo "stage deletion? [y/n] y" >> $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 "diff $commit_id $testroot/wt (staged changes)" \
+		> $testroot/stdout.expected
+	echo -n 'blob - ' >> $testroot/stdout.expected
+	(cd $testroot/wt && got stage -l beta) | cut -d' ' -f 1 \
+		>> $testroot/stdout.expected
+	echo 'blob + /dev/null' >> $testroot/stdout.expected
+	echo "--- beta" >> $testroot/stdout.expected
+	echo "+++ /dev/null" >> $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_stage_basic
 run_test test_stage_no_changes
 run_test test_stage_list
@@ -1157,3 +1549,6 @@ run_test test_stage_rebase
 run_test test_stage_update
 run_test test_stage_commit_non_staged
 run_test test_stage_commit
+run_test test_stage_patch
+run_test test_stage_patch_added
+run_test test_stage_patch_removed