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

From:
Mark Jamsek <mark@jamsek.com>
Subject:
Re: got: add diffstat to got diffg
To:
Game of Trees <gameoftrees@openbsd.org>
Date:
Mon, 9 Jan 2023 22:38:37 +1100

Download raw body.

Thread
On 23-01-09 07:21PM, Mark Jamsek wrote:
> On 23-01-08 06:01PM, Stefan Sperling wrote:
> > On Mon, Jan 09, 2023 at 03:11:01AM +1100, Mark Jamsek wrote:
> > > Like the recent 'got log -d' addition, display a diffstat if the -d
> > > switch is passed to 'got diff'.
> > > 
> > 
> > But I am also wondering whether it would make sense to keep showing the
> > full diff even with -d, such that we display a diffstat above the full
> > diff, like we in tog diff. To me it seems like a diffstat is rarely
> > useful in isolation, but maybe I am missing some use case?
> 
> You've given me something to think about. I'm heading off for dinner and
> will consider it further. But I think you're right and 'got diff -d'
> should always output the diff too. Leave it with me and I'll work on
> this when I get back. Thanks, Stefan!

As mentioned on irc, you convinced me we should always display the diff
with -d, so the below diff makes this change from the previous revision.

We now output the diffstat first, which is followed by the diff. One
positive result is that, unlike tog and got log, we only compute the
diff once, so this improves performance a little. We also now need to
use a temp file, though, because we want the diffstat output first, but
we need the entire diff to get our field widths and totals. As such, we
can't print to stdout as we build the diff like we previously did. But
this isn't a big deal.

Sorry for the large diff; if you prefer, I can send a diff of changes
from the previous as it's a bit smaller. Just let me know!

diffstat refs/remotes/origin/main d2a6fcbdbf7aacd6f9e0e0c52a57cfe58ee786ca
 M  got/got.1           |    5+   1-
 M  got/got.c           |  160+  38-
 M  include/got_diff.h  |   13+   7-
 M  lib/diff.c          |  179+  69-
 M  lib/worktree.c      |    3+   3-
 M  tog/tog.c           |    4+   3-

6 files changed, 364 insertions(+), 121 deletions(-)

diff refs/remotes/origin/main d2a6fcbdbf7aacd6f9e0e0c52a57cfe58ee786ca
commit - 243a8f150f5f79c7530b415ca9df09bdbebd0cf9
commit + d2a6fcbdbf7aacd6f9e0e0c52a57cfe58ee786ca
blob - c86779cca9d917f301a8ac66e723282c6c0bcb57
blob + 3014446b87c1bbbc0c5271ec13deecb93f5ccc0c
--- 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,10 @@ option.
 Cannot be used together with the
 .Fl P
 option.
+.It Fl d
+Display diffstat of changes before the actual diff by annotating each file path
+or blob hash being diffed with the total number of lines added and removed.
+A summary line will display the total number of changes across all files.
 .It Fl P
 Interpret all arguments as paths only.
 This option can be used to resolve ambiguity in cases where paths
blob - 9a89d5b62f1635fb4b385534abdd217ee4a06611
blob + 35eaf23bb157cf65a7f6f79c315fd2f1b9206e5d
--- 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,8 @@ 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.diffstat = NULL;
 	arg.diff_algo = GOT_DIFF_ALGORITHM_PATIENCE;
 	arg.outfile = outfile;
 	arg.lines = NULL;
@@ -3891,7 +3894,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,
@@ -4164,6 +4167,31 @@ print_commit(struct got_commit_object *commit, struct 
 }
 
 static const struct got_error *
