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

From:
Mark Jamsek <mark@jamsek.com>
Subject:
got: add diffstat to got diff
To:
Game of Trees <gameoftrees@openbsd.org>
Date:
Mon, 9 Jan 2023 03:11:01 +1100

Download raw body.

Thread
Like the recent 'got log -d' addition, display a diffstat if the -d
switch is passed to 'got diff'.

Output is slightly different, we don't add a blank line between the last
file line and the summary line like we do with got log and the tog diff
view; I think it looks a bit disconnected when we do it in this case.
And for the `got diff blob-hash blob-hash` case, like `git diff --stat`,
we show blob hashes rather than a file path; for example:

----8<---------------------------------------------------------------
  ~/src/got » g diff -d 5e9010e76c3a c9a5f834b0a
   M  5e9010e76c -> c9a5f834b0  |  193+  81-
  1 file changed, 193 insertions(+), 81 deletions(-)

   ---

  ~/src/got » g diff -d 6b5e6124584 333a3ff1f9e                          <
  diff 6b5e61245844733fe2df78892399d5956f422311 333a3ff1f9e9a32f813e293d900492b4ec7c9ca6
  commit - 6b5e61245844733fe2df78892399d5956f422311
  commit + 333a3ff1f9e9a32f813e293d900492b4ec7c9ca6
   M  got/got.1           |    3+   1-
   M  got/got.c           |   70+  31-
   M  include/got_diff.h  |   12+   7-
   M  lib/diff.c          |  193+  81-
   M  lib/worktree.c      |    3+   3-
   M  tog/tog.c           |    4+   3-
  6 files changed, 285 insertions(+), 126 deletions(-)

  ---

  ~/src/got » g diff -d -c 6b5e6124584 -c 333a3ff1f9e got
  diff 6b5e61245844733fe2df78892399d5956f422311 333a3ff1f9e9a32f813e293d900492b4ec7c9ca6
  commit - 6b5e61245844733fe2df78892399d5956f422311
  commit + 333a3ff1f9e9a32f813e293d900492b4ec7c9ca6
   M  got/got.1  |   3+   1-
   M  got/got.c  |  70+  31-
  2 files changed, 73 insertions(+), 32 deletions(-)
--------------------------------------------------------------->8----

-----------------------------------------------
commit 333a3ff1f9e9a32f813e293d900492b4ec7c9ca6
from: Mark Jamsek <mark@jamsek.dev>
date: Sun Jan  8 16:03:25 2023 UTC
 
 got: expand diffstat -d option to 'got diff'
 
 Like got log -d, add the switch to got diff to display a diffstat of the
 specified diff.
 
 M  got/got.1           |    3+   1-
 M  got/got.c           |   70+  31-
 M  include/got_diff.h  |   12+   7-
 M  lib/diff.c          |  193+  81-
 M  lib/worktree.c      |    3+   3-
 M  tog/tog.c           |    4+   3-

6 files changed, 285 insertions(+), 126 deletions(-)

diff 6b5e61245844733fe2df78892399d5956f422311 333a3ff1f9e9a32f813e293d900492b4ec7c9ca6
commit - 6b5e61245844733fe2df78892399d5956f422311
commit + 333a3ff1f9e9a32f813e293d900492b4ec7c9ca6
blob - c86779cca9d917f301a8ac66e723282c6c0bcb57
blob + 531645e51924cf65f5a19918c0bb0c56bf4ac847
--- got/got.1
+++ got/got.1
@@ -904,7 +904,7 @@ is never traversed.
 .Tg di
 .It Xo
 .Cm diff
-.Op Fl aPsw
+.Op Fl adPsw
 .Op Fl C Ar number
 .Op Fl c Ar commit
 .Op Fl r Ar repository-path
@@ -962,6 +962,8 @@ option.
 Cannot be used together with the
 .Fl P
 option.
+.It Fl d
+Display diffstat of changes.
 .It Fl P
 Interpret all arguments as paths only.
 This option can be used to resolve ambiguity in cases where paths
