diff family: add --check option

Actually, it is a diff option now, so you can say

	git diff --check

to ask if what you are about to commit is a good patch.

[jc: this also would work for fmt-patch, but the point is that
 the check is done before making a commit.  format-patch is run
 from an already created commit, and that is too late to catch
 whitespace damaged change.]

Signed-off-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>
Signed-off-by: Junio C Hamano <junkio@cox.net>
diff --git a/diff.c b/diff.c
index e16e0bf..af5db0e 100644
--- a/diff.c
+++ b/diff.c
@@ -397,6 +397,46 @@
 			total_files, adds, dels);
 }
 
+struct checkdiff_t {
+	struct xdiff_emit_state xm;
+	const char *filename;
+	int lineno;
+};
+
+static void checkdiff_consume(void *priv, char *line, unsigned long len)
+{
+	struct checkdiff_t *data = priv;
+
+	if (line[0] == '+') {
+		int i, spaces = 0;
+
+		data->lineno++;
+
+		/* check space before tab */
+		for (i = 1; i < len && (line[i] == ' ' || line[i] == '\t'); i++)
+			if (line[i] == ' ')
+				spaces++;
+		if (line[i - 1] == '\t' && spaces)
+			printf("%s:%d: space before tab:%.*s\n",
+				data->filename, data->lineno, (int)len, line);
+
+		/* check white space at line end */
+		if (line[len - 1] == '\n')
+			len--;
+		if (isspace(line[len - 1]))
+			printf("%s:%d: white space at end: %.*s\n",
+				data->filename, data->lineno, (int)len, line);
+	} else if (line[0] == ' ')
+		data->lineno++;
+	else if (line[0] == '@') {
+		char *plus = strchr(line, '+');
+		if (plus)
+			data->lineno = strtol(plus, line + len, 10);
+		else
+			die("invalid diff");
+	}
+}
+
 static unsigned char *deflate_it(char *data,
 				 unsigned long size,
 				 unsigned long *result_size)
@@ -624,6 +664,41 @@
 	}
 }
 
+static void builtin_checkdiff(const char *name_a, const char *name_b,
+			     struct diff_filespec *one,
+			     struct diff_filespec *two)
+{
+	mmfile_t mf1, mf2;
+	struct checkdiff_t data;
+
+	if (!two)
+		return;
+
+	memset(&data, 0, sizeof(data));
+	data.xm.consume = checkdiff_consume;
+	data.filename = name_b ? name_b : name_a;
+	data.lineno = 0;
+
+	if (fill_mmfile(&mf1, one) < 0 || fill_mmfile(&mf2, two) < 0)
+		die("unable to read files to diff");
+
+	if (mmfile_is_binary(&mf2))
+		return;
+	else {
+		/* Crazy xdl interfaces.. */
+		xpparam_t xpp;
+		xdemitconf_t xecfg;
+		xdemitcb_t ecb;
+
+		xpp.flags = XDF_NEED_MINIMAL;
+		xecfg.ctxlen = 0;
+		xecfg.flags = 0;
+		ecb.outf = xdiff_outf;
+		ecb.priv = &data;
+		xdl_diff(&mf1, &mf2, &xpp, &xecfg, &ecb);
+	}
+}
+
 struct diff_filespec *alloc_filespec(const char *path)
 {
 	int namelen = strlen(path);
@@ -1180,6 +1255,25 @@
 	builtin_diffstat(name, other, p->one, p->two, diffstat, complete_rewrite);
 }
 
+static void run_checkdiff(struct diff_filepair *p, struct diff_options *o)
+{
+	const char *name;
+	const char *other;
+
+	if (DIFF_PAIR_UNMERGED(p)) {
+		/* unmerged */
+		return;
+	}
+
+	name = p->one->path;
+	other = (strcmp(name, p->two->path) ? p->two->path : NULL);
+
+	diff_fill_sha1_info(p->one);
+	diff_fill_sha1_info(p->two);
+
+	builtin_checkdiff(name, other, p->one, p->two);
+}
+
 void diff_setup(struct diff_options *options)
 {
 	memset(options, 0, sizeof(*options));
@@ -1205,7 +1299,8 @@
 	 * recursive bits for other formats here.
 	 */
 	if ((options->output_format == DIFF_FORMAT_PATCH) ||
-	    (options->output_format == DIFF_FORMAT_DIFFSTAT))
+	    (options->output_format == DIFF_FORMAT_DIFFSTAT) ||
+	    (options->output_format == DIFF_FORMAT_CHECKDIFF))
 		options->recursive = 1;
 
 	if (options->detect_rename && options->rename_limit < 0)
@@ -1288,6 +1383,8 @@
 	}
 	else if (!strcmp(arg, "--stat"))
 		options->output_format = DIFF_FORMAT_DIFFSTAT;
+	else if (!strcmp(arg, "--check"))
+		options->output_format = DIFF_FORMAT_CHECKDIFF;
 	else if (!strcmp(arg, "--summary"))
 		options->summary = 1;
 	else if (!strcmp(arg, "--patch-with-stat")) {
@@ -1610,6 +1707,19 @@
 	run_diffstat(p, o, diffstat);
 }
 
+static void diff_flush_checkdiff(struct diff_filepair *p,
+		struct diff_options *o)
+{
+	if (diff_unmodified_pair(p))
+		return;
+
+	if ((DIFF_FILE_VALID(p->one) && S_ISDIR(p->one->mode)) ||
+	    (DIFF_FILE_VALID(p->two) && S_ISDIR(p->two->mode)))
+		return; /* no tree diffs in patch format */
+
+	run_checkdiff(p, o);
+}
+
 int diff_queue_is_empty(void)
 {
 	struct diff_queue_struct *q = &diff_queued_diff;
@@ -1740,6 +1850,9 @@
 		case DIFF_FORMAT_DIFFSTAT:
 			diff_flush_stat(p, options, diffstat);
 			break;
+		case DIFF_FORMAT_CHECKDIFF:
+			diff_flush_checkdiff(p, options);
+			break;
 		case DIFF_FORMAT_PATCH:
 			diff_flush_patch(p, options);
 			break;