Commit Diff


commit - 805253d5155091691f7cf36e54134cc87b2ea91a
commit + e9ce266e31923cc339954b331d273d9bba543f6f
blob - 8b431936ed43a5398c391d18637ae6dbc0f5348f
blob + 8dfd844f3da472b6ed040a62acaf85403cbc07ea
--- got/Makefile
+++ got/Makefile
@@ -13,7 +13,7 @@ SRCS=		got.c blame.c commit_graph.c delta.c diff.c \
 		diff_myers.c diff_output.c diff_output_plain.c \
 		diff_output_unidiff.c diff_output_edscript.c \
 		diff_patience.c send.c deltify.c pack_create.c dial.c \
-		bloom.c murmurhash2.c ratelimit.c
+		bloom.c murmurhash2.c ratelimit.c patch.c
 
 MAN =		${PROG}.1 got-worktree.5 git-repository.5 got.conf.5
 
blob - eaafde8411ebca559ac74b4970f3a9ee755e26b9
blob + 61596d1cfc534598b2516ffa737754cfb667abb1
--- got/got.1
+++ got/got.1
@@ -1285,6 +1285,35 @@ option)
 .It ! Ta versioned file expected on disk but missing
 .El
 .El
+.Tg pa
+.It Cm patch Op Ar patchfile
+.Dl Pq alias: Cm pa
+Apply changes from
+.Ar patchfile
+.Pq or standard input
+and record the state of the affected files afterwards.
+The content of
+.Ar patchfile
+must be an unified diff.
+If
+.Ar patchfile
+contains more than one patch,
+.Nm
+.Cm patch
+will try to apply them all.
+.Pp
+Show the status of each affected file, using the following status codes:
+.Bl -column XYZ description
+.It M Ta modified file
+.It D Ta deleted file
+.It A Ta added file
+.El
+.Pp
+If a change does not match at its exact line number,
+.Nm
+.Cm patch
+applies it somewhere else in the file if it can find a good spot before
+giving up.
 .Tg rv
 .It Cm revert Oo Fl p Oc Oo Fl F Ar response-script Oc Oo Fl R Oc Ar path ...
 .Dl Pq alias: Cm rv
blob - 2c02ca33857bec5cf55e156d4e9cb8783514c705
blob + 3e66e251220b096cd82830703d4481ddfc399307
--- got/got.c
+++ got/got.c
@@ -56,6 +56,7 @@
 #include "got_opentemp.h"
 #include "got_gotconfig.h"
 #include "got_dial.h"
+#include "got_patch.h"
 
 #ifndef nitems
 #define nitems(_a)	(sizeof((_a)) / sizeof((_a)[0]))
@@ -101,6 +102,7 @@ __dead static void	usage_branch(void);
 __dead static void	usage_tag(void);
 __dead static void	usage_add(void);
 __dead static void	usage_remove(void);
+__dead static void	usage_patch(void);
 __dead static void	usage_revert(void);
 __dead static void	usage_commit(void);
 __dead static void	usage_send(void);