blob - 9a89d5b62f1635fb4b385534abdd217ee4a06611
blob + bbac2d276a4495a9976cd76f1ce352bbd4797f1c
--- got/got.c
+++ got/got.c
@@ -3624,7 +3624,8 @@ diff_blobs(struct got_object_id *blob_id1, struct got_
 static const struct got_error *
 diff_blobs(struct got_object_id *blob_id1, struct got_object_id *blob_id2,
     const char *path, int diff_context, int ignore_whitespace,
-    int force_text_diff, struct got_repository *repo, FILE *outfile)
+    int force_text_diff, int show_diffstat, struct got_repository *repo,
+    FILE *outfile)
 {
 	const struct got_error *err = NULL;
 	struct got_blob_object *blob1 = NULL, *blob2 = NULL;
@@ -3666,7 +3667,7 @@ diff_blobs(struct got_object_id *blob_id1, struct got_
 		path++;
 	err = got_diff_blob(NULL, NULL, blob1, blob2, f1, f2, path, path,
 	    GOT_DIFF_ALGORITHM_PATIENCE, diff_context, ignore_whitespace,
-	    force_text_diff, outfile);
+	    force_text_diff, show_diffstat, NULL, outfile);
 done:
 	if (fd1 != -1 && close(fd1) == -1 && err == NULL)
 		err = got_error_from_errno("close");
@@ -3727,6 +3728,7 @@ diff_trees(struct got_object_id *tree_id1, struct got_
 	arg.diff_context = diff_context;
 	arg.ignore_whitespace = ignore_whitespace;
 	arg.force_text_diff = force_text_diff;
+	arg.show_diffstat = 0;
 	arg.diff_algo = GOT_DIFF_ALGORITHM_PATIENCE;
 	arg.outfile = outfile;
 	arg.lines = NULL;
@@ -3891,7 +3893,7 @@ print_patch(struct got_commit_object *commit, struct g
 		switch (obj_type) {
 		case GOT_OBJ_TYPE_BLOB:
 			err = diff_blobs(obj_id1, obj_id2, path, diff_context,
-			    0, 0, repo, outfile);
+			    0, 0, 0, repo, outfile);
 			break;
 		case GOT_OBJ_TYPE_TREE:
 			err = diff_trees(obj_id1, obj_id2, path, diff_context,
@@ -4163,6 +4165,32 @@ static const struct got_error *
 	return err;
 }
 
+static void
+print_diffstat(struct got_diffstat_cb_arg *dsa, struct got_pathlist_head *paths,
+    int newline)
+{
+	struct got_pathlist_entry *pe;
+
+	TAILQ_FOREACH(pe, paths, entry) {
+		struct got_diff_changed_path *cp = pe->data;
+		int pad = dsa->max_path_len - pe->path_len + 1;
+
+		printf(" %c  %s%*c | %*d+ %*d-\n", cp->status, pe->path, pad,
+		    ' ', dsa->add_cols + 1, cp->add, dsa->rm_cols + 1, cp->rm);
+
+		free(pe->data);
+		free((char *)pe->path);
+	}
+	if (newline)
+		printf("\n");
+	printf("%d file%s changed, %d insertions(+), %d deletions(-)\n",
+	    dsa->nfiles, dsa->nfiles > 1 ? "s" : "", dsa->ins, dsa->del);
+	if (newline)
+		printf("\n");
+
+	got_pathlist_free(paths);
+}
+
 static const struct got_error *
 print_commit(struct got_commit_object *commit, struct got_object_id *id,
     struct got_repository *repo, const char *path,
@@ -4237,31 +4265,16 @@ print_commit(struct got_commit_object *commit, struct 
 	} while (line);
 	free(logmsg0);
 
-	if (changed_paths) {
+	if (dsa && changed_paths)
+		print_diffstat(dsa, changed_paths, 1);
+	else if (changed_paths) {
 		struct got_pathlist_entry *pe;
 
 		TAILQ_FOREACH(pe, changed_paths, entry) {
 			struct got_diff_changed_path *cp = pe->data;
-			char *stat = NULL;
 
-			if (dsa) {
-				int pad = dsa->max_path_len - pe->path_len + 1;
-
-				if (asprintf(&stat, "%*c | %*d+ %*d-",
-				    pad, ' ', dsa->add_cols + 1, cp->add,
-				    dsa->rm_cols + 1, cp->rm) == -1) {
-					err = got_error_from_errno("asprintf");
-					goto done;
-				}
-			}
-			printf(" %c  %s%s\n", cp->status, pe->path,
-			    stat ? stat : "");
-			free(stat);
+			printf(" %c  %s\n", cp->status, pe->path);
 		}
-		if (dsa)
-			printf("\n%d file%s changed, %d insertions(+), "
-			    "%d deletions(-)\n", dsa->nfiles,
-			    dsa->nfiles > 1 ? "s" : "", dsa->ins, dsa->del);
 		printf("\n");
 	}
 	if (show_patch) {
@@ -4727,7 +4740,7 @@ usage_diff(void)
 __dead static void
 usage_diff(void)
 {
-	fprintf(stderr, "usage: %s diff [-aPsw] [-C number] [-c commit] "
+	fprintf(stderr, "usage: %s diff [-adPsw] [-C number] [-c commit] "
 	    "[-r repository-path] [object1 object2 | path ...]\n",
 	    getprogname());
 	exit(1);
@@ -4736,6 +4749,7 @@ struct print_diff_arg {
 struct print_diff_arg {
 	struct got_repository *repo;
 	struct got_worktree *worktree;
+	struct got_diffstat_cb_arg *diffstat;
 	int diff_context;
 	const char *id_str;
 	int header_shown;
@@ -4743,6 +4757,7 @@ struct print_diff_arg {
 	enum got_diff_algorithm diff_algo;
 	int ignore_whitespace;
 	int force_text_diff;
+	int show_diffstat;
 	FILE *f1;
 	FILE *f2;
 };
@@ -4874,7 +4889,8 @@ print_diff(void *arg, unsigned char status, unsigned c
 		err = got_diff_objects_as_blobs(NULL, NULL, a->f1, a->f2,
 		    fd1, fd2, blob_id, staged_blob_id, label1, label2,
 		    a->diff_algo, a->diff_context, a->ignore_whitespace,
-		    a->force_text_diff, a->repo, stdout);
+		    a->force_text_diff, a->show_diffstat, a->diffstat, a->repo,
+		    stdout);
 		goto done;
 	}
 
@@ -4964,7 +4980,8 @@ print_diff(void *arg, unsigned char status, unsigned c
 
 	err = got_diff_blob_file(blob1, a->f1, size1, label1, f2 ? f2 : a->f2,
 	    f2_exists, &sb, path, GOT_DIFF_ALGORITHM_PATIENCE, a->diff_context,
-	    a->ignore_whitespace, a->force_text_diff, stdout);
+	    a->ignore_whitespace, a->force_text_diff, a->show_diffstat,
+	    a->diffstat, stdout);
 done:
 	if (fd1 != -1 && close(fd1) == -1 && err == NULL)
 		err = got_error_from_errno("close");
@@ -4993,17 +5010,19 @@ cmd_diff(int argc, char *argv[])
 	char *labels[2] = { NULL, NULL };
 	int type1 = GOT_OBJ_TYPE_ANY, type2 = GOT_OBJ_TYPE_ANY;
 	int diff_context = 3, diff_staged = 0, ignore_whitespace = 0, ch, i;
-	int force_text_diff = 0, force_path = 0, rflag = 0;
+	int force_text_diff = 0, force_path = 0, rflag = 0, show_diffstat = 0;
 	const char *errstr;
 	struct got_reflist_head refs;
-	struct got_pathlist_head paths;
+	struct got_pathlist_head diffstat_paths, paths;
 	struct got_pathlist_entry *pe;
 	FILE *f1 = NULL, *f2 = NULL;
 	int fd1 = -1, fd2 = -1;
 	int *pack_fds = NULL;
+	struct got_diffstat_cb_arg dsa;
 
 	TAILQ_INIT(&refs);
 	TAILQ_INIT(&paths);
+	TAILQ_INIT(&diffstat_paths);
 
 #ifndef PROFILE
 	if (pledge("stdio rpath wpath cpath flock proc exec sendfd unveil",
@@ -5011,7 +5030,7 @@ cmd_diff(int argc, char *argv[])
 		err(1, "pledge");
 #endif
 
-	while ((ch = getopt(argc, argv, "aC:c:Pr:sw")) != -1) {
+	while ((ch = getopt(argc, argv, "aC:c:dPr:sw")) != -1) {
 		switch (ch) {
 		case 'a':
 			force_text_diff = 1;
@@ -5028,6 +5047,9 @@ cmd_diff(int argc, char *argv[])
 				errx(1, "too many -c options used");
 			commit_args[ncommit_args++] = optarg;
 			break;
+		case 'd':
+			show_diffstat = 1;
+			break;
 		case 'P':
 			force_path = 1;
 			break;
@@ -5091,6 +5113,14 @@ cmd_diff(int argc, char *argv[])
 	if (error != NULL)
 		goto done;
 
+	if (show_diffstat) {
+		memset(&dsa, 0, sizeof(dsa));
+		dsa.paths = &diffstat_paths;
+		dsa.force_text = force_text_diff;
+		dsa.ignore_ws = ignore_whitespace;
+		dsa.diff_algo = GOT_DIFF_ALGORITHM_PATIENCE;
+	}
+
 	if (rflag || worktree == NULL || ncommit_args > 0) {
 		if (force_path) {
 			error = got_error_msg(GOT_ERR_NOT_IMPL,
@@ -5189,6 +5219,8 @@ cmd_diff(int argc, char *argv[])
 		arg.diff_staged = diff_staged;
 		arg.ignore_whitespace = ignore_whitespace;
 		arg.force_text_diff = force_text_diff;
+		arg.show_diffstat = show_diffstat;
+		arg.diffstat = &dsa;
 		arg.f1 = f1;
 		arg.f2 = f2;
 
@@ -5329,25 +5361,31 @@ cmd_diff(int argc, char *argv[])
 		error = got_diff_objects_as_blobs(NULL, NULL, f1, f2,
 		    fd1, fd2, ids[0], ids[1], NULL, NULL,
 		    GOT_DIFF_ALGORITHM_PATIENCE, diff_context,
-		    ignore_whitespace, force_text_diff, repo, stdout);
+		    ignore_whitespace, force_text_diff, show_diffstat,
+		    show_diffstat ? &dsa : NULL, repo, stdout);
 		break;
 	case GOT_OBJ_TYPE_TREE:
 		error = got_diff_objects_as_trees(NULL, NULL, f1, f2, fd1, fd2,
 		    ids[0], ids[1], &paths, "", "",
 		    GOT_DIFF_ALGORITHM_PATIENCE, diff_context,
-		    ignore_whitespace, force_text_diff, repo, stdout);
+		    ignore_whitespace, force_text_diff, show_diffstat,
+		    show_diffstat ? &dsa : NULL, repo, stdout);
 		break;
 	case GOT_OBJ_TYPE_COMMIT:
 		printf("diff %s %s\n", labels[0], labels[1]);
 		error = got_diff_objects_as_commits(NULL, NULL, f1, f2,
 		    fd1, fd2, ids[0], ids[1], &paths,
 		    GOT_DIFF_ALGORITHM_PATIENCE, diff_context,
-		    ignore_whitespace, force_text_diff, repo, stdout);
+		    ignore_whitespace, force_text_diff, show_diffstat,
+		    show_diffstat ? &dsa : NULL, repo, stdout);
 		break;
 	default:
 		error = got_error(GOT_ERR_OBJ_TYPE);
 	}
+
 done:
+	if (show_diffstat && dsa.nfiles > 0 && error == NULL)
+		print_diffstat(&dsa, &diffstat_paths, 0);
 	free(labels[0]);
 	free(labels[1]);
 	free(ids[0]);
@@ -5368,6 +5406,7 @@ done:
 	TAILQ_FOREACH(pe, &paths, entry)
 		free((char *)pe->path);
 	got_pathlist_free(&paths);
+	got_pathlist_free(&diffstat_paths);
 	got_ref_list_free(&refs);
 	if (f1 && fclose(f1) == EOF && error == NULL)
 		error = got_error_from_errno("fclose");
blob - 18fe6a19ccc9b5b2450e313f81d9246ce51b630d
blob + 8f6632dd374680ed883152c72ac77c5ace39531e
--- include/got_diff.h
+++ include/got_diff.h
@@ -44,6 +44,7 @@ struct got_diff_line {
 	uint8_t	type;
 };
 
+struct got_diffstat_cb_arg;
 /*
  * Compute the differences between two blobs and write unified diff text
  * to the provided output file. Two open temporary files must be provided
@@ -61,8 +62,8 @@ const struct got_error *got_diff_blob(struct got_diff_
  */
 const struct got_error *got_diff_blob(struct got_diff_line **, size_t *,
     struct got_blob_object *, struct got_blob_object *, FILE *, FILE *,
-    const char *, const char *, enum got_diff_algorithm, int, int, int,
-    FILE *);
+    const char *, const char *, enum got_diff_algorithm, int, int, int, int,
+    struct got_diffstat_cb_arg *, FILE *);
 
 /*
  * Compute the differences between a blob and a file and write unified diff
@@ -75,7 +76,8 @@ const struct got_error *got_diff_blob_file(struct got_
  */
 const struct got_error *got_diff_blob_file(struct got_blob_object *, FILE *,
     off_t, const char *, FILE *, int, struct stat *, const char *,
-    enum got_diff_algorithm, int, int, int, FILE *);
+    enum got_diff_algorithm, int, int, int, int, struct got_diffstat_cb_arg *,
+    FILE *);
 
 /*
  * A callback function invoked to handle the differences between two blobs
@@ -105,6 +107,7 @@ struct got_diff_blob_output_unidiff_arg {
 	int diff_context;	/* Sets the number of context lines. */
 	int ignore_whitespace;	/* Ignore whitespace differences. */
 	int force_text_diff;	/* Assume text even if binary data detected. */
+	int show_diffstat;
 	enum got_diff_algorithm diff_algo; /* Diffing algorithm to use. */
 
 	/*
@@ -204,7 +207,8 @@ const struct got_error *got_diff_objects_as_blobs(stru
 const struct got_error *got_diff_objects_as_blobs(struct got_diff_line **,
     size_t *, FILE *, FILE *, int, int, struct got_object_id *,
     struct got_object_id *, const char *, const char *, enum got_diff_algorithm,
-    int, int, int, struct got_repository *, FILE *);
+    int, int, int, int, struct got_diffstat_cb_arg *, struct got_repository *,
+    FILE *);
 
 struct got_pathlist_head;
 
@@ -226,8 +230,8 @@ const struct got_error *got_diff_objects_as_trees(stru
 const struct got_error *got_diff_objects_as_trees(struct got_diff_line **,
     size_t *, FILE *, FILE *, int, int, struct got_object_id *,
     struct got_object_id *, struct got_pathlist_head *, const char *,
-    const char *, enum got_diff_algorithm, int, int, int,
-    struct got_repository *, FILE *);
+    const char *, enum got_diff_algorithm, int, int, int, int,
+    struct got_diffstat_cb_arg *, struct got_repository *, FILE *);
 
 /*
  * Diff two objects, assuming both objects are commits.
@@ -244,6 +248,7 @@ const struct got_error *got_diff_objects_as_commits(st
 const struct got_error *got_diff_objects_as_commits(struct got_diff_line **,
     size_t *, FILE *, FILE *, int, int, struct got_object_id *,
     struct got_object_id *, struct got_pathlist_head *, enum got_diff_algorithm,
-    int, int, int, struct got_repository *, FILE *);
+    int, int, int, int, struct got_diffstat_cb_arg *, struct got_repository *,
+    FILE *);
 
 #define GOT_DIFF_MAX_CONTEXT	64
blob - 5e9010e76c3aa5de6de83e6b5cd54a4749c4f51a
blob + c9a5f834b0a8edd0dc02cf506d56dbc4d77e85b8
--- lib/diff.c
+++ lib/diff.c
@@ -59,12 +59,75 @@ static const struct got_error *
 	return NULL;
 }
 
+static void
+diffstat_field_width(size_t *maxlen, int *add_cols, int *rm_cols, size_t len,
+    uint32_t add, uint32_t rm)
+{
+	int d1 = 1, d2 = 1;
+
+	if (maxlen)
+		*maxlen = MAX(*maxlen, len);
+
+	while (add /= 10)
+		++d1;
+	*add_cols = MAX(*add_cols, d1);
+
+	while (rm /= 10)
+		++d2;
+	*rm_cols = MAX(*rm_cols, d2);
+}
+
 static const struct got_error *
+get_diffstat(struct got_diffstat_cb_arg *ds, const char *path,
+    struct got_diff_changed_path *change, struct diff_result *r, int force_text)
+{
+	const struct got_error *err;
+	struct got_pathlist_entry *pe;
+	int flags = (r->left->atomizer_flags | r->right->atomizer_flags);
+	int isbin = (flags & DIFF_ATOMIZER_FOUND_BINARY_DATA);
+	int i;
+
+	change->add = 0;
+	change->rm = 0;
+
+	if (!isbin || force_text) {
+		for (i = 0; i < r->chunks.len; ++i) {
+			struct diff_chunk *c;
+			int clc, crc;
+
+			c = diff_chunk_get(r, i);
+			clc = diff_chunk_get_left_count(c);
+			crc = diff_chunk_get_right_count(c);
+
+			if (crc && !clc)
+				change->add += crc;
+			if (clc && !crc)
+				change->rm += clc;
+		}
+	}
+
+	ds->ins += change->add;
+	ds->del += change->rm;
+	++ds->nfiles;
+
+	err = got_pathlist_append(ds->paths, path, change);
+	if (err)
+		return err;
+
+	pe = TAILQ_LAST(ds->paths, got_pathlist_head);
+	diffstat_field_width(&ds->max_path_len, &ds->add_cols, &ds->rm_cols,
+	    pe->path_len, change->add, change->rm);
+
+	return NULL;
+}
+
+static const struct got_error *
 diff_blobs(struct got_diff_line **lines, size_t *nlines,
     struct got_diffreg_result **resultp, struct got_blob_object *blob1,
     struct got_blob_object *blob2, FILE *f1, FILE *f2,
     const char *label1, const char *label2, mode_t mode1, mode_t mode2,
-    int diff_context, int ignore_whitespace, int force_text_diff, FILE *outfile,
+    int diff_context, int ignore_whitespace, int force_text_diff,
+    int show_diffstat, struct got_diffstat_cb_arg *ds, FILE *outfile,
     enum got_diff_algorithm diff_algo)
 {
 	const struct got_error *err = NULL, *free_err;
@@ -118,7 +181,7 @@ diff_blobs(struct got_diff_line **lines, size_t *nline
 	} else
 		idstr2 = "/dev/null";
 
-	if (outfile) {
+	if (outfile && !show_diffstat) {
 		char *modestr1 = NULL, *modestr2 = NULL;
 		int modebits;
 		if (mode1 && mode1 != mode2) {
@@ -170,12 +233,59 @@ diff_blobs(struct got_diff_line **lines, size_t *nline
 		free(modestr1);
 		free(modestr2);
 	}
+
 	err = got_diffreg(&result, f1, f2, diff_algo, ignore_whitespace,
 	     force_text_diff);
 	if (err)
 		goto done;
 
-	if (outfile) {
+	if (show_diffstat) {
+		struct got_diff_changed_path	*change = NULL;
+		char				*path = NULL;
+
+		if (label1 == NULL && label2 == NULL) {
+			/* diffstat of blobs, show hash instead of path */
+			if (asprintf(&path, "%.10s -> %.10s",
+			    idstr1, idstr2) == -1) {
+				err = got_error_from_errno("asprintf");
+				goto done;
+			}
+		} else {
+			path = strdup(label2 ? label2 : label1);
+			if (path == NULL) {
+				err = got_error_from_errno("malloc");
+				goto done;
+			}
+		}
+
+		change = malloc(sizeof(*change));
+		if (change == NULL) {
+			free(path);
+			err = got_error_from_errno("malloc");
+			goto done;
+		}
+
+		/*
+		 * Ignore 'm'ode status change: if there's no accompanying
+		 * content change, there'll be no diffstat, and if there
+		 * are actual changes, 'M'odified takes precedence.
+		 */
+		change->status = GOT_STATUS_NO_CHANGE;
+		if (blob1 == NULL)
+			change->status = GOT_STATUS_ADD;
+		else if (blob2 == NULL)
+			change->status = GOT_STATUS_DELETE;
+		else
+			change->status = GOT_STATUS_MODIFY;
+
+		err = get_diffstat(ds, path, change, result->result,
+		    force_text_diff);
+		if (err) {
+			free(change);
+			free(path);
+			goto done;
+		}
+	} else if (outfile) {
 		err = got_diffreg_output(lines, nlines, result,
 		    blob1 != NULL, blob2 != NULL,
 		    label1 ? label1 : idstr1,
@@ -208,7 +318,8 @@ got_diff_blob_output_unidiff(void *arg, struct got_blo
 
 	return diff_blobs(&a->lines, &a->nlines, NULL,
 	    blob1, blob2, f1, f2, label1, label2, mode1, mode2, a->diff_context,
-	    a->ignore_whitespace, a->force_text_diff, a->outfile, a->diff_algo);
+	    a->ignore_whitespace, a->force_text_diff, a->show_diffstat, NULL,
+	    a->outfile, a->diff_algo);
 }
 
 const struct got_error *
@@ -216,11 +327,12 @@ got_diff_blob(struct got_diff_line **lines, size_t*nli
     struct got_blob_object *blob1, struct got_blob_object *blob2,
     FILE *f1, FILE *f2, const char *label1, const char *label2,
     enum got_diff_algorithm diff_algo, int diff_context,
-    int ignore_whitespace, int force_text_diff, FILE *outfile)
+    int ignore_whitespace, int force_text_diff, int show_diffstat,
+    struct got_diffstat_cb_arg *ds, FILE *outfile)
 {
 	return diff_blobs(lines, nlines, NULL, blob1, blob2, f1, f2,
 	    label1, label2, 0, 0, diff_context, ignore_whitespace,
-	    force_text_diff, outfile, diff_algo);
+	    force_text_diff, show_diffstat, ds, outfile, diff_algo);
 }
 
 static const struct got_error *
@@ -228,7 +340,8 @@ diff_blob_file(struct got_diffreg_result **resultp,
     struct got_blob_object *blob1, FILE *f1, off_t size1, const char *label1,
     FILE *f2, int f2_exists, struct stat *sb2, const char *label2,
     enum got_diff_algorithm diff_algo, int diff_context, int ignore_whitespace,
-    int force_text_diff, FILE *outfile)
+    int force_text_diff, int show_diffstat, struct got_diffstat_cb_arg *ds,
+    FILE *outfile)
 {
 	const struct got_error *err = NULL, *free_err;
 	char hex1[SHA1_DIGEST_STRING_LENGTH];
@@ -243,7 +356,7 @@ diff_blob_file(struct got_diffreg_result **resultp,
 	else
 		idstr1 = "/dev/null";
 
-	if (outfile) {
+	if (outfile && !show_diffstat) {
 		char	*mode = NULL;
 
 		/* display file mode for new added files only */
@@ -267,7 +380,7 @@ diff_blob_file(struct got_diffreg_result **resultp,
 	if (err)
 		goto done;
 
-	if (outfile) {
+	if (outfile && !show_diffstat) {
 		err = got_diffreg_output(NULL, NULL, result,
 		    blob1 != NULL, f2_exists,
 		    label2, /* show local file's path, not a blob ID */
@@ -275,6 +388,43 @@ diff_blob_file(struct got_diffreg_result **resultp,
 		    diff_context, outfile);
 		if (err)
 			goto done;
+	} else if (show_diffstat) {
+		struct got_diff_changed_path	*change = NULL;
+		char				*path = NULL;
+
+		path = strdup(label2 ? label2 : label1);
+		if (path == NULL) {
+			err = got_error_from_errno("malloc");
+			goto done;
+		}
+
+		change = malloc(sizeof(*change));
+		if (change == NULL) {
+			free(path);
+			err = got_error_from_errno("malloc");
+			goto done;
+		}
+
+		/*
+		 * Ignore 'm'ode status change: if there's no accompanying
+		 * content change, there'll be no diffstat, and if there
+		 * are actual changes, 'M'odified takes precedence.
+		 */
+		change->status = GOT_STATUS_NO_CHANGE;
+		if (blob1 == NULL)
+			change->status = GOT_STATUS_ADD;
+		else if (!f2_exists)
+			change->status = GOT_STATUS_DELETE;
+		else
+			change->status = GOT_STATUS_MODIFY;
+
+		err = get_diffstat(ds, path, change, result->result,
+		    force_text_diff);
+		if (err) {
+			free(change);
+			free(path);
+			goto done;
+		}
 	}
 
 done:
@@ -292,11 +442,12 @@ got_diff_blob_file(struct got_blob_object *blob1, FILE
 got_diff_blob_file(struct got_blob_object *blob1, FILE *f1, off_t size1,
     const char *label1, FILE *f2, int f2_exists, struct stat *sb2,
     const char *label2, enum got_diff_algorithm diff_algo, int diff_context,
-    int ignore_whitespace, int force_text_diff, FILE *outfile)
+    int ignore_whitespace, int force_text_diff, int show_diffstat,
+    struct got_diffstat_cb_arg *ds, FILE *outfile)
 {
 	return diff_blob_file(NULL, blob1, f1, size1, label1, f2, f2_exists,
 	    sb2, label2, diff_algo, diff_context, ignore_whitespace,
-	    force_text_diff, outfile);
+	    force_text_diff, show_diffstat, ds, outfile);
 }
 
 static const struct got_error *
@@ -611,23 +762,6 @@ static void
 	    NULL, label2, 0, te2->mode, repo);
 }
 
-static void
-diffstat_field_width(size_t *maxlen, int *add_cols, int *rm_cols, size_t len,
-    uint32_t add, uint32_t rm)
-{
-	int d1 = 1, d2 = 1;
-
-	*maxlen = MAX(*maxlen, len);
-
-	while (add /= 10)
-		++d1;
-	*add_cols = MAX(*add_cols, d1);
-
-	while (rm /= 10)
-		++d2;
-	*rm_cols = MAX(*rm_cols, d2);
-}
-
 const struct got_error *
 got_diff_tree_compute_diffstat(void *arg, struct got_blob_object *blob1,
     struct got_blob_object *blob2, FILE *f1, FILE *f2,
@@ -637,12 +771,9 @@ got_diff_tree_compute_diffstat(void *arg, struct got_b
 {
 	const struct got_error		*err = NULL;
 	struct got_diffreg_result	*result = NULL;
-	struct diff_result		*r;
 	struct got_diff_changed_path	*change = NULL;
 	struct got_diffstat_cb_arg	*a = arg;
-	struct got_pathlist_entry	*pe;
 	char				*path = NULL;
-	int				 i;
 
 	path = strdup(label2 ? label2 : label1);
 	if (path == NULL)
@@ -697,36 +828,8 @@ got_diff_tree_compute_diffstat(void *arg, struct got_b
 	if (err)
 		goto done;
 
-	for (i = 0, r = result->result; i < r->chunks.len; ++i) {
-		int flags = (r->left->atomizer_flags | r->right->atomizer_flags);
-		int isbin = (flags & DIFF_ATOMIZER_FOUND_BINARY_DATA);
+	err = get_diffstat(a, path, change, result->result, a->force_text);
 
-		if (!isbin || a->force_text) {
-			struct diff_chunk *c;
-			int clc, crc;
-
-			c = diff_chunk_get(r, i);
-			clc = diff_chunk_get_left_count(c);
-			crc = diff_chunk_get_right_count(c);
-
-			if (clc && !crc)
-				change->rm += clc;
-			else if (crc && !clc)
-				change->add += crc;
-		}
-	}
-
-	err = got_pathlist_append(a->paths, path, change);
-	if (err)
-		goto done;
-
-	pe = TAILQ_LAST(a->paths, got_pathlist_head);
-	diffstat_field_width(&a->max_path_len, &a->add_cols, &a->rm_cols,
-	    pe->path_len, change->add, change->rm);
-	a->ins += change->add;
-	a->del += change->rm;
-	++a->nfiles;
-
 done:
 	if (result) {
 		const struct got_error *free_err;
@@ -885,8 +988,8 @@ got_diff_objects_as_blobs(struct got_diff_line **lines
     struct got_object_id *id1, struct got_object_id *id2,
     const char *label1, const char *label2,
     enum got_diff_algorithm diff_algo, int diff_context,
-    int ignore_whitespace, int force_text_diff,
-    struct got_repository *repo, FILE *outfile)
+    int ignore_whitespace, int force_text_diff, int show_diffstat,
+    struct got_diffstat_cb_arg *ds, struct got_repository *repo, FILE *outfile)
 {
 	const struct got_error *err;
 	struct got_blob_object *blob1 = NULL, *blob2 = NULL;
@@ -906,7 +1009,7 @@ got_diff_objects_as_blobs(struct got_diff_line **lines
 	}
 	err = got_diff_blob(lines, nlines, blob1, blob2, f1, f2, label1, label2,
 	    diff_algo, diff_context, ignore_whitespace, force_text_diff,
-	    outfile);
+	    show_diffstat, ds, outfile);
 done:
 	if (blob1)
 		got_object_blob_close(blob1);
@@ -1080,12 +1183,14 @@ diff_objects_as_trees(struct got_diff_line **lines, si
     struct got_object_id *id1, struct got_object_id *id2,
     struct got_pathlist_head *paths, const char *label1, const char *label2,
     int diff_context, int ignore_whitespace, int force_text_diff,
+    int show_diffstat, struct got_diffstat_cb_arg *dsa,
     struct got_repository *repo, FILE *outfile,
     enum got_diff_algorithm diff_algo)
 {
 	const struct got_error *err;
 	struct got_tree_object *tree1 = NULL, *tree2 = NULL;
 	struct got_diff_blob_output_unidiff_arg arg;
+	got_diff_blob_cb cb = got_diff_blob_output_unidiff;
 	int want_linemeta = (lines != NULL && *lines != NULL);
 
 	if (id1 == NULL && id2 == NULL)
@@ -1102,25 +1207,30 @@ diff_objects_as_trees(struct got_diff_line **lines, si
 			goto done;
 	}
 
-	arg.diff_algo = diff_algo;
-	arg.diff_context = diff_context;
-	arg.ignore_whitespace = ignore_whitespace;
-	arg.force_text_diff = force_text_diff;
-	arg.outfile = outfile;
-	if (want_linemeta) {
-		arg.lines = *lines;
-		arg.nlines = *nlines;
-	} else {
-		arg.lines = NULL;
-		arg.nlines = 0;
+	if (show_diffstat)
+		cb = got_diff_tree_compute_diffstat;
+	else {
+		arg.diff_algo = diff_algo;
+		arg.diff_context = diff_context;
+		arg.ignore_whitespace = ignore_whitespace;
+		arg.force_text_diff = force_text_diff;
+		arg.show_diffstat = show_diffstat;
+		arg.outfile = outfile;
+		if (want_linemeta) {
+			arg.lines = *lines;
+			arg.nlines = *nlines;
+		} else {
+			arg.lines = NULL;
+			arg.nlines = 0;
+		}
 	}
 	if (paths == NULL || TAILQ_EMPTY(paths)) {
 		err = got_diff_tree(tree1, tree2, f1, f2, fd1, fd2,
-		    label1, label2, repo,
-		    got_diff_blob_output_unidiff, &arg, 1);
+		    label1, label2, repo, cb,
+		    show_diffstat ? (void *)dsa : &arg, 1);
 	} else {
 		err = diff_paths(tree1, tree2, f1, f2, fd1, fd2, paths, repo,
-		    got_diff_blob_output_unidiff, &arg);
+		    cb, show_diffstat ? (void *)dsa : &arg);
 	}
 	if (want_linemeta) {
 		*lines = arg.lines; /* was likely re-allocated */
@@ -1140,7 +1250,8 @@ got_diff_objects_as_trees(struct got_diff_line **lines
     struct got_object_id *id1, struct got_object_id *id2,
     struct got_pathlist_head *paths, const char *label1, const char *label2,
     enum got_diff_algorithm diff_algo, int diff_context, int ignore_whitespace,
-    int force_text_diff, struct got_repository *repo, FILE *outfile)
+    int force_text_diff, int show_diffstat, struct got_diffstat_cb_arg *dsa,
+    struct got_repository *repo, FILE *outfile)
 {
 	const struct got_error *err;
 	char *idstr = NULL;
@@ -1182,7 +1293,7 @@ got_diff_objects_as_trees(struct got_diff_line **lines
 
 	err = diff_objects_as_trees(lines, nlines, f1, f2, fd1, fd2, id1, id2,
 	    paths, label1, label2, diff_context, ignore_whitespace,
-	    force_text_diff, repo, outfile, diff_algo);
+	    force_text_diff, show_diffstat, dsa, repo, outfile, diff_algo);
 done:
 	free(idstr);
 	return err;
@@ -1194,6 +1305,7 @@ got_diff_objects_as_commits(struct got_diff_line **lin
     struct got_object_id *id1, struct got_object_id *id2,
     struct got_pathlist_head *paths, enum got_diff_algorithm diff_algo,
     int diff_context, int ignore_whitespace, int force_text_diff,
+    int show_diffstat, struct got_diffstat_cb_arg *dsa,
     struct got_repository *repo, FILE *outfile)
 {
 	const struct got_error *err;
@@ -1237,8 +1349,8 @@ got_diff_objects_as_commits(struct got_diff_line **lin
 	err = diff_objects_as_trees(lines, nlines, f1, f2, fd1, fd2,
 	    commit1 ? got_object_commit_get_tree_id(commit1) : NULL,
 	    got_object_commit_get_tree_id(commit2), paths, "", "",
-	    diff_context, ignore_whitespace, force_text_diff, repo, outfile,
-	    diff_algo);
+	    diff_context, ignore_whitespace, force_text_diff, show_diffstat,
+	    dsa, repo, outfile, diff_algo);
 done:
 	if (commit1)
 		got_object_commit_close(commit1);
blob - b40f67cf87889e4cd271c0870db2a357160443ad
blob + e92fe5aa6a9932cfb59504fd20073ebc1fb4f6d4
--- lib/worktree.c
+++ lib/worktree.c
@@ -5083,8 +5083,8 @@ append_ct_diff(struct got_commitable *ct, int *diff_he
 		}
 		err = got_diff_objects_as_blobs(NULL, NULL, f1, f2,
 		    fd1, fd2, ct->base_blob_id, ct->staged_blob_id,
-		    label1, label2, GOT_DIFF_ALGORITHM_PATIENCE, 3, 0, 0,
-		    repo, diff_outfile);
+		    label1, label2, GOT_DIFF_ALGORITHM_PATIENCE, 3, 0, 0, 0,
+		    NULL, repo, diff_outfile);
 		goto done;
 	}
 
@@ -5154,7 +5154,7 @@ append_ct_diff(struct got_commitable *ct, int *diff_he
 
 	err = got_diff_blob_file(blob1, f1, size1, label1,
 	    ondisk_file ? ondisk_file : f2, f2_exists, &sb, ct->path,
-	    GOT_DIFF_ALGORITHM_PATIENCE, 3, 0, 0, diff_outfile);
+	    GOT_DIFF_ALGORITHM_PATIENCE, 3, 0, 0, 0, NULL, diff_outfile);
 done:
 	if (fd1 != -1 && close(fd1) == -1 && err == NULL)
 		err = got_error_from_errno("close");
blob - 7a91723cf327c438d7801b8d486b165bd84ba1ea
blob + e05667c5078d9ea47470c917b489eef99e99c0a7
--- tog/tog.c
+++ tog/tog.c
@@ -4782,13 +4782,14 @@ create_diff(struct tog_diff_view_state *s)
 		err = got_diff_objects_as_blobs(&s->lines, &s->nlines,
 		    s->f1, s->f2, s->fd1, s->fd2, s->id1, s->id2,
 		    s->label1, s->label2, tog_diff_algo, s->diff_context,
-		    s->ignore_whitespace, s->force_text_diff, s->repo, s->f);
+		    s->ignore_whitespace, s->force_text_diff, 0, NULL, s->repo,
+		    s->f);
 		break;
 	case GOT_OBJ_TYPE_TREE:
 		err = got_diff_objects_as_trees(&s->lines, &s->nlines,
 		    s->f1, s->f2, s->fd1, s->fd2, s->id1, s->id2, NULL, "", "",
 		    tog_diff_algo, s->diff_context, s->ignore_whitespace,
-		    s->force_text_diff, s->repo, s->f);
+		    s->force_text_diff, 0, NULL, s->repo, s->f);
 		break;
 	case GOT_OBJ_TYPE_COMMIT: {
 		const struct got_object_id_queue *parent_ids;
@@ -4826,7 +4827,7 @@ create_diff(struct tog_diff_view_state *s)
 		err = got_diff_objects_as_commits(&s->lines, &s->nlines,
 		    s->f1, s->f2, s->fd1, s->fd2, s->id1, s->id2, NULL,
 		    tog_diff_algo, s->diff_context, s->ignore_whitespace,
-		    s->force_text_diff, s->repo, s->f);
+		    s->force_text_diff, 0, NULL, s->repo, s->f);
 		break;
 	}
 	default:


-- 
Mark Jamsek <fnc.bsdbox.org>
GPG: F2FF 13DE 6A06 C471 CA80  E6E2 2930 DC66 86EE CF68