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

From:
Omar Polo <op@omarpolo.com>
Subject:
Re: got patch: add flag to ignore whitespace?
To:
Omar Polo <op@omarpolo.com>
Cc:
gameoftrees@openbsd.org
Date:
Sat, 02 Jul 2022 17:27:30 +0200

Download raw body.

Thread
Omar Polo <op@omarpolo.com> wrote:
> The last patch from Mark reminded me that I thought some time ago to add
> a "-w" flag for got patch to ignore white spaces.  It's conceptually
> similar to patch(1) -l/--ignore-whitespace or git-apply(1)
> --ignore-whitespace.
> 
> Here's a possible usage (using Mark' last diff)
> 
> 	% mshow | got patch
> 	#  tog/tog.1
> 	@@ -295,7 +305,7 @@ hunk failed to apply
> 	G  tog/tog.c
> 	got: patch failed to apply
> 	% # woops, try again with -w
> 	% mshow | got patch -w
> 	G  tog/tog.1
> 	G  tog/tog.c
> 	% # yay!
> 
> The diff does exactly what one would expect.  The line comparisons are
> now done in linecmp, which first runs strcmp(3) and optionally falls
> back to a whitespace-loose matching.
> 
> There's a bit of churn in apply_hunk now.  Before, I was re-using the
> lines saved in the hunk to apply it, now I can't.  If the context lines
> of the hunk may have the whitespaces mangled and so I have to re-read
> the lines from the original file.  Even if we're using a loose match,
> the context line needs not to be changed.

Wops, it introduced a stupid error in apply_hunk: when adding a file we
wouldn't add \n at the end of the lines.  The regress suite didn't catch
this, I'll add a testcase for it later as it's independent from this
diff

The change with the previous is to strip newlines read in apply_hunk and
always add them when printing to the temp file.

diff refs/heads/main refs/heads/pw
commit - acf749fc600a43d8e276321e8a63cd97484f30bb
commit + 9fc8d3de91bd9dbf0aaa7cac05fcffec04d4c558
blob - d2db3f11b55b0dcd7008e9f4662210887aa2742b
blob + ac432bba43e6aa8dc04e5981fa8f99a8c9ae9e39
--- got/got.1
+++ got/got.1
@@ -1307,7 +1307,7 @@ option)
 .El
 .El
 .Tg pa
-.It Cm patch Oo Fl n Oc Oo Fl p Ar strip-count Oc Oo Fl R Oc Op Ar patchfile
+.It Cm patch Oo Fl n Oc Oo Fl p Ar strip-count Oc Oo Fl R Oc Oo Fl w Oc Op Ar patchfile
 .Dl Pq alias: Cm pa
 Apply changes from
 .Ar patchfile
@@ -1393,6 +1393,11 @@ path prefixes generated by
 will be recognized and stripped automatically.
 .It Fl R
 Reverse the patch before applying it.
+.It Fl w
+Ignore whitespaces in context lines.
+Useful in case the tabs and spaces in the
+.Ar patchfile
+have been mangled.
 .El
 .Tg rv
 .It Cm revert Oo Fl p Oc Oo Fl F Ar response-script Oc Oo Fl R Oc Ar path ...
blob - 5f7f00007937a048564e685aac0a8c6b9e98adab
blob + 62afd6dac8942d13f30de9135d7d8dcba4e79a9e
--- got/got.c
+++ got/got.c
@@ -7702,7 +7702,7 @@ __dead static void
 usage_patch(void)
 {
 	fprintf(stderr, "usage: %s patch [-n] [-p strip-count] "
-	    "[-R] [patchfile]\n", getprogname());
+	    "[-R] [-w] [patchfile]\n", getprogname());
 	exit(1);
 }
 
@@ -7791,11 +7791,11 @@ cmd_patch(int argc, char *argv[])
 	struct got_repository *repo = NULL;
 	const char *errstr;
 	char *cwd = NULL;