+print_diffstat(struct got_diffstat_cb_arg *dsa, struct got_pathlist_head *paths,
+    const char *header)
+{
+	struct got_pathlist_entry *pe;
+
+	if (header != NULL)
+		printf("%s\n", header);
+
+	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);
+	}
+	printf("\n%d file%s changed, %d insertions(+), %d deletions(-)\n\n",
+	    dsa->nfiles, dsa->nfiles > 1 ? "s" : "", dsa->ins, dsa->del);
+
+	if (fflush(stdout) != 0)
+		return got_error_from_errno("fflush");
+
+	return NULL;
+}
+
+static const struct got_error *
 print_commit(struct got_commit_object *commit, struct got_object_id *id,
     struct got_repository *repo, const char *path,
     struct got_pathlist_head *changed_paths, struct got_diffstat_cb_arg *dsa,
@@ -4237,31 +4265,18 @@ print_commit(struct got_commit_object *commit, struct 
 	} while (line);
 	free(logmsg0);
 
-	if (changed_paths) {
+	if (dsa && changed_paths) {
+		err = print_diffstat(dsa, changed_paths, NULL);
+		if (err)
+			goto done;
+	} 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 +4742,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 +4751,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,8 +4759,10 @@ struct print_diff_arg {
 	enum got_diff_algorithm diff_algo;
 	int ignore_whitespace;
 	int force_text_diff;
+	int show_diffstat;
 	FILE *f1;
 	FILE *f2;
+	FILE *outfile;
 };
 
 /*
@@ -4836,12 +4854,22 @@ print_diff(void *arg, unsigned char status, unsigned c
 		return got_error_from_errno("got_opentemp_truncate");
 
 	if (!a->header_shown) {
-		printf("diff %s%s\n", a->diff_staged ? "-s " : "",
-		    got_worktree_get_root_path(a->worktree));
-		printf("commit - %s\n", a->id_str);
-		printf("path + %s%s\n",
+		if (fprintf(a->outfile, "diff %s%s\n",
+		    a->diff_staged ? "-s " : "",
+		    got_worktree_get_root_path(a->worktree)) < 0) {
+			err = got_error_from_errno("fprintf");
+			goto done;
+		}
+		if (fprintf(a->outfile, "commit - %s\n", a->id_str) < 0) {
+			err = got_error_from_errno("fprintf");
+			goto done;
+		}
+		if (fprintf(a->outfile, "path + %s%s\n",
 		    got_worktree_get_root_path(a->worktree),
-		    a->diff_staged ? " (staged changes)" : "");
+		    a->diff_staged ? " (staged changes)" : "") < 0) {
+			err = got_error_from_errno("fprintf");
+			goto done;
+		}
 		a->header_shown = 1;
 	}
 
@@ -4874,7 +4902,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,
+		    a->outfile);
 		goto done;
 	}
 
@@ -4964,7 +4993,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, a->outfile);
 done:
 	if (fd1 != -1 && close(fd1) == -1 && err == NULL)
 		err = got_error_from_errno("close");
@@ -4981,6 +5011,30 @@ cmd_diff(int argc, char *argv[])
 }
 
 static const struct got_error *
+printfile(FILE *f)
+{
+	char	buf[8192];
+	size_t	r;
+
+	if (fseeko(f, 0L, SEEK_SET) == -1)
+		return got_error_from_errno("fseek");
+
+	for (;;) {
+		r = fread(buf, 1, sizeof(buf), f);
+		if (r == 0) {
+			if (ferror(f))
+				return got_error_from_errno("fread");
+			if (feof(f))
+				break;
+		}
+		if (fwrite(buf, 1, r, stdout) != r)
+			return got_ferror(stdout, GOT_ERR_IO);
+	}
+
+	return NULL;
+}
+
+static const struct got_error *
 cmd_diff(int argc, char *argv[])
 {
 	const struct got_error *error;
@@ -4993,17 +5047,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;
+	FILE *f1 = NULL, *f2 = NULL, *outfile = 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 +5067,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 +5084,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 +5150,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,
@@ -5150,6 +5217,12 @@ cmd_diff(int argc, char *argv[])
 		goto done;
 	}
 
+	outfile = got_opentemp();
+	if (outfile == NULL) {
+		error = got_error_from_errno("got_opentemp");
+		goto done;
+	}
+
 	if (ncommit_args == 0 && (ids[0] == NULL || ids[1] == NULL)) {
 		struct print_diff_arg arg;
 		char *id_str;
@@ -5189,12 +5262,33 @@ 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;
+		arg.outfile = outfile;
 
 		error = got_worktree_status(worktree, &paths, repo, 0,
 		    print_diff, &arg, check_cancelled, NULL);
 		free(id_str);
+		if (error)
+			goto done;
+
+		if (show_diffstat && dsa.nfiles > 0) {
+			char *header;
+
+			if (asprintf(&header, "diffstat %s%s",
+			    diff_staged ? "-s " : "",
+			    got_worktree_get_root_path(worktree)) == -1)
+				goto done;
+
+			error = print_diffstat(&dsa, &diffstat_paths, header);
+			free(header);
+			if (error)
+				goto done;
+		}
+
+		error = printfile(outfile);
 		goto done;
 	}
 
@@ -5329,24 +5423,45 @@ 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, outfile);
 		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, outfile);
 		break;
 	case GOT_OBJ_TYPE_COMMIT:
-		printf("diff %s %s\n", labels[0], labels[1]);
+		fprintf(outfile, "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, outfile);
 		break;
 	default:
 		error = got_error(GOT_ERR_OBJ_TYPE);
 	}
+	if (error)
+		goto done;
+
+	if (show_diffstat && dsa.nfiles > 0) {
+		char *header = NULL;
+
+		if (asprintf(&header, "diffstat %s %s",
+		    labels[0], labels[1]) == -1)
+			goto done;
+
+		error = print_diffstat(&dsa, &diffstat_paths, header);
+		free(header);
+		if (error)
+			goto done;
+	}
+
+	error = printfile(outfile);
+
 done:
 	free(labels[0]);
 	free(labels[1]);
@@ -5368,7 +5483,14 @@ done:
 	TAILQ_FOREACH(pe, &paths, entry)
 		free((char *)pe->path);
 	got_pathlist_free(&paths);
+	TAILQ_FOREACH(pe, &diffstat_paths, entry) {
+		free((char *)pe->path);
+		free(pe->data);
+	}
+	got_pathlist_free(&diffstat_paths);
 	got_ref_list_free(&refs);
+	if (outfile && fclose(outfile) == EOF && error == NULL)
+		error = got_error_from_errno("fclose");
 	if (f1 && fclose(f1) == EOF && error == NULL)
 		error = got_error_from_errno("fclose");
 	if (f2 && fclose(f2) == EOF && error == NULL)
blob - 18fe6a19ccc9b5b2450e313f81d9246ce51b630d
blob + 1d85a74b4cff17c1a1603795bdddac204025f5fc
--- 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,8 @@ 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;	/* Compute diffstat of changes */
+	struct got_diffstat_cb_arg *diffstat;
 	enum got_diff_algorithm diff_algo; /* Diffing algorithm to use. */
 
 	/*
@@ -204,7 +208,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 +231,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 +249,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 + 709e47798537a5f216664229cc751413b0a66942
--- 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;
@@ -170,11 +233,60 @@ 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 (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;
+		}
+	}
+
 	if (outfile) {
 		err = got_diffreg_output(lines, nlines, result,
 		    blob1 != NULL, blob2 != NULL,
@@ -208,7 +320,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,
+	    a->diffstat, a->outfile, a->diff_algo);
 }
 
 const struct got_error *
@@ -216,11 +329,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 +342,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];
@@ -277,6 +392,45 @@ done:
 			goto done;
 	}
 
+	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:
 	if (resultp && err == NULL)
 		*resultp = result;
@@ -292,11 +446,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 +766,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 +775,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 +832,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 +992,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 +1013,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,6 +1187,7 @@ 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)
 {
@@ -1106,6 +1214,8 @@ diff_objects_as_trees(struct got_diff_line **lines, si
 	arg.diff_context = diff_context;
 	arg.ignore_whitespace = ignore_whitespace;
 	arg.force_text_diff = force_text_diff;
+	arg.show_diffstat = show_diffstat;
+	arg.diffstat = dsa;
 	arg.outfile = outfile;
 	if (want_linemeta) {
 		arg.lines = *lines;
@@ -1114,14 +1224,12 @@ diff_objects_as_trees(struct got_diff_line **lines, si
 		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);
-	} else {
+	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);
+	else
 		err = diff_paths(tree1, tree2, f1, f2, fd1, fd2, paths, repo,
 		    got_diff_blob_output_unidiff, &arg);
-	}
 	if (want_linemeta) {
 		*lines = arg.lines; /* was likely re-allocated */
 		*nlines = arg.nlines;
@@ -1140,7 +1248,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 +1291,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 +1303,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 +1347,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