"GOT", but the "O" is a cute, smiling pufferfish. Index | Thread | Search

From:
Omar Polo <op@omarpolo.com>
Subject:
add got patch subcommand
To:
gameoftrees@openbsd.org
Date:
Mon, 07 Mar 2022 21:27:28 +0100

Download raw body.

Thread
Hello,

some time ago stsp suggested that got should be able to apply diffs by
itself.  Using the VCS to apply them allows in fact some nice
comodities: for instance added and removed files are automatically
tracked.

The rather long diff attached is just that.  It's a simple and (i think)
straightforward implementation of patch(1) only integrated inside got.
There's a new got-read-patch libexec helpers (which borrows some lines
of code from Larry' patch, but it's mostly rewritten from scratch) that
is used to parse the content of the diff, similarly to the others
got-read-* helpers and under the same restrictions, and lib/patch.c that
wraps it.

There are quite a few missing parts yet, for example:

 * it doesn't handle yet the "\ No newline at end of file".
 * it doesn't create missing directories yet.
 * it doesn't try too hard to find a place where the patch is applicable
   (could be considered a feature.)
 * it doesn't support fuzzing (i.e. ignoring some line from the context)
   or ignoring whitespaces.

and other additions that I'd like to add, like a "-p" strip option, a
"-n" nop/check, or eventually handle multiple patch file.  The diff is
already long enough though, so these points can be sorted out in tree I
think.

stsp also kindly pointed me to the svn patch regression suite, which I
still have to go through completely.  More tests will follow.  Possibly
bug fixes too ;)

In the meantime, I'm happy that note that `got patch' can handle itself:

	% got checkout ~/git/got.git
	...
	% cd got
	% # it doesn't create missing directories (yet)
	% mkdir libexec/got-read-patch
	% got patch < /tmp/got-patch.diff
	M  got/Makefile
	M  got/got.1
	M  got/got.c
	M  include/got_error.h
	A  include/got_patch.h
	M  lib/got_lib_privsep.h
	A  lib/patch.c
	M  lib/privsep.c
	M  libexec/Makefile
	A  libexec/got-read-patch/Makefile
	A  libexec/got-read-patch/got-read-patch.c
	M  regress/cmdline/Makefile
	A  regress/cmdline/patch.sh


Cheers,

Omar Polo

diff refs/heads/main refs/heads/patch-second-try
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 + 97248718d40eaf3a712653112d62e64d0beca870
--- 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" },
@@ -7107,6 +7111,133 @@ done:
 }
 
 __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;
+}
+
+__dead static void
 usage_revert(void)
 {
 	fprintf(stderr, "usage: %s revert [-p] [-F response-script] [-R] "
@@ -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 + 8d62a59817a8465d55c8c4770a983504c28e76fa (mode 755)
--- /dev/null
+++ regress/cmdline/patch.sh
@@ -0,0 +1,638 @@
+#!/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
+		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