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

From:
Omar Polo <op@omarpolo.com>
Subject:
Re: got patch: merge patches with diff3
To:
Stefan Sperling <stsp@stsp.name>
Cc:
gameoftrees@openbsd.org
Date:
Fri, 17 Jun 2022 13:04:50 +0200

Download raw body.

Thread
Stefan Sperling <stsp@stsp.name> wrote:
> On Fri, Jun 17, 2022 at 11:48:31AM +0200, Omar Polo wrote:
> > Stefan Sperling <stsp@stsp.name> wrote:
> > > On Thu, Jun 16, 2022 at 12:35:31AM +0200, Omar Polo wrote:
> > > apply_patch() seems to treat the occurrence of a merge conflict as a
> > > fatal error. Why?
> > 
> > in fact, i don't want to treat them as fatal error.  The idea was to use
> > GOT_ERR_PATCH_CONFLICT to signal the caller (got_patch) that a conflict
> > was found, but not to stop the patch operation.  (like what happens for
> > a failed patch, so that in the end 'got patch' exits with non-zero)
> > 
> > I lost a change in got_patch when putting the diff into shape, thanks
> > for spotting this!
> 
> You could instead pass overlapcnt back to the caller.

thanks for the suggestion; it's neater this way, thanks!

here's the pile of patches updated.  I'm attaching also the previous
refactoring diff (that i've split into two).

regarding the merge i've:

 - fixed the case where the blob id is not found, we will silently skip
   it.  The parsing code is not bulletproof and may pick up noise as
   a blob id.  adding a test case for this too.

 - added back a missing check in the last fseeko

 - changed the merge labels to "--- path/to/file", "+++ path/to/file"
   and the blob id instead of "ancestor", as suggested in irc (thanks)

-----------------------------------------------
commit a2ac884fc64becea0771c8d0fe85c49a0c8c3580
from: Omar Polo <op@omarpolo.com>
date: Fri Jun 17 10:41:10 2022 UTC
 
 patch_file: move file ownership to parent
 
diff eea58cb9757387e01590325f3106af94fe69b570 dac899116d2c355fbd05e81b459fa78213c87bc5
blob - 7167f541b49d2fddbb5d61d45e13bed060e2d33e
blob + cb80bed14230259d21207905afb95b11f1e4063c
--- lib/patch.c
+++ lib/patch.c
@@ -455,14 +455,12 @@ apply_hunk(FILE *tmp, struct got_patch_hunk *h, int *l
 }
 
 static const struct got_error *
-patch_file(struct got_patch *p, const char *path, FILE *tmp, int nop,
-    mode_t *mode)
+patch_file(struct got_patch *p, FILE *orig, FILE *tmp, int nop, mode_t *mode)
 {
 	const struct got_error *err = NULL;
 	struct got_patch_hunk *h;
 	struct stat sb;
 	int lineno = 0;
-	FILE *orig;
 	off_t copypos, pos;
 	char *line = NULL;
 	size_t linesize = 0;
@@ -477,15 +475,8 @@ patch_file(struct got_patch *p, const char *path, FILE
 		return apply_hunk(tmp, h, &lineno);
 	}
 
-	if ((orig = fopen(path, "r")) == NULL) {
-		err = got_error_from_errno2("fopen", path);
-		goto done;
-	}
-
-	if (fstat(fileno(orig), &sb) == -1) {
-		err = got_error_from_errno("fstat");
-		goto done;
-	}
+	if (fstat(fileno(orig), &sb) == -1)
+		return got_error_from_errno("fstat");
 	*mode = sb.st_mode;
 
 	copypos = 0;
@@ -495,11 +486,11 @@ patch_file(struct got_patch *p, const char *path, FILE
 		if (err != NULL && err->code == GOT_ERR_HUNK_FAILED)
 			h->err = err;
 		if (err != NULL)
-			goto done;
+			return err;
 		if (!nop)
 			err = copy(tmp, orig, copypos, pos);
 		if (err != NULL)
-			goto done;
+			return err;
 		copypos = pos;
 
 		err = test_hunk(orig, h);
@@ -508,20 +499,16 @@ patch_file(struct got_patch *p, const char *path, FILE
 			 * try to apply the hunk again starting the search
 			 * after the previous partial match.
 			 */
-			if (fseeko(orig, pos, SEEK_SET) == -1) {
-				err = got_error_from_errno("fseeko");
-				goto done;
-			}
+			if (fseeko(orig, pos, SEEK_SET) == -1)
+				return got_error_from_errno("fseeko");
 			linelen = getline(&line, &linesize, orig);
-			if (linelen == -1) {
-				err = got_error_from_errno("getline");
-				goto done;
-			}
+			if (linelen == -1)
+				return got_error_from_errno("getline");
 			lineno++;
 			goto tryagain;
 		}
 		if (err != NULL)
-			goto done;
+			return err;
 
 		if (lineno + 1 != h->old_from)
 			h->offset = lineno + 1 - h->old_from;
@@ -529,13 +516,11 @@ patch_file(struct got_patch *p, const char *path, FILE
 		if (!nop)
 			err = apply_hunk(tmp, h, &lineno);
 		if (err != NULL)
-			goto done;
+			return err;
 
 		copypos = ftello(orig);
-		if (copypos == -1) {
-			err = got_error_from_errno("ftello");
-			goto done;
-		}
+		if (copypos == -1)
+			return got_error_from_errno("ftello");
 	}
 
 	if (p->new == NULL && sb.st_size != copypos) {
@@ -545,9 +530,6 @@ patch_file(struct got_patch *p, const char *path, FILE
 	} else if (!nop && !feof(orig))
 		err = copy(tmp, orig, copypos, -1);
 
-done:
-	if (orig != NULL && fclose(orig) == EOF && err == NULL)
-		err = got_error_from_errno("fclose");
 	return err;
 }
 
@@ -600,7 +582,7 @@ apply_patch(struct got_worktree *worktree, struct got_
 	int file_renamed = 0;
 	char *oldpath = NULL, *newpath = NULL;
 	char *tmppath = NULL, *template = NULL, *parent = NULL;
-	FILE *tmp = NULL;
+	FILE *oldfile = NULL, *tmp = NULL;
 	mode_t mode = GOT_DEFAULT_FILE_MODE;
 
 	if (asprintf(&oldpath, "%s/%s", got_worktree_get_root_path(worktree),
@@ -623,11 +605,16 @@ apply_patch(struct got_worktree *worktree, struct got_
 		goto done;
 	}
 
+	if (p->old != NULL && (oldfile = fopen(oldpath, "r")) == NULL) {
+		err = got_error_from_errno2("open", oldpath);
+		goto done;
+	}
+
 	if (!nop)
 		err = got_opentemp_named(&tmppath, &tmp, template);
 	if (err)
 		goto done;
-	err = patch_file(p, oldpath, tmp, nop, &mode);
+	err = patch_file(p, oldfile, tmp, nop, &mode);
 	if (err)
 		goto done;
 
@@ -691,6 +678,8 @@ done:
 		err = got_error_from_errno("fclose");
 	free(tmppath);
 	free(oldpath);
+	if (oldfile != NULL && fclose(oldfile) == EOF && err == NULL)
+		err = got_error_from_errno("fclose");
 	free(newpath);
 	return err;
 }

-----------------------------------------------
commit 752735af2da45fc2ecf3474decd6f71ade942181
from: Omar Polo <op@omarpolo.com>
date: Fri Jun 17 10:55:03 2022 UTC
 
 patch_file: fill tmp with the patched file even in nop mode
 
 future work with diff3 merge will need the fully patched file even in
 the nop mode
 
diff dac899116d2c355fbd05e81b459fa78213c87bc5 0708493b774308f86e86f52afba802cae311ecdc
blob - cb80bed14230259d21207905afb95b11f1e4063c
blob + 0a98afc1b334061cef12aac2a80ebe56c7b4a167
--- lib/patch.c
+++ lib/patch.c
@@ -455,7 +455,7 @@ apply_hunk(FILE *tmp, struct got_patch_hunk *h, int *l
 }
 
 static const struct got_error *
-patch_file(struct got_patch *p, FILE *orig, FILE *tmp, int nop, mode_t *mode)
+patch_file(struct got_patch *p, FILE *orig, FILE *tmp, mode_t *mode)
 {
 	const struct got_error *err = NULL;
 	struct got_patch_hunk *h;
@@ -470,8 +470,6 @@ patch_file(struct got_patch *p, FILE *orig, FILE *tmp,
 		h = STAILQ_FIRST(&p->head);
 		if (h == NULL || STAILQ_NEXT(h, entries) != NULL)
 			return got_error(GOT_ERR_PATCH_MALFORMED);
-		if (nop)
-			return NULL;
 		return apply_hunk(tmp, h, &lineno);
 	}
 
@@ -487,8 +485,7 @@ patch_file(struct got_patch *p, FILE *orig, FILE *tmp,
 			h->err = err;
 		if (err != NULL)
 			return err;
-		if (!nop)
-			err = copy(tmp, orig, copypos, pos);
+		err = copy(tmp, orig, copypos, pos);
 		if (err != NULL)
 			return err;
 		copypos = pos;
@@ -513,8 +510,7 @@ patch_file(struct got_patch *p, FILE *orig, FILE *tmp,
 		if (lineno + 1 != h->old_from)
 			h->offset = lineno + 1 - h->old_from;
 
-		if (!nop)
-			err = apply_hunk(tmp, h, &lineno);
+		err = apply_hunk(tmp, h, &lineno);
 		if (err != NULL)
 			return err;
 
@@ -527,7 +523,7 @@ patch_file(struct got_patch *p, FILE *orig, FILE *tmp,
 		h = STAILQ_FIRST(&p->head);
 		h->err = got_error(GOT_ERR_HUNK_FAILED);
 		err = h->err;
-	} else if (!nop && !feof(orig))
+	} else if (!feof(orig))
 		err = copy(tmp, orig, copypos, -1);
 
 	return err;
@@ -610,11 +606,10 @@ apply_patch(struct got_worktree *worktree, struct got_
 		goto done;
 	}
 
-	if (!nop)
-		err = got_opentemp_named(&tmppath, &tmp, template);
+	err = got_opentemp_named(&tmppath, &tmp, template);
 	if (err)
 		goto done;
-	err = patch_file(p, oldfile, tmp, nop, &mode);
+	err = patch_file(p, oldfile, tmp, &mode);
 	if (err)
 		goto done;
 

-----------------------------------------------
commit c3c0a28be901f5ecdbaba66d6b24ee6c04342ec8 (pmerge)
from: Omar Polo <op@omarpolo.com>
date: Fri Jun 17 10:55:03 2022 UTC
 
 got patch: use diff3 to merge the changes
 
 Parse the "blob -" metadata in diffs produced by 'got diff' so that
 later it's possible to patch the original file and merging it to the
 current version.
 
 This automatically solves many failure to apply a patch or at least
 turns them into a conflict.
 
diff 0708493b774308f86e86f52afba802cae311ecdc 2e60647c81f1c14b2fab8c027c04d12e1f1d35eb
blob - af9557d85c3c38603367d54310b5f2a59d7b3b11
blob + 8b9f4c059435abf6b351d2c9f180dd9926969528
--- lib/got_lib_privsep.h
+++ lib/got_lib_privsep.h
@@ -613,6 +613,7 @@ struct got_imsg_patch {
 	int	git;
 	char	old[PATH_MAX];
 	char	new[PATH_MAX];
+	char	blob[41];
 };
 
 /*
blob - 0a98afc1b334061cef12aac2a80ebe56c7b4a167
blob + 2f88cf5f2ee99332023bf8a005e3023ba04816cd
--- lib/patch.c
+++ lib/patch.c
@@ -42,10 +42,12 @@
 #include "got_reference.h"
 #include "got_cancel.h"
 #include "got_worktree.h"
+#include "got_repository.h"
 #include "got_opentemp.h"
 #include "got_patch.h"
 
 #include "got_lib_delta.h"
+#include "got_lib_diff.h"
 #include "got_lib_object.h"
 #include "got_lib_privsep.h"
 
@@ -70,6 +72,7 @@ STAILQ_HEAD(got_patch_hunk_head, got_patch_hunk);
 struct got_patch {
 	char	*old;
 	char	*new;
+	char	 blob[41];
 	struct got_patch_hunk_head head;
 };
 
@@ -180,11 +183,14 @@ recv_patch(struct imsgbuf *ibuf, int *done, struct got
 	memcpy(&patch, imsg.data, sizeof(patch));
 
 	if (patch.old[sizeof(patch.old)-1] != '\0' ||
-	    patch.new[sizeof(patch.new)-1] != '\0') {
+	    patch.new[sizeof(patch.new)-1] != '\0' ||
+	    patch.blob[sizeof(patch.blob)-1] != '\0') {
 		err = got_error(GOT_ERR_PRIVSEP_LEN);
 		goto done;
 	}
 
+	strlcpy(p->blob, patch.blob, sizeof(p->blob));
+
 	/* automatically set strip=1 for git-style diffs */
 	if (strip == -1 && patch.git &&
 	    (*patch.old == '\0' || !strncmp(patch.old, "a/", 2)) &&
@@ -569,18 +575,80 @@ patch_add(void *arg, unsigned char status, const char 
 }
 
 static const struct got_error *
-apply_patch(struct got_worktree *worktree, struct got_repository *repo,
-    struct got_fileindex *fileindex, const char *old, const char *new,
-    struct got_patch *p, int nop, struct patch_args *pa,
-    got_cancel_cb cancel_cb, void *cancel_arg)
+open_blob(char **path, FILE **fp, const char *blobid,
+    struct got_repository *repo)
 {
 	const struct got_error *err = NULL;
-	int file_renamed = 0;
+	struct got_object_id *id = NULL;
+	struct got_blob_object *blob = NULL;
+
+	*fp = NULL;
+	*path = NULL;
+
+	err = got_repo_match_object_id(&id, NULL, blobid,
+	    GOT_OBJ_TYPE_BLOB, NULL /* do not resolve tags */,
+	    repo);
+	if (err)
+		return err;
+
+	err = got_object_open_as_blob(&blob, repo, id, 8192);
+	if (err)
+		goto done;
+
+	err = got_opentemp_named(path, fp, GOT_TMPDIR_STR "/got-patch-blob");
+	if (err)
+		goto done;
+
+	err = got_object_blob_dump_to_file(NULL, NULL, NULL, *fp, blob);
+	if (err)
+		goto done;
+
+done:
+	if (id)
+		free(id);
+	if (blob)
+		got_object_blob_close(blob);
+	if (err) {
+		if (*fp != NULL)
+			fclose(*fp);
+		if (*path != NULL)
+			unlink(*path);
+		free(*path);
+		*fp = NULL;
+		*path = NULL;
+	}
+	return err;
+}
+
+static const struct got_error *
+apply_patch(int *overlapcnt, struct got_worktree *worktree,
+    struct got_repository *repo, struct got_fileindex *fileindex,
+    const char *old, const char *new, struct got_patch *p, int nop,
+    struct patch_args *pa, got_cancel_cb cancel_cb, void *cancel_arg)
+{
+	const struct got_error *err = NULL;
+	int do_merge = 0, file_renamed = 0;
+	char *oldlabel = NULL, *newlabel = NULL;
 	char *oldpath = NULL, *newpath = NULL;
 	char *tmppath = NULL, *template = NULL, *parent = NULL;
-	FILE *oldfile = NULL, *tmp = NULL;
+	char *apath = NULL, *mergepath = NULL;
+	FILE *oldfile = NULL, *tmpfile = NULL, *afile = NULL, *mergefile = NULL;
+	int outfd;
+	const char *outpath;
 	mode_t mode = GOT_DEFAULT_FILE_MODE;
 
+	*overlapcnt = 0;
+
+	/* don't run the diff3 merge on creations/deletions */
+	if (*p->blob != '\0' && p->old != NULL && p->new != NULL) {
+		err = open_blob(&apath, &afile, p->blob, repo);
+		if (err && err->code != GOT_ERR_NOT_REF)
+			return err;
+		else if (err == NULL)
+			do_merge = 1;
+		err = NULL;
+	}
+
 	if (asprintf(&oldpath, "%s/%s", got_worktree_get_root_path(worktree),
 	    old) == -1) {
 		err = got_error_from_errno("asprintf");
@@ -606,13 +674,48 @@ apply_patch(struct got_worktree *worktree, struct got_
 		goto done;
 	}
 
-	err = got_opentemp_named(&tmppath, &tmp, template);
+	err = got_opentemp_named(&tmppath, &tmpfile, template);
 	if (err)
 		goto done;
-	err = patch_file(p, oldfile, tmp, &mode);
+	outpath = tmppath;
+	outfd = fileno(tmpfile);
+	err = patch_file(p, afile != NULL ? afile : oldfile, tmpfile, &mode);
 	if (err)
 		goto done;
 
+	if (do_merge) {
+		if (fseeko(afile, 0, SEEK_SET) == -1 ||
+		    fseeko(oldfile, 0, SEEK_SET) == -1 ||
+		    fseeko(tmpfile, 0, SEEK_SET) == -1) {
+			err = got_error_from_errno("fseeko");
+			goto done;
+		}
+
+		if (asprintf(&oldlabel, "--- %s", p->old) == -1) {
+			err = got_error_from_errno("asprintf");
+			oldlabel = NULL;
+			goto done;
+		}
+
+		if (asprintf(&newlabel, "+++ %s", p->new) == -1) {
+			err = got_error_from_errno("asprintf");
+			newlabel = NULL;
+			goto done;
+		}
+
+		err = got_opentemp_named(&mergepath, &mergefile, template);
+		if (err)
+			goto done;
+		outpath = mergepath;
+		outfd = fileno(mergefile);
+
+		err = got_merge_diff3(overlapcnt, outfd, tmpfile, afile,
+		    oldfile, oldlabel, p->blob, newlabel,
+		    GOT_DIFF_ALGORITHM_PATIENCE);
+		if (err)
+			goto done;
+	}
+
 	if (nop)
 		goto done;
 
@@ -622,14 +725,14 @@ apply_patch(struct got_worktree *worktree, struct got_
 		goto done;
 	}
 
-	if (fchmod(fileno(tmp), mode) == -1) {
+	if (fchmod(outfd, mode) == -1) {
 		err = got_error_from_errno2("chmod", tmppath);
 		goto done;
 	}
 
-	if (rename(tmppath, newpath) == -1) {
+	if (rename(outpath, newpath) == -1) {
 		if (errno != ENOENT) {
-			err = got_error_from_errno3("rename", tmppath,
+			err = got_error_from_errno3("rename", outpath,
 			    newpath);
 			goto done;
 		}
@@ -640,8 +743,8 @@ apply_patch(struct got_worktree *worktree, struct got_
 		err = got_path_mkdir(parent);
 		if (err != NULL)
 			goto done;
-		if (rename(tmppath, newpath) == -1) {
-			err = got_error_from_errno3("rename", tmppath,
+		if (rename(outpath, newpath) == -1) {
+			err = got_error_from_errno3("rename", outpath,
 			    newpath);
 			goto done;
 		}
@@ -661,21 +764,40 @@ apply_patch(struct got_worktree *worktree, struct got_
 		    fileindex, patch_add, pa);
 		if (err)
 			unlink(newpath);
-	} else
+	} else if (*overlapcnt > 0)
+		err = report_progress(pa, old, new, GOT_STATUS_CONFLICT, NULL);
+	else
 		err = report_progress(pa, old, new, GOT_STATUS_MODIFY, NULL);
 
 done:
 	free(parent);
 	free(template);
+
 	if (tmppath != NULL)
 		unlink(tmppath);
-	if (tmp != NULL && fclose(tmp) == EOF && err == NULL)
+	if (tmpfile != NULL && fclose(tmpfile) == EOF && err == NULL)
 		err = got_error_from_errno("fclose");
 	free(tmppath);
+
 	free(oldpath);
 	if (oldfile != NULL && fclose(oldfile) == EOF && err == NULL)
 		err = got_error_from_errno("fclose");
+
+	if (apath != NULL)
+		unlink(apath);
+	if (afile != NULL && fclose(afile) == EOF && err == NULL)
+		err = got_error_from_errno("fclose");
+	free(apath);
+
+	if (mergepath != NULL)
+		unlink(mergepath);
+	if (mergefile != NULL && fclose(mergefile) == EOF && err == NULL)
+		err = got_error_from_errno("fclose");
+	free(mergepath);
+
 	free(newpath);
+	free(oldlabel);
+	free(newlabel);
 	return err;
 }
 
@@ -719,7 +841,7 @@ got_patch(int fd, struct got_worktree *worktree, struc
 	char *oldpath, *newpath;
 	struct imsgbuf *ibuf;
 	int imsg_fds[2] = {-1, -1};
-	int done = 0, failed = 0;
+	int overlapcnt, done = 0, failed = 0;
 	pid_t pid;
 
 	ibuf = calloc(1, sizeof(*ibuf));
@@ -778,8 +900,9 @@ got_patch(int fd, struct got_worktree *worktree, struc
 		err = got_worktree_patch_check_path(p.old, p.new, &oldpath,
 		    &newpath, worktree, repo, fileindex);
 		if (err == NULL)
-			err = apply_patch(worktree, repo, fileindex, oldpath,
-			    newpath, &p, nop, &pa, cancel_cb, cancel_arg);
+			err = apply_patch(&overlapcnt, worktree, repo,
+			    fileindex, oldpath, newpath, &p, nop, &pa,
+			    cancel_cb, cancel_arg);
 		if (err != NULL) {
 			failed = 1;
 			/* recoverable errors */
@@ -791,6 +914,8 @@ got_patch(int fd, struct got_worktree *worktree, struc
 				err = report_progress(&pa, p.old, p.new,
 				    GOT_STATUS_CANNOT_UPDATE, NULL);
 		}
+		if (overlapcnt != 0)
+			failed = 1;
 
 		free(oldpath);
 		free(newpath);
blob - 23212abe02328d5837822225fecbf174e677f2bb
blob + 3ace8a308d014810ccbc1758463bc197cfee8657
--- libexec/got-read-patch/got-read-patch.c
+++ libexec/got-read-patch/got-read-patch.c
@@ -60,7 +60,7 @@
 struct imsgbuf ibuf;
 
 static const struct got_error *
-send_patch(const char *oldname, const char *newname, int git)
+send_patch(const char *oldname, const char *newname, const char *blob, int git)
 {
 	struct got_imsg_patch p;
 
@@ -72,9 +72,11 @@ send_patch(const char *oldname, const char *newname, i
 	if (newname != NULL)
 		strlcpy(p.new, newname, sizeof(p.new));
 
+	if (blob != NULL)
+		strlcpy(p.blob, blob, sizeof(p.blob));
+
 	p.git = git;
-	if (imsg_compose(&ibuf, GOT_IMSG_PATCH, 0, 0, -1,
-	    &p, sizeof(p)) == -1)
+	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;
 }
@@ -127,6 +129,7 @@ find_patch(int *done, FILE *fp)
 {
 	const struct got_error *err = NULL;
 	char	*old = NULL, *new = NULL;
+	char	*blob = NULL;
 	char	*line = NULL;
 	size_t	 linesize = 0;
 	ssize_t	 linelen;
@@ -147,13 +150,19 @@ find_patch(int *done, FILE *fp)
 		} else if (!strncmp(line, "+++ ", 4)) {
 			free(new);
 			err = filename(line+4, &new);
+		} else if (!git && !strncmp(line, "blob - ", 7)) {
+			free(blob);
+			err = filename(line + 7, &blob);
 		} else if (rename && !strncmp(line, "rename to ", 10)) {
 			free(new);
 			err = filename(line + 10, &new);
 		} else if (git && !strncmp(line, "similarity index 100%", 21))
 			rename = 1;
-		else if (!strncmp(line, "diff --git a/", 13))
+		else if (!strncmp(line, "diff --git a/", 13)) {
 			git = 1;
+			free(blob);
+			blob = NULL;
+		}
 
 		if (err)
 			break;
@@ -165,7 +174,7 @@ find_patch(int *done, FILE *fp)
 		 */
 		if (rename && old != NULL && new != NULL) {
 			*done = 1;
-			err = send_patch(old, new, git);
+			err = send_patch(old, new, blob, git);
 			break;
 		}
 
@@ -175,7 +184,7 @@ find_patch(int *done, FILE *fp)
 			    (!create && old == NULL))
 				err = got_error(GOT_ERR_PATCH_MALFORMED);
 			else
-				err = send_patch(old, new, git);
+				err = send_patch(old, new, blob, git);
 
 			if (err)
 				break;
@@ -189,6 +198,7 @@ find_patch(int *done, FILE *fp)
 
 	free(old);
 	free(new);
+	free(blob);
 	free(line);
 	if (ferror(fp) && err == NULL)
 		err = got_error_from_errno("getline");
blob - c1e703a5e36a9140b460df532dc84b36583e5884
blob + defce1a79fc5cd6a7677f7c6021e2062b7d135ea
--- regress/cmdline/patch.sh
+++ regress/cmdline/patch.sh
@@ -1443,6 +1443,177 @@ EOF
 	test_done $testroot $ret
 }
 
+test_patch_merge_simple() {
+	local testroot=`test_init patch_merge_simple`
+
+	got checkout $testroot/repo $testroot/wt > /dev/null
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		test_done $testroot $ret
+		return 1
+	fi
+
+	jot 10 > $testroot/wt/numbers
+	(cd $testroot/wt && got add numbers && got commit -m +numbers) \
+		> /dev/null
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		test_done $testroot $ret
+		return 1
+	fi
+
+	jot 10 | sed 's/4/four/g' > $testroot/wt/numbers
+
+	(cd $testroot/wt && got diff > $testroot/old.diff \
+		&& got revert numbers) >/dev/null
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		test_done $testroot $ret
+		return 1
+	fi
+
+	jot 10 | sed 's/6/six/g' > $testroot/wt/numbers
+	(cd $testroot/wt && got commit -m 'edit numbers') \
+		> /dev/null
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		test_done $testroot $ret
+		return 1
+	fi
+
+	(cd $testroot/wt && got patch $testroot/old.diff) \
+		2>&1 > /dev/null
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		test_done $testroot $ret
+		return 1
+	fi
+
+	jot 10 | sed -e s/4/four/ -e s/6/six/ > $testroot/wt/numbers.expected
+	cmp -s $testroot/wt/numbers $testroot/wt/numbers.expected
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/wt/numbers $testroot/wt/numbers.expected
+	fi
+	test_done $testroot $ret
+}
+
+test_patch_merge_conflict() {
+	local testroot=`test_init patch_merge_conflict`
+
+	got checkout $testroot/repo $testroot/wt > /dev/null
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		test_done $testroot $ret
+		return 1
+	fi
+
+	jot 10 > $testroot/wt/numbers
+	(cd $testroot/wt && got add numbers && got commit -m +numbers) \
+		> /dev/null
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		test_done $testroot $ret
+		return 1
+	fi
+
+	jot 10 | sed 's/6/six/g' > $testroot/wt/numbers
+
+	(cd $testroot/wt && got diff > $testroot/old.diff \
+		&& got revert numbers) >/dev/null
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		test_done $testroot $ret
+		return 1
+	fi
+
+	jot 10 | sed 's/6/3+3/g' > $testroot/wt/numbers
+	(cd $testroot/wt && got commit -m 'edit numbers') \
+		> /dev/null
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		test_done $testroot $ret
+		return 1
+	fi
+
+	(cd $testroot/wt && got patch $testroot/old.diff) \
+		>/dev/null 2>&1
+	ret=$?
+	if [ $ret -eq 0 ]; then
+		echo "got patch merged a diff that should conflict" >&2
+		test_done $testroot 0
+		return 1
+	fi
+
+	# XXX: prefixing every line with a tab otherwise got thinks
+	# the file has conflicts in it.
+	cat <<-EOF > $testroot/wt/numbers.expected
+	1
+	2
+	3
+	4
+	5
+	<<<<<<< --- numbers
+	six
+	||||||| f00c965d8307308469e537302baa73048488f162
+	6
+	=======
+	3+3
+	>>>>>>> +++ numbers
+	7
+	8
+	9
+	10
+EOF
+
+	cmp -s $testroot/wt/numbers $testroot/wt/numbers.expected
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/wt/numbers $testroot/wt/numbers.expected
+	fi
+	test_done $testroot $ret
+}
+
+test_patch_merge_unknown_blob() {
+	local testroot=`test_init patch_merge_unknown_blob`
+
+	got checkout $testroot/repo $testroot/wt > /dev/null
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		test_done $testroot $ret
+		return 1
+	fi
+
+	cat <<EOF > $testroot/wt/patch
+I've got a
+blob - aaaabbbbccccddddeeeeffffgggghhhhiiiillll
+and also a
+blob + 0000111122223333444455556666888899990000
+for this dummy diff
+--- alpha
++++ alpha
+@@ -1 +1 @@
+-alpha
++ALPHA
+will it work?
+EOF
+
+	(cd $testroot/wt/ && got patch patch) > $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		test_done $testroot $ret
+		return 1
+	fi
+
+	echo 'M  alpha' > $testroot/stdout.expected
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+	fi
+	test_done $testroot $ret
+}
+
 test_parseargs "$@"
 run_test test_patch_simple_add_file
 run_test test_patch_simple_rm_file
@@ -1468,3 +1639,6 @@ run_test test_patch_relative_paths
 run_test test_patch_with_path_prefix
 run_test test_patch_relpath_with_path_prefix
 run_test test_patch_reverse
+run_test test_patch_merge_simple
+run_test test_patch_merge_conflict
+run_test test_patch_merge_unknown_blob