@@ -131,6 +133,7 @@ static const struct got_error*		cmd_branch(int, char *
 static const struct got_error*		cmd_tag(int, char *[]);
 static const struct got_error*		cmd_add(int, char *[]);
 static const struct got_error*		cmd_remove(int, char *[]);
+static const struct got_error*		cmd_patch(int, char *[]);
 static const struct got_error*		cmd_revert(int, char *[]);
 static const struct got_error*		cmd_commit(int, char *[]);
 static const struct got_error*		cmd_send(int, char *[]);
@@ -162,6 +165,7 @@ static const struct got_cmd got_commands[] = {
 	{ "tag",	cmd_tag,	usage_tag,	"" },
 	{ "add",	cmd_add,	usage_add,	"" },
 	{ "remove",	cmd_remove,	usage_remove,	"rm" },
+	{ "patch",	cmd_patch,	usage_patch,	"pa" },
 	{ "revert",	cmd_revert,	usage_revert,	"rv" },
 	{ "commit",	cmd_commit,	usage_commit,	"ci" },
 	{ "send",	cmd_send,	usage_send,	"se" },
@@ -7102,6 +7106,133 @@ done:
 	TAILQ_FOREACH(pe, &paths, entry)
 		free((char *)pe->path);
 	got_pathlist_free(&paths);
+	free(cwd);
+	return error;
+}
+
+__dead static void
+usage_patch(void)
+{
+	fprintf(stderr, "usage: %s patch [patchfile]\n",
+	    getprogname());
+	exit(1);
+}
+
+static const struct got_error *
+patch_from_stdin(int *patchfd)
+{
+	const struct got_error *err = NULL;
+	ssize_t r;
+	char *path, buf[BUFSIZ];
+	sig_t sighup, sigint, sigquit;
+
+	err = got_opentemp_named_fd(&path, patchfd,
+	    GOT_TMPDIR_STR "/got-patch");
+	if (err)
+		return err;
+	unlink(path);
+	free(path);
+
+	sighup = signal(SIGHUP, SIG_DFL);
+	sigint = signal(SIGINT, SIG_DFL);
+	sigquit = signal(SIGQUIT, SIG_DFL);
+
+	for (;;) {
+		r = read(0, buf, sizeof(buf));
+		if (r == -1) {
+			err = got_error_from_errno("read");
+			break;
+		}
+		if (r == 0)
+			break;
+		if (write(*patchfd, buf, r) == -1) {
+			err = got_error_from_errno("write");
+			break;
+		}
+	}
+
+	signal(SIGHUP, sighup);
+	signal(SIGINT, sigint);
+	signal(SIGQUIT, sigquit);
+
+	if (err != NULL)
+		close(*patchfd);
+	return NULL;
+}
+
+static const struct got_error *
+cmd_patch(int argc, char *argv[])
+{
+	const struct got_error *error = NULL, *close_error = NULL;
+	struct got_worktree *worktree = NULL;
+	struct got_repository *repo = NULL;
+	char *cwd = NULL;
+	int ch;
+	int patchfd;
+
+	while ((ch = getopt(argc, argv, "")) != -1) {
+		switch (ch) {
+		default:
+			usage_patch();
+			/* NOTREACHED */
+		}
+	}
+
+	argc -= optind;
+	argv += optind;
+
+	if (argc == 0) {
+		error = patch_from_stdin(&patchfd);
+		if (error)
+			return error;
+	} else if (argc == 1) {
+		patchfd = open(argv[0], O_RDONLY);
+		if (patchfd == -1) {
+			error = got_error_from_errno2("open", argv[0]);
+			return error;
+		}
+	} else
+		usage_patch();
+
+	if ((cwd = getcwd(NULL, 0)) == NULL) {
+		error = got_error_from_errno("getcwd");
+		goto done;
+	}
+
+	error = got_worktree_open(&worktree, cwd);
+	if (error != NULL)
+		goto done;
+
+	const char *repo_path = got_worktree_get_repo_path(worktree);
+	error = got_repo_open(&repo, repo_path, NULL);
+	if (error != NULL)
+		goto done;
+
+	error = apply_unveil(got_repo_get_path(repo), 0,
+	    worktree ? got_worktree_get_root_path(worktree) : NULL);
+	if (error != NULL)
+		goto done;
+
+#ifndef PROFILE
+	if (pledge("stdio rpath wpath cpath proc exec sendfd flock",
+	    NULL) == -1)
+		err(1, "pledge");
+#endif
+
+	error = got_patch(patchfd, worktree, repo, &print_remove_status,
+	    &add_progress);
+
+done:
+	if (repo) {
+		close_error = got_repo_close(repo);
+		if (error == NULL)
+			error = close_error;
+	}
+	if (worktree != NULL) {
+		close_error = got_worktree_close(worktree);
+		if (error == NULL)
+			error = close_error;
+	}
 	free(cwd);
 	return error;
 }
@@ -7238,7 +7369,6 @@ choose_patch(int *choice, void *arg, unsigned char sta
 	return NULL;
 }
 
-
 static const struct got_error *
 cmd_revert(int argc, char *argv[])
 {
blob - bfdc8fac28522667c8ec28af0e4485c8e46a75a3
blob + 64f2cb93558b933d2ffdcc0da7dedecf78d8ee52
--- include/got_error.h
+++ include/got_error.h
@@ -162,6 +162,11 @@
 #define GOT_ERR_MERGE_BUSY	144
 #define GOT_ERR_MERGE_PATH	145
 #define GOT_ERR_FILE_BINARY	146
+#define GOT_ERR_PATCH_MALFORMED	147
+#define GOT_ERR_PATCH_TRUNCATED	148
+#define GOT_ERR_PATCH_DONT_APPLY 149
+#define GOT_ERR_PATCH_PATHS_DIFFER 150
+#define GOT_ERR_NO_PATCH	151
 
 static const struct got_error {
 	int code;
@@ -338,6 +343,12 @@ static const struct got_error {
 	{ GOT_ERR_MERGE_PATH,	"cannot merge branch which contains "
 	    "changes outside of this work tree's path prefix" },
 	{ GOT_ERR_FILE_BINARY, "found a binary file instead of text" },
+	{ GOT_ERR_PATCH_MALFORMED, "malformed patch" },
+	{ GOT_ERR_PATCH_TRUNCATED, "patch truncated" },
+	{ GOT_ERR_PATCH_DONT_APPLY, "patch doesn't apply" },
+	{ GOT_ERR_PATCH_PATHS_DIFFER, "the paths mentioned in the patch "
+	    "are different." },
+	{ GOT_ERR_NO_PATCH, "no patch found" },
 };
 
 /*
blob - /dev/null
blob + 3f56d45c54c3ff202d4e7db59288e3ec6717ed78 (mode 644)
--- /dev/null
+++ include/got_patch.h
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2022 Omar Polo <op@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/*
+ * Apply the (already opened) patch to the repository and register the
+ * status of the added and removed files.
+ *
+ * The patch file descriptor *must* be seekable.
+ */
+const struct got_error *
+got_patch(int, struct got_worktree *, struct got_repository *,
+    got_worktree_delete_cb, got_worktree_checkout_cb);
blob - 274e89878290befef48084afc0ae191cd5c36b16
blob + fef20e3a85c35f0faa4743d896a34ac04f0a4397
--- lib/got_lib_privsep.h
+++ lib/got_lib_privsep.h
@@ -44,6 +44,7 @@
 #define GOT_PROG_READ_PACK	got-read-pack
 #define GOT_PROG_READ_GITCONFIG	got-read-gitconfig
 #define GOT_PROG_READ_GOTCONFIG	got-read-gotconfig
+#define GOT_PROG_READ_PATCH	got-read-patch
 #define GOT_PROG_FETCH_PACK	got-fetch-pack
 #define GOT_PROG_INDEX_PACK	got-index-pack
 #define GOT_PROG_SEND_PACK	got-send-pack
@@ -68,6 +69,8 @@
 	GOT_STRINGVAL(GOT_LIBEXECDIR) "/" GOT_STRINGVAL(GOT_PROG_READ_GITCONFIG)
 #define GOT_PATH_PROG_READ_GOTCONFIG \
 	GOT_STRINGVAL(GOT_LIBEXECDIR) "/" GOT_STRINGVAL(GOT_PROG_READ_GOTCONFIG)
+#define GOT_PATH_PROG_READ_PATCH \
+	GOT_STRINGVAL(GOT_LIBEXECDIR) "/" GOT_STRINGVAL(GOT_PROG_READ_PATCH)
 #define GOT_PATH_PROG_FETCH_PACK \
 	GOT_STRINGVAL(GOT_LIBEXECDIR) "/" GOT_STRINGVAL(GOT_PROG_FETCH_PACK)
 #define GOT_PATH_PROG_SEND_PACK \
@@ -179,6 +182,14 @@ enum got_imsg_type {
 	GOT_IMSG_RAW_DELTA_OUTFD,
 	GOT_IMSG_RAW_DELTA_REQUEST,
 	GOT_IMSG_RAW_DELTA,
+
+	/* Messages related to patch files. */
+	GOT_IMSG_PATCH_FILE,
+	GOT_IMSG_PATCH_HUNK,
+	GOT_IMSG_PATCH_DONE,
+	GOT_IMSG_PATCH_LINE,
+	GOT_IMSG_PATCH,
+	GOT_IMSG_PATCH_EOF,
 };
 
 /* Structure for GOT_IMSG_ERROR. */
@@ -510,6 +521,24 @@ struct got_imsg_remotes {
 	int nremotes; /* This many GOT_IMSG_GITCONFIG_REMOTE messages follow. */
 };
 
+/*
+ * Structure for GOT_IMSG_PATCH data.
+ */
+struct got_imsg_patch {
+	char	old[PATH_MAX];
+	char	new[PATH_MAX];
+};
+
+/*
+ * Structure for GOT_IMSG_PATCH_HUNK data.
+ */
+struct got_imsg_patch_hunk {
+	long	oldfrom;
+	long	oldlines;
+	long	newfrom;
+	long	newlines;
+};
+
 struct got_remote_repo;
 struct got_pack;
 struct got_packidx;
blob - /dev/null
blob + 84226a57dca7da7a69187a76d804e8ceda7558ba (mode 644)
--- /dev/null
+++ lib/patch.c
@@ -0,0 +1,596 @@
+/*
+ * Copyright (c) 2022 Omar Polo <op@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ *
+ * Apply patches.
+ *
+ * Things that are still missing:
+ *     + "No final newline" handling
+ *
+ * Things that we may want to support:
+ *     + support indented patches?
+ *     + support other kinds of patches?
+ */
+
+#include <sys/types.h>
+#include <sys/queue.h>
+#include <sys/socket.h>
+#include <sys/uio.h>
+
+#include <limits.h>
+#include <sha1.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <imsg.h>
+
+#include "got_error.h"
+#include "got_object.h"
+#include "got_path.h"
+#include "got_reference.h"
+#include "got_cancel.h"
+#include "got_worktree.h"
+#include "got_opentemp.h"
+#include "got_patch.h"
+
+#include "got_lib_delta.h"
+#include "got_lib_object.h"
+#include "got_lib_privsep.h"
+
+#define MIN(a, b) ((a) < (b) ? (a) : (b))
+
+struct got_patch_hunk {
+	STAILQ_ENTRY(got_patch_hunk) entries;
+	long	old_from;
+	long	old_lines;
+	long	new_from;
+	long	new_lines;
+	size_t	len;
+	size_t	cap;
+	char	**lines;
+};
+
+struct got_patch {
+	char	*old;
+	char	*new;
+	STAILQ_HEAD(, got_patch_hunk) head;
+};
+
+static const struct got_error *
+send_patch(struct imsgbuf *ibuf, int fd)
+{
+	const struct got_error *err = NULL;
+
+	if (imsg_compose(ibuf, GOT_IMSG_PATCH_FILE, 0, 0, fd,
+	    NULL, 0) == -1) {
+		err = got_error_from_errno(
+		    "imsg_compose GOT_IMSG_PATCH_FILE");
+		close(fd);
+		return err;
+	}
+
+	if (imsg_flush(ibuf) == -1) {
+		err = got_error_from_errno("imsg_flush");
+		imsg_clear(ibuf);
+	}
+
+	return err;
+}
+
+static void
+patch_free(struct got_patch *p)
+{
+	struct got_patch_hunk *h;
+	size_t i;
+
+	while (!STAILQ_EMPTY(&p->head)) {
+		h = STAILQ_FIRST(&p->head);
+		STAILQ_REMOVE_HEAD(&p->head, entries);
+
+		for (i = 0; i < h->len; ++i)
+			free(h->lines[i]);
+		free(h->lines);
+		free(h);
+	}
+
+	free(p->new);
+	free(p->old);
+}
+
+static const struct got_error *
+pushline(struct got_patch_hunk *h, const char *line)
+{
+	void 	*t;
+	size_t	 newcap;
+
+	if (h->len == h->cap) {
+		if ((newcap = h->cap * 1.5) == 0)
+			newcap = 16;
+		t = recallocarray(h->lines, h->cap, newcap,
+		    sizeof(h->lines[0]));
+		if (t == NULL)
+			return got_error_from_errno("recallocarray");
+		h->lines = t;
+		h->cap = newcap;
+	}
+
+	if ((t = strdup(line)) == NULL)
+		return got_error_from_errno("strdup");
+
+	h->lines[h->len++] = t;
+	return NULL;
+}
+
+static const struct got_error *
+recv_patch(struct imsgbuf *ibuf, int *done, struct got_patch *p)
+{
+	const struct got_error *err = NULL;
+	struct imsg imsg;
+	struct got_imsg_patch_hunk hdr;
+	struct got_imsg_patch patch;
+	struct got_patch_hunk *h = NULL;
+	size_t datalen;
+
+	memset(p, 0, sizeof(*p));
+	STAILQ_INIT(&p->head);
+
+	err = got_privsep_recv_imsg(&imsg, ibuf, 0);
+	if (err)
+		return err;
+	if (imsg.hdr.type == GOT_IMSG_PATCH_EOF) {
+		*done = 1;
+		goto done;
+	}
+	if (imsg.hdr.type != GOT_IMSG_PATCH) {
+		err = got_error(GOT_ERR_PRIVSEP_MSG);
+		goto done;
+	}
+	datalen = imsg.hdr.len - IMSG_HEADER_SIZE;
+	if (datalen != sizeof(patch)) {
+		err = got_error(GOT_ERR_PRIVSEP_LEN);
+		goto done;
+	}
+	memcpy(&patch, imsg.data, sizeof(patch));
+	if (*patch.old != '\0' && (p->old = strdup(patch.old)) == NULL) {
+		err = got_error_from_errno("strdup");
+		goto done;
+	}
+	if (*patch.new != '\0' && (p->new = strdup(patch.new)) == NULL) {
+		err = got_error_from_errno("strdup");
+		goto done;
+	}
+
+	imsg_free(&imsg);
+
+	for (;;) {
+		char *t;
+
+		err = got_privsep_recv_imsg(&imsg, ibuf, 0);
+		if (err)
+			return err;
+
+		switch (imsg.hdr.type) {
+		case GOT_IMSG_PATCH_DONE:
+			goto done;
+		case GOT_IMSG_PATCH_HUNK:
+			datalen = imsg.hdr.len - IMSG_HEADER_SIZE;
+			if (datalen != sizeof(hdr)) {
+				err = got_error(GOT_ERR_PRIVSEP_LEN);
+				goto done;
+			}
+			memcpy(&hdr, imsg.data, sizeof(hdr));
+			if ((h = calloc(1, sizeof(*h))) == NULL) {
+				err = got_error_from_errno("calloc");
+				goto done;
+			}
+			h->old_from = hdr.oldfrom;
+			h->old_lines = hdr.oldlines;
+			h->new_from = hdr.newfrom;
+			h->new_lines = hdr.newlines;
+			STAILQ_INSERT_TAIL(&p->head, h, entries);
+			break;
+		case GOT_IMSG_PATCH_LINE:
+			if (h == NULL) {
+				err = got_error(GOT_ERR_PRIVSEP_MSG);
+				goto done;
+			}
+			datalen = imsg.hdr.len - IMSG_HEADER_SIZE;
+			t = imsg.data;
+			/* at least one char plus newline */
+			if (datalen < 2 || t[datalen-1] != '\0') {
+				err = got_error(GOT_ERR_PRIVSEP_MSG);
+				goto done;
+			}
+			if (*t != ' ' && *t != '-' && *t != '+') {
+				err = got_error(GOT_ERR_PRIVSEP_MSG);
+				goto done;
+			}
+			err = pushline(h, t);
+			if (err)
+				goto done;
+			break;
+		default:
+			err = got_error(GOT_ERR_PRIVSEP_MSG);
+			goto done;
+		}
+
+		imsg_free(&imsg);
+	}
+
+done:
+	imsg_free(&imsg);
+	return err;
+}
+
+/*
+ * Copy data from orig starting at copypos until pos into tmp.
+ * If pos is -1, copy until EOF.
+ */
+static const struct got_error *
+copy(FILE *tmp, FILE *orig, off_t copypos, off_t pos)
+{
+	char buf[BUFSIZ];
+	size_t len, r, w;
+
+	if (fseek(orig, copypos, SEEK_SET) == -1)
+		return got_error_from_errno("fseek");
+
+	while (pos == -1 || copypos < pos) {
+		len = sizeof(buf);
+		if (pos > 0)
+			len = MIN(len, (size_t)pos - copypos);
+		r = fread(buf, 1, len, orig);
+		if (r != len && ferror(orig))
+			return got_error_from_errno("fread");
+		w = fwrite(buf, 1, r, tmp);
+		if (w != r)
+			return got_error_from_errno("fwrite");
+		copypos += len;
+		if (r != len && feof(orig)) {
+			if (pos == -1)
+				return NULL;
+			return got_error(GOT_ERR_PATCH_DONT_APPLY);
+		}
+	}
+	return NULL;
+}
+
+static const struct got_error *
+locate_hunk(FILE *orig, struct got_patch_hunk *h, long *lineno)
+{
+	const struct got_error *err = NULL;
+	char *line = NULL;
+	char mode = *h->lines[0];
+	size_t linesize = 0;
+	ssize_t linelen;
+	off_t match = -1;
+	long match_lineno = -1;
+
+	for (;;) {
+		linelen = getline(&line, &linesize, orig);
+		if (linelen == -1) {
+			if (ferror(orig))
+				err = got_error_from_errno("getline");
+			else if (match == -1)
+				err = got_error(GOT_ERR_PATCH_DONT_APPLY);
+			break;
+		}
+		(*lineno)++;
+
+		if ((mode == ' ' && !strcmp(h->lines[0]+1, line)) ||
+		    (mode == '-' && !strcmp(h->lines[0]+1, line)) ||
+		    (mode == '+' && *lineno == h->old_from)) {
+			match = ftello(orig);
+			if (match == -1) {
+				err = got_error_from_errno("ftello");
+				break;
+			}
+			match -= linelen;
+			match_lineno = (*lineno)-1;
+		}
+
+		if (*lineno >= h->old_from && match != -1)
+			break;
+	}
+
+	if (err == NULL) {
+		*lineno = match_lineno;
+		if (fseek(orig, match, SEEK_SET) == -1)
+			err = got_error_from_errno("fseek");
+	}
+
+	free(line);
+	return err;
+}
+
+static const struct got_error *
+test_hunk(FILE *orig, struct got_patch_hunk *h)
+{
+	const struct got_error *err = NULL;
+	char *line = NULL;
+	size_t linesize = 0, i = 0;
+	ssize_t linelen;
+
+	for (i = 0; i < h->len; ++i) {
+		switch (*h->lines[i]) {
+		case '+':
+			continue;
+		case ' ':
+		case '-':
+			linelen = getline(&line, &linesize, orig);
+			if (linelen == -1) {
+				if (ferror(orig))
+					err = got_error_from_errno("getline");
+				else
+					err = got_error(
+					    GOT_ERR_PATCH_DONT_APPLY);
+				goto done;
+			}
+			if (strcmp(h->lines[i]+1, line)) {
+				err = got_error(GOT_ERR_PATCH_DONT_APPLY);
+				goto done;
+			}
+			break;
+		}
+	}
+
+done:
+	free(line);
+	return err;
+}
+
+static const struct got_error *
+apply_hunk(FILE *tmp, struct got_patch_hunk *h, long *lineno)
+{
+	size_t i = 0;
+
+	for (i = 0; i < h->len; ++i) {
+		switch (*h->lines[i]) {
+		case ' ':
+			if (fprintf(tmp, "%s", h->lines[i]+1) < 0)
+				return got_error_from_errno("fprintf");
+			/* fallthrough */
+		case '-':
+			(*lineno)++;
+			break;
+		case '+':
+			if (fprintf(tmp, "%s", h->lines[i]+1) < 0)
+				return got_error_from_errno("fprintf");
+			break;
+		}
+	}
+	return NULL;
+}
+
+static const struct got_error *
+apply_patch(struct got_worktree *worktree, struct got_repository *repo,
+    struct got_patch *p, got_worktree_delete_cb delete_cb,
+    got_worktree_checkout_cb add_cb)
+{
+	const struct got_error *err = NULL;
+	struct got_pathlist_head paths;
+	struct got_pathlist_entry *pe;
+	char *path = NULL, *tmppath = NULL;
+	FILE *orig = NULL, *tmp = NULL;
+	struct got_patch_hunk *h;
+	size_t i;
+	long lineno = 0;
+	off_t copypos, pos;
+	char *line = NULL;
+	size_t linesize = 0;
+	ssize_t linelen;
+
+	TAILQ_INIT(&paths);
+
+	if (p->old == NULL && p->new == NULL)
+		return got_error(GOT_ERR_PATCH_MALFORMED);
+
+	err = got_worktree_resolve_path(&path, worktree,
+	    p->new != NULL ? p->new : p->old);
+	if (err)
+		return err;
+	err = got_pathlist_insert(&pe, &paths, path, NULL);
+	if (err)
+		goto done;
+
+	if (p->old != NULL && p->new == NULL) {
+		/*
+		 * special case: delete a file.  don't try to match
+		 * the lines but just schedule the removal.
+		 */
+		err = got_worktree_schedule_delete(worktree, &paths,
+		    0, NULL, delete_cb, NULL, repo, 0, 0);
+		goto done;
+	} else if (p->old != NULL && strcmp(p->old, p->new)) {
+		err = got_error(GOT_ERR_PATCH_PATHS_DIFFER);
+		goto done;
+	}
+
+	err = got_opentemp_named(&tmppath, &tmp,
+	    got_worktree_get_root_path(worktree));
+	if (err)
+		goto done;
+
+	if (p->old == NULL) {				/* create */
+		h = STAILQ_FIRST(&p->head);
+		if (h == NULL || STAILQ_NEXT(h, entries) != NULL) {
+			err = got_error(GOT_ERR_PATCH_MALFORMED);
+			goto done;
+		}
+		for (i = 0; i < h->len; ++i) {
+			if (fprintf(tmp, "%s", h->lines[i]+1) < 0) {
+				err = got_error_from_errno("fprintf");
+				goto done;
+			}
+		}
+		goto rename;
+	}
+
+	if ((orig = fopen(path, "r")) == NULL) {
+		err = got_error_from_errno2("fopen", path);
+		goto done;
+	}
+
+	copypos = 0;
+	STAILQ_FOREACH(h, &p->head, entries) {
+	tryagain:
+		err = locate_hunk(orig, h, &lineno);
+		if (err != NULL)
+			goto done;
+		if ((pos = ftello(orig)) == -1) {
+			err = got_error_from_errno("ftello");
+			goto done;
+		}
+		err = copy(tmp, orig, copypos, pos);
+		if (err != NULL)
+			goto done;
+		copypos = pos;
+
+		err = test_hunk(orig, h);
+		if (err != NULL && err->code == GOT_ERR_PATCH_DONT_APPLY) {
+			/*
+			 * try to apply the hunk again starting the search
+			 * after the previous partial match.
+			 */
+			if (fseek(orig, pos, SEEK_SET) == -1) {
+				err = got_error_from_errno("fseek");
+				goto done;
+			}
+			linelen = getline(&line, &linesize, orig);
+			if (linelen == -1) {
+				err = got_error_from_errno("getline");
+				goto done;
+			}
+			lineno++;
+			goto tryagain;
+		}
+		if (err != NULL)
+			goto done;
+
+		err = apply_hunk(tmp, h, &lineno);
+		if (err != NULL)
+			goto done;
+		
+		copypos = ftello(orig);
+		if (copypos == -1) {
+			err = got_error_from_errno("ftello");
+			goto done;
+		}
+	}
+
+	if (!feof(orig)) {
+		err = copy(tmp, orig, copypos, -1);
+		if (err)
+			goto done;
+	}
+
+rename:
+	if (rename(tmppath, path) == -1) {
+		err = got_error_from_errno3("rename", tmppath, path);
+		goto done;
+	}
+
+	if (p->old == NULL)
+		err = got_worktree_schedule_add(worktree, &paths,
+		    add_cb, NULL, repo, 1);
+	else
+		printf("M  %s\n", path); /* XXX */
+done:
+	if (err != NULL && p->old == NULL && path != NULL)
+		unlink(path);
+	if (tmp != NULL)
+		fclose(tmp);
+	if (tmppath != NULL)
+		unlink(tmppath);
+	free(tmppath);
+	if (orig != NULL) {
+		if (p->old == NULL && err != NULL)
+			unlink(path);
+		fclose(orig);
+	}
+	free(path);
+	free(line);
+	got_pathlist_free(&paths);
+	return err;
+}
+
+const struct got_error *
+got_patch(int fd, struct got_worktree *worktree, struct got_repository *repo,
+    got_worktree_delete_cb delete_cb, got_worktree_checkout_cb add_cb)
+{
+	const struct got_error *err = NULL;
+	struct imsgbuf *ibuf;
+	int imsg_fds[2] = {-1, -1};
+	int done = 0;
+	pid_t pid;
+
+	ibuf = calloc(1, sizeof(*ibuf));
+	if (ibuf == NULL) {
+		err = got_error_from_errno("calloc");
+		goto done;
+	}
+
+	if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, imsg_fds) == -1) {
+		err = got_error_from_errno("socketpair");
+		goto done;
+	}
+
+	pid = fork();
+	if (pid == -1) {
+		err = got_error_from_errno("fork");
+		goto done;
+	} else if (pid == 0) {
+		got_privsep_exec_child(imsg_fds, GOT_PATH_PROG_READ_PATCH,
+		    NULL);
+		/* not reached */
+	}
+
+	if (close(imsg_fds[1]) == -1) {
+		err = got_error_from_errno("close");
+		goto done;
+	}
+	imsg_fds[1] = -1;
+	imsg_init(ibuf, imsg_fds[0]);
+
+	err = send_patch(ibuf, fd);
+	fd = -1;
+	if (err)
+		goto done;
+
+	while (!done && err == NULL) {
+		struct got_patch p;
+
+		err = recv_patch(ibuf, &done, &p);
+		if (err || done)
+			break;
+
+		err = apply_patch(worktree, repo, &p, delete_cb, add_cb);
+		patch_free(&p);
+		if (err)
+			break;
+	}
+
+done:
+	if (fd != -1 && close(fd) == -1 && err == NULL)
+		err = got_error_from_errno("close");
+	if (ibuf != NULL)
+		imsg_clear(ibuf);
+	if (imsg_fds[0] != -1 && close(imsg_fds[0]) == -1 && err == NULL)
+		err = got_error_from_errno("close");
+	if (imsg_fds[1] != -1 && close(imsg_fds[1]) == -1 && err == NULL)
+		err = got_error_from_errno("close");
+	return err;
+}
blob - 4cbc40e6f66cdf7a166db6612c052858ffaba1b6
blob + 67b0e54997c29a12feaca8d74bc968633b1711b4
--- lib/privsep.c
+++ lib/privsep.c
@@ -2842,6 +2842,7 @@ got_privsep_unveil_exec_helpers(void)
 	    GOT_PATH_PROG_READ_TAG,
 	    GOT_PATH_PROG_READ_GITCONFIG,
 	    GOT_PATH_PROG_READ_GOTCONFIG,
+	    GOT_PATH_PROG_READ_PATCH,
 	    GOT_PATH_PROG_FETCH_PACK,
 	    GOT_PATH_PROG_INDEX_PACK,
 	    GOT_PATH_PROG_SEND_PACK,
blob - 3783b56689f6ab58fbacbd8f0f990a7154d90f61
blob + cfd4876a2dfa135816bb51fb862396c0cd6a4331
--- libexec/Makefile
+++ libexec/Makefile
@@ -1,5 +1,6 @@
 SUBDIR = got-read-blob got-read-commit got-read-object got-read-tree \
 	got-read-tag got-fetch-pack got-index-pack got-read-pack \
-	got-read-gitconfig got-read-gotconfig got-send-pack
+	got-read-gitconfig got-read-gotconfig got-send-pack \
+	got-read-patch
 
 .include <bsd.subdir.mk>
blob - /dev/null
blob + 9eddbae60cbd3e82dc3178ffebc9903391caa40c (mode 644)
--- /dev/null
+++ libexec/got-read-patch/Makefile
@@ -0,0 +1,13 @@
+.PATH:${.CURDIR}/../../lib
+
+.include "../../got-version.mk"
+
+PROG=		got-read-patch
+SRCS=		got-read-patch.c error.c inflate.c object_parse.c \
+		path.c privsep.c sha1.c
+
+CPPFLAGS = -I${.CURDIR}/../../include -I${.CURDIR}/../../lib
+LDADD = -lz -lutil
+DPADD = ${LIBZ} ${LIBUTIL}
+
+.include <bsd.prog.mk>
blob - /dev/null
blob + ed5eb50b17c3c73f369b043bdf1ea72f54f5ff88 (mode 644)
--- /dev/null
+++ libexec/got-read-patch/got-read-patch.c
@@ -0,0 +1,480 @@
+/*
+ * Copyright 1986, Larry Wall
+ * 
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following condition is met:
+ * 1. Redistributions of source code must retain the above copyright notice,
+ * this condition and the following disclaimer.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+/*
+ * Copyright (c) 2022 Omar Polo <op@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/types.h>
+#include <sys/queue.h>
+#include <sys/uio.h>
+
+#include <ctype.h>
+#include <limits.h>
+#include <paths.h>
+#include <sha1.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <imsg.h>
+
+#include "got_error.h"
+#include "got_object.h"
+
+#include "got_lib_delta.h"
+#include "got_lib_object.h"
+#include "got_lib_privsep.h"
+
+struct imsgbuf ibuf;
+
+static const struct got_error *
+send_patch(const char *oldname, const char *newname)
+{
+	struct got_imsg_patch p;
+
+	memset(&p, 0, sizeof(p));
+
+	if (oldname != NULL)
+		strlcpy(p.old, oldname, sizeof(p.old));
+	if (newname != NULL)
+		strlcpy(p.new, newname, sizeof(p.new));
+
+	if (imsg_compose(&ibuf, GOT_IMSG_PATCH, 0, 0, -1,
+	    &p, sizeof(p)) == -1)
+		return got_error_from_errno("imsg_compose GOT_IMSG_PATCH");
+	return NULL;
+}
+
+static const struct got_error *
+send_patch_done(void)
+{
+	if (imsg_compose(&ibuf, GOT_IMSG_PATCH_DONE, 0, 0, -1,
+	    NULL, 0) == -1)
+		return got_error_from_errno("imsg_compose GOT_IMSG_PATCH_EOF");
+	if (imsg_flush(&ibuf) == -1)
+		return got_error_from_errno("imsg_flush");
+	return NULL;
+}
+
+/* based on fetchname from usr.bin/patch/util.c */
+static const struct got_error *
+filename(const char *at, char **name, int strip)
+{
+	char	*fullname, *t;
+	int	 l, tab;
+
+	*name = NULL;
+	if (*at == '\0')
+		return NULL;
+
+	while (isspace((unsigned char)*at))
+		at++;
+
+	/* files can be created or removed by diffing against /dev/null */
+	if (!strncmp(at, _PATH_DEVNULL, sizeof(_PATH_DEVNULL)-1))
+		return NULL;
+
+	t = strdup(at);
+	if (t == NULL)
+		return got_error_from_errno("strdup");
+	*name = fullname = t;
+	tab = strchr(t, '\t') != NULL;
+
+	/* strip off path components and NUL-terminate */
+	for (l = strip;
+	    *t != '\0' && ((tab && *t != '\t') || !isspace((unsigned char)*t));
+	    ++t) {
+		if (t[0] == '/' && t[1] != '/' && t[1] != '\0')
+			if (--l >= 0)
+				*name = t+1;
+	}
+	*t = '\0';
+
+	*name = strdup(*name);
+	free(fullname);
+	if (*name == NULL)
+		return got_error_from_errno("strdup");
+	return NULL;
+}
+
+static const struct got_error *
+find_patch(FILE *fp)
+{
+	const struct got_error *err = NULL;
+	char	*old = NULL, *new = NULL;
+	char	*line = NULL;
+	size_t	 linesize = 0;
+	ssize_t	 linelen;
+	int	 create, git = 0;
+
+	while ((linelen = getline(&line, &linesize, fp)) != -1) {
+		/*
+		 * Ignore the Index name like GNU and larry' patch,
+		 * we don't have to follow POSIX.
+		 */
+
+		if (git && !strncmp(line, "--- a/", 6)) {
+			free(old);
+			err = filename(line+6, &old, 0);
+		} else if (!strncmp(line, "--- ", 4)) {
+			free(old);
+			err = filename(line+4, &old, 0);
+		} else if (git && !strncmp(line, "+++ b/", 6)) {
+			free(new);
+			err = filename(line+6, &new, 0);
+		} else if (!strncmp(line, "+++ ", 4)) {
+			free(new);
+			err = filename(line+4, &new, 0);
+		} else if (!strncmp(line, "diff --git a/", 13))
+			git = 1;
+
+		if (err)
+			break;
+
+		if (!strncmp(line, "@@ -", 4)) {
+			create = !strncmp(line+4, "0,0", 3);
+			if ((old == NULL && new == NULL) ||
+			    (!create && old == NULL))
+				err = got_error(GOT_ERR_PATCH_MALFORMED);
+			else
+				err = send_patch(old, new);
+
+			free(old);
+			free(new);
+
+			if (err)
+				break;
+
+			/* rewind to previous line */
+			if (fseek(fp, linelen * -1, SEEK_CUR) == -1)
+				err = got_error_from_errno("fseek");
+			break;
+		}
+	}
+
+	free(line);
+	if (ferror(fp) && err == NULL)
+		err = got_error_from_errno("getline");
+	if (feof(fp) && err == NULL)
+		err = got_error(GOT_ERR_NO_PATCH);
+	return err;
+}
+
+static const struct got_error *
+strtolnum(char **str, long *n)
+{
+	char		*p, c;
+	const char	*errstr;
+
+	for (p = *str; isdigit((unsigned char)*p); ++p)
+		/* nop */;
+
+	c = *p;
+	*p = '\0';
+
+	*n = strtonum(*str, 0, LONG_MAX, &errstr);
+	if (errstr != NULL)
+		return got_error(GOT_ERR_PATCH_MALFORMED);
+
+	*p = c;
+	*str = p;
+	return NULL;
+}
+
+static const struct got_error *
+parse_hdr(char *s, int *ok, struct got_imsg_patch_hunk *hdr)
+{
+	static const struct got_error *err = NULL;
+
+	*ok = 1;
+	if (strncmp(s, "@@ -", 4)) {
+		*ok = 0;
+		return NULL;
+	}
+
+	s += 4;
+	if (!*s)
+		return NULL;
+	err = strtolnum(&s, &hdr->oldfrom);
+	if (err)
+		return err;
+	if (*s == ',') {
+		s++;
+		err = strtolnum(&s, &hdr->oldlines);
+		if (err)
+			return err;
+	} else
+		hdr->oldlines = 1;
+
+	if (*s == ' ')
+		s++;
+
+	if (*s != '+' || !*++s)
+		return got_error(GOT_ERR_PATCH_MALFORMED);
+	err = strtolnum(&s, &hdr->newfrom);
+	if (err)
+		return err;
+	if (*s == ',') {
+		s++;
+		err = strtolnum(&s, &hdr->newlines);
+		if (err)
+			return err;
+	} else
+		hdr->newlines = 1;
+
+	if (*s == ' ')
+		s++;
+
+	if (*s != '@')
+		return got_error(GOT_ERR_PATCH_MALFORMED);
+
+	if (hdr->oldfrom >= LONG_MAX - hdr->oldlines ||
+	    hdr->newfrom >= LONG_MAX - hdr->newlines ||
+	    /* not so sure about this one */
+	    hdr->oldlines >= LONG_MAX - hdr->newlines - 1)
+		return got_error(GOT_ERR_PATCH_MALFORMED);
+
+	if (hdr->oldlines == 0) {
+		/* larry says to "do append rather than insert"; I don't
+		 * quite get it, but i trust him.
+		 */
+		hdr->oldfrom++;
+	}
+
+	if (imsg_compose(&ibuf, GOT_IMSG_PATCH_HUNK, 0, 0, -1,
+	    hdr, sizeof(*hdr)) == -1)
+		return got_error_from_errno(
+		    "imsg_compose GOT_IMSG_PATCH_HUNK");
+	return NULL;
+}
+
+static const struct got_error *
+send_line(const char *line)
+{
+	static const struct got_error *err = NULL;
+	char *p = NULL;
+
+	if (*line != '+' && *line != '-' && *line != ' ') {
+		if (asprintf(&p, " %s", line) == -1)
+			return got_error_from_errno("asprintf");
+		line = p;
+	}
+
+	if (imsg_compose(&ibuf, GOT_IMSG_PATCH_LINE, 0, 0, -1,
+	    line, strlen(line)+1) == -1)
+		err = got_error_from_errno(
+		    "imsg_compose GOT_IMSG_PATCH_LINE");
+
+	free(p);
+	return err;
+}
+
+static const struct got_error *
+parse_hunk(FILE *fp, int *ok)
+{
+	static const struct got_error *err = NULL;
+	struct got_imsg_patch_hunk hdr;
+	char	*line = NULL, ch;
+	size_t	 linesize = 0;
+	ssize_t	 linelen;
+	long	 leftold, leftnew;
+
+	linelen = getline(&line, &linesize, fp);
+	if (linelen == -1) {
+		*ok = 0;
+		goto done;
+	}
+
+	err = parse_hdr(line, ok, &hdr);
+	if (err)
+		goto done;
+	if (!*ok) {
+		if (fseek(fp, linelen * -1, SEEK_CUR) == -1)
+			err = got_error_from_errno("fseek");
+		goto done;
+	}
+
+	leftold = hdr.oldlines;
+	leftnew = hdr.newlines;
+
+	while (leftold > 0 || leftnew > 0) {
+		linelen = getline(&line, &linesize, fp);
+		if (linelen == -1) {
+			if (ferror(fp)) {
+				err = got_error_from_errno("getline");
+				goto done;
+			}
+
+			/* trailing newlines may be chopped */
+			if (leftold < 3 && leftnew < 3) {
+				*ok = 0;
+				break;
+			}
+
+			err = got_error(GOT_ERR_PATCH_TRUNCATED);
+			goto done;
+		}
+
+		/* usr.bin/patch allows '=' as context char */
+		if (*line == '=')
+			*line = ' ';
+
+		ch = *line;
+		if (ch == '\t' || ch == '\n')
+			ch = ' ';	/* the space got eaten */
+
+		switch (ch) {
+		case '-':
+			leftold--;
+			break;
+		case ' ':
+			leftold--;
+			leftnew--;
+			break;
+		case '+':
+			leftnew--;
+			break;
+		default:
+			err = got_error(GOT_ERR_PATCH_MALFORMED);
+			goto done;
+		}
+
+		if (leftold < 0 || leftnew < 0) {
+			err = got_error(GOT_ERR_PATCH_MALFORMED);
+			goto done;
+		}
+
+		err = send_line(line);
+		if (err)
+			goto done;
+	}
+
+done:
+	free(line);
+	return err;
+}
+
+static const struct got_error *
+read_patch(struct imsgbuf *ibuf, int fd)
+{
+	const struct got_error *err = NULL;
+	FILE *fp;
+	int ok, patch_found = 0;
+
+	if ((fp = fdopen(fd, "r")) == NULL) {
+		err = got_error_from_errno("fdopen");
+		close(fd);
+		return err;
+	}
+
+	while (!feof(fp)) {
+		err = find_patch(fp);
+		if (err)
+			goto done;
+
+		patch_found = 1;
+		for (;;) {
+			err = parse_hunk(fp, &ok);
+			if (err)
+				goto done;
+			if (!ok) {
+				err = send_patch_done();
+				if (err)
+					goto done;
+				break;
+			}
+		}
+	}
+
+done:
+	fclose(fp);
+
+	/* ignore trailing gibberish */
+	if (err != NULL && err->code == GOT_ERR_NO_PATCH && patch_found)
+		err = NULL;
+
+	return err;
+}
+
+int
+main(int argc, char **argv)
+{
+	const struct got_error *err = NULL;
+	struct imsg imsg;
+#if 0
+	static int attached;
+	while (!attached)
+		sleep(1);
+#endif
+
+	imsg_init(&ibuf, GOT_IMSG_FD_CHILD);
+#ifndef PROFILE
+	/* revoke access to most system calls */
+	if (pledge("stdio recvfd", NULL) == -1) {
+		err = got_error_from_errno("pledge");
+		got_privsep_send_error(&ibuf, err);
+		return 1;
+	}
+#endif
+
+	err = got_privsep_recv_imsg(&imsg, &ibuf, 0);
+	if (err)
+		goto done;
+	if (imsg.hdr.type != GOT_IMSG_PATCH_FILE || imsg.fd == -1) {
+		err = got_error(GOT_ERR_PRIVSEP_MSG);
+		goto done;
+	}
+
+	err = read_patch(&ibuf, imsg.fd);
+	if (err)
+		goto done;
+	if (imsg_compose(&ibuf, GOT_IMSG_PATCH_EOF, 0, 0, -1,
+	    NULL, 0) == -1) {
+		err = got_error_from_errno("imsg_compose GOT_IMSG_PATCH_EOF");
+		goto done;
+	}
+	err = got_privsep_flush_imsg(&ibuf);
+done:
+	imsg_free(&imsg);
+	if (err != NULL) {
+		got_privsep_send_error(&ibuf, err);
+		err = NULL;
+	}
+	if (close(GOT_IMSG_FD_CHILD) == -1 && err == NULL)
+		err = got_error_from_errno("close");
+	if (err && err->code != GOT_ERR_PRIVSEP_PIPE)
+		fprintf(stderr, "%s: %s\n", getprogname(), err->msg);
+	return err ? 1 : 0;
+}
blob - 54055c09da65df95bc8676121ad774abaed5f07c
blob + a1b33c05a7dbcb845170a3d4eabcc5a6cbc68802
--- regress/cmdline/Makefile
+++ regress/cmdline/Makefile
@@ -1,6 +1,7 @@
 REGRESS_TARGETS=checkout update status log add rm diff blame branch tag \
 	ref commit revert cherrypick backout rebase import histedit \
-	integrate merge stage unstage cat clone fetch tree pack cleanup
+	integrate merge stage unstage cat clone fetch tree patch pack \
+	cleanup
 NOOBJ=Yes
 
 GOT_TEST_ROOT=/tmp
@@ -86,6 +87,9 @@ send:
 tree:
 	./tree.sh -q -r "$(GOT_TEST_ROOT)"
 
+patch:
+	./patch.sh -q -r "$(GOT_TEST_ROOT)"
+
 pack:
 	./pack.sh -q -r "$(GOT_TEST_ROOT)"
 
blob - /dev/null
blob + cb9ff81d665f65b182c6a4ecb6bd3b4185b0ad6d (mode 755)
--- /dev/null
+++ regress/cmdline/patch.sh
@@ -0,0 +1,639 @@
+#!/bin/sh
+#
+# Copyright (c) 2022 Omar Polo <op@openbsd.org>
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+. ./common.sh
+
+test_patch_simple_add_file() {
+	local testroot=`test_init patch_simple_add_file`
+
+	got checkout $testroot/repo $testroot/wt > /dev/null
+	ret=$?
+	if [ $ret != 0 ]; then
+		test_done $testroot $ret
+		return 1
+	fi
+
+	cat <<EOF > $testroot/wt/patch
+--- /dev/null
++++ eta
+@@ -0,0 +1 @@
++eta
+EOF
+
+	(cd $testroot/wt && got patch patch) > $testroot/stdout
+	ret=$?
+	if [ $ret != 0 ]; then
+		test_done $testroot $ret
+		return 1
+	fi
+
+	echo "A  eta" > $testroot/stdout.expected
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret != 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done $testroot $ret
+		return 1
+	fi
+
+	echo eta > $testroot/wt/eta.expected
+	cmp -s $testroot/wt/eta.expected $testroot/wt/eta
+	ret=$?
+	if [ $ret != 0 ]; then
+		diff -u $testroot/wt/eta.expected $testroot/wt/eta
+	fi
+	test_done $testroot $ret
+}
+
+test_patch_simple_rm_file() {
+	local testroot=`test_init patch_simple_rm_file`
+
+	got checkout $testroot/repo $testroot/wt > /dev/null
+	ret=$?
+	if [ $ret != 0 ]; then
+		test_done $testroot $ret
+		return 1
+	fi
+
+	cat <<EOF > $testroot/wt/patch
+--- alpha
++++ /dev/null
+@@ -1 +0,0 @@
+-alpha
+EOF
+
+	echo "D  alpha" > $testroot/stdout.expected
+
+	(cd $testroot/wt && got patch patch) > $testroot/stdout
+	ret=$?
+	if [ $ret != 0 ]; then
+		test_done $testroot $ret
+		return 1
+	fi
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret != 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done $testroot $ret
+		return 1
+	fi
+
+	if [ -f $testroot/wt/alpha ]; then
+		ret=1
+		echo "alpha still exists!"
+	fi
+	test_done $testroot $ret
+}
+
+test_patch_simple_edit_file() {
+	local testroot=`test_init patch_simple_edit_file`
+
+	got checkout $testroot/repo $testroot/wt > /dev/null
+	ret=$?
+	if [ $ret != 0 ]; then
+		test_done $testroot $ret
+		return 1
+	fi
+
+	cat <<EOF > $testroot/wt/patch
+--- alpha
++++ alpha
+@@ -1 +1 @@
+-alpha
++alpha is my favourite character
+EOF
+
+	echo "M  alpha" > $testroot/stdout.expected
+
+	(cd $testroot/wt && got patch patch) > $testroot/stdout
+	ret=$?
+	if [ $ret != 0 ]; then
+		test_done $testroot $ret
+		return 1
+	fi
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret != 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done $testroot $ret
+		return 1
+	fi
+
+	echo 'alpha is my favourite character' > $testroot/wt/alpha.expected
+	cmp -s $testroot/wt/alpha.expected $testroot/wt/alpha
+	ret=$?
+	if [ $ret != 0 ]; then
+		diff -u $testroot/wt/alpha.expected $testroot/wt/alpha
+	fi
+	test_done $testroot $ret
+}
+
+test_patch_prepend_line() {
+	local testroot=`test_init patch_prepend_line`
+
+	got checkout $testroot/repo $testroot/wt > /dev/null
+	ret=$?
+	if [ $ret != 0 ]; then
+		test_done $testroot $ret
+		return 1
+	fi
+
+	cat <<EOF > $testroot/wt/patch
+--- alpha
++++ alpha
+@@ -1 +1,2 @@
++hatsuseno
+ alpha
+EOF
+
+	echo "M  alpha" > $testroot/stdout.expected
+
+	(cd $testroot/wt && got patch patch) > $testroot/stdout
+	ret=$?
+	if [ $ret != 0 ]; then
+		test_done $testroot $ret
+		return 1
+	fi
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret != 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done $testroot $ret
+		return 1
+	fi
+
+	echo hatsuseno > $testroot/wt/alpha.expected
+	echo alpha    >> $testroot/wt/alpha.expected
+	cmp -s $testroot/wt/alpha.expected $testroot/wt/alpha
+	ret=$?
+	if [ $ret != 0 ]; then
+		diff -u $testroot/wt/alpha.expected $testroot/wt/alpha
+	fi
+	test_done $testroot $ret
+}
+
+test_patch_replace_line() {
+	local testroot=`test_init patch_replace_line`
+
+	got checkout $testroot/repo $testroot/wt > /dev/null
+	ret=$?
+	if [ $ret != 0 ]; then
+		test_done $testroot $ret
+		return 1
+	fi
+
+	jot 10 > $testroot/wt/numbers
+	(cd $testroot/wt/ && got add numbers && got ci -m 'add numbers') \
+		>/dev/null
+	ret=$?
+	if [ $ret != 0 ]; then
+		test_done $testroot $ret
+		return 1
+	fi
+
+	cat <<EOF > $testroot/wt/patch
+--- numbers
++++ numbers
+@@ -3,7 +3,7 @@
+ 3
+ 4
+ 5
+-6
++foo
+ 7
+ 8
+ 9
+EOF
+
+	echo "M  numbers" > $testroot/stdout.expected
+
+	(cd $testroot/wt && got patch patch) > $testroot/stdout
+	ret=$?
+	if [ $ret != 0 ]; then
+		test_done $testroot $ret
+		return 1
+	fi
+
+	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
+
+	jot 10 | sed 's/6/foo/' > $testroot/wt/numbers.expected
+	cmp -s $testroot/wt/numbers.expected $testroot/wt/numbers
+	ret=$?
+	if [ $ret != 0 ]; then
+		diff -u $testroot/wt/numbers.expected $testroot/wt/numbers
+	fi
+	test_done $testroot $ret
+}
+
+test_patch_multiple_hunks() {
+	local testroot=`test_init patch_replace_multiple_lines`
+
+	got checkout $testroot/repo $testroot/wt > /dev/null
+	ret=$?
+	if [ $ret != 0 ]; then
+		test_done $testroot $ret
+		return 1
+	fi
+
+	jot 100 > $testroot/wt/numbers
+	(cd $testroot/wt/ && got add numbers && got ci -m 'add numbers') \
+		>/dev/null
+	ret=$?
+	if [ $ret != 0 ]; then
+		test_done $testroot $ret
+		return 1
+	fi
+
+	cat <<EOF > $testroot/wt/patch
+--- numbers
++++ numbers
+@@ -3,7 +3,7 @@
+ 3
+ 4
+ 5
+-6
++foo
+ 7
+ 8
+ 9
+@@ -57,7 +57,7 @@
+ 57
+ 58
+ 59
+-60
++foo foo
+ 61
+ 62
+ 63
+@@ -98,3 +98,6 @@
+ 98
+ 99
+ 100
++101
++102
++...
+EOF
+
+	echo "M  numbers" > $testroot/stdout.expected
+
+	(cd $testroot/wt && got patch patch) > $testroot/stdout
+	ret=$?
+	if [ $ret != 0 ]; then
+		test_done $testroot $ret
+		return 1
+	fi
+
+	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
+
+	jot 100 | sed -e 's/^6$/foo/' -e 's/^60$/foo foo/' \
+		> $testroot/wt/numbers.expected
+	echo "101" >> $testroot/wt/numbers.expected
+	echo "102" >> $testroot/wt/numbers.expected
+	echo "..." >> $testroot/wt/numbers.expected
+
+	cmp -s $testroot/wt/numbers.expected $testroot/wt/numbers
+	ret=$?
+	if [ $ret != 0 ]; then
+		diff -u $testroot/wt/numbers.expected $testroot/wt/numbers
+	fi
+	test_done $testroot $ret
+}
+
+test_patch_multiple_files() {
+	local testroot=`test_init patch_multiple_files`
+
+	got checkout $testroot/repo $testroot/wt > /dev/null
+	ret=$?
+	if [ $ret != 0 ]; then
+		test_done $testroot $ret
+		return 1
+	fi
+
+	cat <<EOF > $testroot/wt/patch
+--- alpha	Mon Mar  7 19:02:07 2022
++++ alpha	Mon Mar  7 19:01:53 2022
+@@ -1 +1,3 @@
++new
+ alpha
++available
+--- beta	Mon Mar  7 19:02:11 2022
++++ beta	Mon Mar  7 19:01:46 2022
+@@ -1 +1,3 @@
+ beta
++was
++improved
+--- gamma/delta	Mon Mar  7 19:02:17 2022
++++ gamma/delta	Mon Mar  7 19:01:37 2022
+@@ -1 +1 @@
+-delta
++delta new
+EOF
+
+	echo "M  alpha" > $testroot/stdout.expected
+	echo "M  beta" >> $testroot/stdout.expected
+	echo "M  gamma/delta" >> $testroot/stdout.expected
+
+	(cd $testroot/wt && got patch patch) > $testroot/stdout
+	ret=$?
+	if [ $ret != 0 ]; then
+		test_done $testrot $ret
+		return 1
+	fi
+
+	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 'new\nalpha\navailable\n' > $testroot/wt/alpha.expected
+	printf 'beta\nwas\nimproved\n' > $testroot/wt/beta.expected
+	printf 'delta new\n' > $testroot/wt/gamma/delta.expected
+
+	for f in alpha beta gamma/delta; do
+		cmp -s $testroot/wt/$f.expected $testroot/wt/$f
+		ret=$?
+		if [ $ret != 0 ]; then
+			diff -u $testroot/wt/$f.expected $testroot/wt/$f
+			test_done $testroot $ret
+			return 1
+		fi
+	done
+
+	test_done $testroot 0
+}
+
+test_patch_dont_apply() {
+	local testroot=`test_init patch_dont_apply`
+
+	got checkout $testroot/repo $testroot/wt > /dev/null
+	ret=$?
+	if [ $ret != 0 ]; then
+		test_done $testroot $ret
+		return 1
+	fi
+
+	cat <<EOF > $testroot/wt/patch
+--- alpha
++++ alpha
+@@ -1 +1,2 @@
++hatsuseno
+ alpha something
+EOF
+
+	echo -n > $testroot/stdout.expected
+	echo "got: patch doesn't apply" > $testroot/stderr.expected
+
+	(cd $testroot/wt && got patch patch) \
+		 > $testroot/stdout \
+		2> $testroot/stderr
+	ret=$?
+	if [ $ret == 0 ]; then # should fail
+		test_done $testroot 1
+		return 1
+	fi
+
+	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
+
+	cmp -s $testroot/stderr.expected $testroot/stderr
+	ret=$?
+	if [ $ret != 0 ]; then
+		diff -u $testroot/stderr.expected $testroot/stderr
+		test_done $testroot $ret
+		return 1
+	fi
+
+	test_done $testroot $ret
+}
+
+test_patch_malformed() {
+	local testroot=`test_init patch_malformed`
+
+	got checkout $testroot/repo $testroot/wt > /dev/null
+	ret=$?
+	if [ $ret != 0 ]; then
+		test_done $testroot $ret
+		return 1
+	fi
+
+	# missing "@@"
+	cat <<EOF > $testroot/wt/patch
+--- alpha
++++ alpha
+@@ -1 +1,2
++hatsuseno
+ alpha
+EOF
+
+	echo -n > $testroot/stdout.expected
+	echo "got: malformed patch" > $testroot/stderr.expected
+
+	(cd $testroot/wt && got patch patch) \
+		 > $testroot/stdout \
+		2> $testroot/stderr
+	ret=$?
+	if [ $ret == 0 ]; then
+		echo "got managed to apply an invalid patch"
+		test_done $testroot 1
+		return 1
+	fi
+
+	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
+
+	cmp -s $testroot/stderr.expected $testroot/stderr
+	ret=$?
+	if [ $ret != 0 ]; then
+		diff -u $testroot/stderr.expected $testroot/stderr
+		test_done $testroot $ret
+		return 1
+	fi
+
+	# wrong first character
+	cat <<EOF > $testroot/wt/patch
+--- alpha
++++ alpha
+@@ -1 +1,2 @@
++hatsuseno
+alpha
+EOF
+
+	(cd $testroot/wt && got patch patch) \
+		 > $testroot/stdout \
+		2> $testroot/stderr
+	ret=$?
+	if [ $ret == 0 ]; then
+		echo "got managed to apply an invalid patch"
+		test_done $testroot 1
+		return 1
+	fi
+
+	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
+
+	cmp -s $testroot/stderr.expected $testroot/stderr
+	ret=$?
+	if [ $ret != 0 ]; then
+		diff -u $testroot/stderr.expected $testroot/stderr
+		test_done $testroot $ret
+		return 1
+	fi
+
+	test_done $testroot $ret
+}
+
+test_patch_no_patch() {
+	local testroot=`test_init patch_no_patch`
+
+	got checkout $testroot/repo $testroot/wt > /dev/null
+	ret=$?
+	if [ $ret != 0 ]; then
+		test_done $testroot $ret
+		return 1
+	fi
+
+	cat <<EOF > $testroot/wt/patch
+hello world!
+...
+
+some other nonsense
+...
+
+there's no patch in here!
+EOF
+
+	echo -n > $testroot/stdout.expected
+	echo "got: no patch found" > $testroot/stderr.expected
+
+	(cd $testroot/wt && got patch patch) \
+		 > $testroot/stdout \
+		2> $testroot/stderr
+	ret=$?
+	if [ $ret == 0 ]; then # should fail
+		test_done $testroot 1
+		return 1
+	fi
+
+	
+	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
+
+	cmp -s $testroot/stderr.expected $testroot/stderr
+	ret=$?
+	if [ $ret != 0 ]; then
+		diff -u $testroot/stderr.expected $testroot/stderr
+		test_done $testroot $ret
+		return 1
+	fi
+
+	test_done $testroot $ret
+}
+
+test_patch_equals_for_context() {
+	local testroot=`test_init patch_prepend_line`
+
+	got checkout $testroot/repo $testroot/wt > /dev/null
+	ret=$?
+	if [ $ret != 0 ]; then
+		test_done $testroot $ret
+		return 1
+	fi
+
+	cat <<EOF > $testroot/wt/patch
+--- alpha
++++ alpha
+@@ -1 +1,2 @@
++hatsuseno
+=alpha
+EOF
+
+	echo "M  alpha" > $testroot/stdout.expected
+
+	(cd $testroot/wt && got patch patch) > $testroot/stdout
+	ret=$?
+	if [ $ret != 0 ]; then
+		test_done $testroot $ret
+		return 1
+	fi
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret != 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done $testroot $ret
+		return 1
+	fi
+
+	echo hatsuseno > $testroot/wt/alpha.expected
+	echo alpha    >> $testroot/wt/alpha.expected
+	cmp -s $testroot/wt/alpha.expected $testroot/wt/alpha
+	ret=$?
+	if [ $ret != 0 ]; then
+		diff -u $testroot/wt/alpha.expected $testroot/wt/alpha
+	fi
+	test_done $testroot $ret
+}
+
+test_parseargs "$@"
+run_test test_patch_simple_add_file
+run_test test_patch_simple_rm_file
+run_test test_patch_simple_edit_file
+run_test test_patch_prepend_line
+run_test test_patch_replace_line
+run_test test_patch_multiple_hunks
+run_test test_patch_multiple_files
+run_test test_patch_dont_apply
+run_test test_patch_malformed
+run_test test_patch_no_patch
+run_test test_patch_equals_for_context