-	int ch, nop = 0, strip = -1, reverse = 0;
+	int ch, nop = 0, strip = -1, reverse = 0, ws = 0;
 	int patchfd;
 	int *pack_fds = NULL;
 
-	while ((ch = getopt(argc, argv, "np:R")) != -1) {
+	while ((ch = getopt(argc, argv, "np:Rw")) != -1) {
 		switch (ch) {
 		case 'n':
 			nop = 1;
@@ -7809,6 +7809,9 @@ cmd_patch(int argc, char *argv[])
 		case 'R':
 			reverse = 1;
 			break;
+		case 'w':
+			ws = 1;
+			break;
 		default:
 			usage_patch();
 			/* NOTREACHED */
@@ -7860,7 +7863,7 @@ cmd_patch(int argc, char *argv[])
 		err(1, "pledge");
 #endif
 
-	error = got_patch(patchfd, worktree, repo, nop, strip, reverse,
+	error = got_patch(patchfd, worktree, repo, nop, strip, reverse, ws,
 	    &patch_progress, NULL, check_cancelled, NULL);
 
 done:
blob - bc2e95bb4fa535617bc28f76935a78e36dea8a3e
blob + b91edd6f33b6aa71491a276c624ccb7a933573c6
--- include/got_patch.h
+++ include/got_patch.h
@@ -32,4 +32,4 @@ typedef const struct got_error *(*got_patch_progress_c
  */
 const struct got_error *
 got_patch(int, struct got_worktree *, struct got_repository *, int, int,
-    int, got_patch_progress_cb, void *, got_cancel_cb, void *);
+    int, int, got_patch_progress_cb, void *, got_cancel_cb, void *);
blob - 6b6915f9998d30a1ae5b1846365b299ba2c974fe
blob + eaa49470db5ec9942152cefaba3e0ff5ed22aeb0
--- lib/patch.c
+++ lib/patch.c
@@ -26,6 +26,7 @@
 #include <sys/stat.h>
 #include <sys/uio.h>
 
+#include <ctype.h>
 #include <errno.h>
 #include <limits.h>
 #include <sha1.h>
@@ -401,8 +402,30 @@ locate_hunk(FILE *orig, struct got_patch_hunk *h, off_
 	return err;
 }
 
+static int
+linecmp(const char *a, const char *b, int ws)
+{
+	int c;
+
+	c = strcmp(a, b);
+	if (c == 0 || !ws)
+		return c;
+
+	for (;;) {
+		while (isspace(*a))
+			a++;
+		while (isspace(*b))
+			b++;
+		if (*a == '\0' || *b == '\0' || *a != *b)
+			break;
+		a++, b++;
+	}
+
+	return *a - *b;
+}
+
 static const struct got_error *
-test_hunk(FILE *orig, struct got_patch_hunk *h)
+test_hunk(FILE *orig, struct got_patch_hunk *h, int ws)
 {
 	const struct got_error *err = NULL;
 	char *line = NULL;
@@ -426,7 +449,7 @@ test_hunk(FILE *orig, struct got_patch_hunk *h)
 			}
 			if (line[linelen - 1] == '\n')
 				line[linelen - 1] = '\0';
-			if (strcmp(h->lines[i] + 1, line)) {
+			if (linecmp(h->lines[i] + 1, line, ws)) {
 				err = got_error(GOT_ERR_HUNK_FAILED);
 				goto done;
 			}
@@ -440,36 +463,65 @@ done:
 }
 
 static const struct got_error *
-apply_hunk(FILE *tmp, struct got_patch_hunk *h, int *lineno)
+apply_hunk(FILE *orig, FILE *tmp, struct got_patch_hunk *h, int *lineno,
+    off_t from)
 {
-	size_t i, new = 0;
+	const struct got_error *err = NULL;
+	const char *t;
+	size_t linesize = 0, i, new = 0;
+	char *line = NULL;
+	char mode;
+	ssize_t linelen;
 
+	if (orig != NULL && fseeko(orig, from, SEEK_SET) == -1)
+		return got_error_from_errno("fseeko");
+
 	for (i = 0; i < h->len; ++i) {
-		switch (*h->lines[i]) {
-		case ' ':
-			if (fprintf(tmp, "%s\n", h->lines[i] + 1) < 0)
-				return got_error_from_errno("fprintf");
-			/* fallthrough */
+		switch (mode = *h->lines[i]) {
 		case '-':
+		case ' ':
 			(*lineno)++;
+			if (orig != NULL) {
+				linelen = getline(&line, &linesize, orig);
+				if (linelen == -1) {
+					err = got_error_from_errno("getline");
+					goto done;
+				}
+				if (line[linelen - 1] == '\n')
+					line[linelen - 1] = '\0';
+				t = line;
+			} else
+				t = h->lines[i] + 1;
+			if (mode == '-')
+				continue;
+			if (fprintf(tmp, "%s\n", t) < 0) {
+				err = got_error_from_errno("fprintf");
+				goto done;
+			}
 			break;
 		case '+':
 			new++;
-			if (fprintf(tmp, "%s", h->lines[i] + 1) < 0)
-				return got_error_from_errno("fprintf");
+			if (fprintf(tmp, "%s", h->lines[i] + 1) < 0) {
+				err = got_error_from_errno("fprintf");
+				goto done;
+			}
 			if (new != h->new_lines || !h->new_nonl) {
-				if (fprintf(tmp, "\n") < 0)
-					return got_error_from_errno(
-					    "fprintf");
+				if (fprintf(tmp, "\n") < 0) {
+					err = got_error_from_errno("fprintf");
+					goto done;
+				}
 			}
 			break;
 		}
 	}
-	return NULL;
+
+done:
+	free(line);
+	return err;
 }
 
 static const struct got_error *
-patch_file(struct got_patch *p, FILE *orig, FILE *tmp, mode_t *mode)
+patch_file(struct got_patch *p, FILE *orig, FILE *tmp, mode_t *mode, int ws)
 {
 	const struct got_error *err = NULL;
 	struct got_patch_hunk *h;
@@ -484,7 +536,7 @@ 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);
-		return apply_hunk(tmp, h, &lineno);
+		return apply_hunk(orig, tmp, h, &lineno, 0);
 	}
 
 	if (fstat(fileno(orig), &sb) == -1)
@@ -504,7 +556,7 @@ patch_file(struct got_patch *p, FILE *orig, FILE *tmp,
 			return err;
 		copypos = pos;
 
-		err = test_hunk(orig, h);
+		err = test_hunk(orig, h, ws);
 		if (err != NULL && err->code == GOT_ERR_HUNK_FAILED) {
 			/*
 			 * try to apply the hunk again starting the search
@@ -524,7 +576,7 @@ patch_file(struct got_patch *p, FILE *orig, FILE *tmp,
 		if (lineno + 1 != h->old_from)
 			h->offset = lineno + 1 - h->old_from;
 
-		err = apply_hunk(tmp, h, &lineno);
+		err = apply_hunk(orig, tmp, h, &lineno, pos);
 		if (err != NULL)
 			return err;
 
@@ -647,7 +699,7 @@ done:
 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,
+    const char *old, const char *new, struct got_patch *p, int nop, int ws,
     struct patch_args *pa, got_cancel_cb cancel_cb, void *cancel_arg)
 {
 	const struct got_error *err = NULL;
@@ -708,7 +760,8 @@ apply_patch(int *overlapcnt, struct got_worktree *work
 		goto done;
 	outpath = tmppath;
 	outfd = fileno(tmpfile);
-	err = patch_file(p, afile != NULL ? afile : oldfile, tmpfile, &mode);
+	err = patch_file(p, afile != NULL ? afile : oldfile, tmpfile,
+	    &mode, ws);
 	if (err)
 		goto done;
 
@@ -880,7 +933,7 @@ reverse_patch(struct got_patch *p)
 
 const struct got_error *
 got_patch(int fd, struct got_worktree *worktree, struct got_repository *repo,
-    int nop, int strip, int reverse, got_patch_progress_cb progress_cb,
+    int nop, int strip, int reverse, int ws, got_patch_progress_cb progress_cb,
     void *progress_arg, got_cancel_cb cancel_cb, void *cancel_arg)
 {
 	const struct got_error *err = NULL, *complete_err = NULL;
@@ -949,7 +1002,7 @@ got_patch(int fd, struct got_worktree *worktree, struc
 		    &newpath, worktree, repo, fileindex);
 		if (err == NULL)
 			err = apply_patch(&overlapcnt, worktree, repo,
-			    fileindex, oldpath, newpath, &p, nop, &pa,
+			    fileindex, oldpath, newpath, &p, nop, ws, &pa,
 			    cancel_cb, cancel_arg);
 		if (err != NULL) {
 			failed = 1;
blob - 40309e5b82bddf39321e6da00a91b79dd7ec65b0
blob + ef4dd59512cedc603ff733648311fafe3cc5f207
--- regress/cmdline/patch.sh
+++ regress/cmdline/patch.sh
@@ -1273,6 +1273,96 @@ EOF
 	test_done $testroot 0
 }
 
+test_patch_whitespace() {
+	local testroot=`test_init patch_whitespace`
+
+	got checkout $testroot/repo $testroot/wt > /dev/null
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		test_done $testroot $ret
+		return 1
+	fi
+
+	trailing="		"
+
+	cat <<EOF > $testroot/wt/hello.c
+#include <stdio.h>
+
+int
+main(void)
+{
+	/* the trailing whitespace is on purpose */
+	printf("hello, world\n");$trailing
+	return 0;
+}
+EOF
+
+	(cd $testroot/wt && got add hello.c && got ci -m '+hello.c') \
+		> /dev/null
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		test_done $testroot $ret
+		return 1
+	fi
+
+	# test with a diff with various whitespace corruptions
+	cat <<EOF > $testroot/wt/patch
+--- hello.c
++++ hello.c
+@@ -5,5 +5,5 @@
+ {
+ /* the trailing whitespace is on purpose */
+	printf("hello, world\n");
+-    return 0;
++    return 5; /* always fails */
+ }
+EOF
+
+	(cd $testroot/wt && got patch patch) 2>/dev/null 1>&2
+	ret=$?
+	if [ $ret -ne 1 ]; then
+		echo "applied a mangled patch without -w?" >&2
+		test_done $testroot 1
+		return 1
+	fi
+
+	(cd $testroot/wt && got patch -w patch) 2>/dev/null > $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "failed to apply with -w" >&2
+		test_done $testroot $ret
+		return 1
+	fi
+
+	echo 'M  hello.c' > $testroot/stdout.expected
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done $testroot $ret
+		return 1
+	fi
+
+	cat <<EOF > $testroot/wt/hello.c.expected
+#include <stdio.h>
+
+int
+main(void)
+{
+	/* the trailing whitespace is on purpose */
+	printf("hello, world\n");$trailing
+    return 5; /* always fails */
+}
+EOF
+
+	cmp -s $testroot/wt/hello.c.expected $testroot/wt/hello.c
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/wt/hello.c.expected $testroot/wt/hello.c
+	fi
+	test_done $testroot $ret
+}
+
 test_patch_relative_paths() {
 	local testroot=`test_init patch_relative_paths`
 
@@ -1798,6 +1888,7 @@ run_test test_patch_with_offset
 run_test test_patch_prefer_new_path
 run_test test_patch_no_newline
 run_test test_patch_strip
+run_test test_patch_whitespace
 run_test test_patch_relative_paths
 run_test test_patch_with_path_prefix
 run_test test_patch_relpath_with_path_prefix