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

From:
Mark Jamsek <mark@jamsek.com>
Subject:
add diffstat option to got log and tog diff view
To:
Game of Trees <gameoftrees@openbsd.org>
Date:
Wed, 4 Jan 2023 03:11:35 +1100

Download raw body.

Thread
I often want to get a birdseye view of the changes in terms of total +/-
for a given commit. The below diff adds the -d flag to 'got log' and the
'D' keymap to tog diff to display the number of lines added/removed for
each file changed in the commit, and a summary line showing the total
change across all files. But the implementation looks forward to making
the diffstat available to 'got diff' too.

Although we have to compute the diff twice because the file changeset is
output before the diff is computed and we need the diff to display the
diffstat with the changeset, the performance impact is negligible for
the average commit because we don't pay the high cost of formatting any
output, we just want the diffreg_result so we can sum the changes. At
the high end, the average increase in a 675k line diff is approximately
33% (i.e., 1.5 -> 2s), but for typical commits it's barely perceptible.

That said, if it's a concern, we could probably look at holding onto the
diff result for the actual diff output, in which case there'd be no
extra cost.

I opted to keep the diffstat output simple; for example:

  -----------------------------------------------
  commit 2be11cdeee0c210d3273b3724e924de0422d9f67
  from: Omar Polo <op@omarpolo.com>
  date: Tue Jan  3 14:38:01 2023 UTC

   gotd: allow to express timeouts using minutes/hours

   This allows to use a suffix to indicate the unit of measure, such as
   "1h" for one hour or "30m" for 30 minutes.  The suffix "s" for seconds
   is also accepted for completeness.

   ok stsp

   M  gotd/gotd.conf.5  |  14+  2-
   M  gotd/parse.y      |  51+  0-

  2 files changed, 65 insertions(+), 2 deletions(-)

but we could plot a scaled histogram like git's --stat, or maybe make it
toggleable if that's something we want.

diff refs/heads/main refs/heads/ds
commit - 71cd355cb2711ad528715bbbb5b41be7c26ace2a
commit + 8f7ae5e24bf6a51168d6747cfeb97c1e2965f748
blob - e754777a9202afca9a7b80dfd634b3c605d8a5cd
blob + d7f14e7606560bbef80113d081dca440bc8ef372
--- got/got.1
+++ got/got.1
@@ -829,6 +829,11 @@ if invoked in a work tree, or to the repository's HEAD
 automatically, provided the abbreviation is unique.
 If this option is not specified, default to the work tree's current branch
 if invoked in a work tree, or to the repository's HEAD reference.
+.It Fl d
+Display diffstat of changes introduced in the commit.
+Cannot be used with the
+.Fl s
+option.
 .It Fl l Ar N
 Limit history traversal to a given number of commits.
 If this option is not specified, a default limit value of zero is used,
blob - ad0fec63ec6030f40937e7bf6836769359bb55ba
blob + aaf34c7a1dabde267171e1e19cd7f49b72525be1
--- got/got.c
+++ got/got.c
@@ -3753,13 +3753,42 @@ get_changed_paths(struct got_pathlist_head *paths,
 
 static const struct got_error *
 get_changed_paths(struct got_pathlist_head *paths,
-    struct got_commit_object *commit, struct got_repository *repo)
+    struct got_commit_object *commit, struct got_repository *repo,
+    struct got_diffstat_cb_arg *dsa)
 {
 	const struct got_error *err = NULL;
 	struct got_object_id *tree_id1 = NULL, *tree_id2 = NULL;
 	struct got_tree_object *tree1 = NULL, *tree2 = NULL;
 	struct got_object_qid *qid;
+	got_diff_blob_cb cb = got_diff_tree_collect_changed_paths;
+	FILE *f1 = NULL, *f2 = NULL;
+	int fd1 = -1, fd2 = -1;
 
+	if (dsa) {
+		cb = got_diff_tree_compute_diffstat;
+
+		f1 = got_opentemp();
+		if (f1 == NULL) {
+			err = got_error_from_errno("got_opentemp");
+			goto done;
+		}
+		f2 = got_opentemp();
+		if (f2 == NULL) {
+			err = got_error_from_errno("got_opentemp");
+			goto done;
+		}
+		fd1 = got_opentempfd();
+		if (fd1 == -1) {
+			err = got_error_from_errno("got_opentempfd");
+			goto done;
+		}
+		fd2 = got_opentempfd();
+		if (fd2 == -1) {
+			err = got_error_from_errno("got_opentempfd");
+			goto done;
+		}
+	}
+
 	qid = STAILQ_FIRST(got_object_commit_get_parent_ids(commit));
 	if (qid != NULL) {
 		struct got_commit_object *pcommit;
@@ -3789,13 +3818,21 @@ get_changed_paths(struct got_pathlist_head *paths,
 	if (err)
 		goto done;
 
-	err = got_diff_tree(tree1, tree2, NULL, NULL, -1, -1, "", "", repo,
-	    got_diff_tree_collect_changed_paths, paths, 0);
+	err = got_diff_tree(tree1, tree2, f1, f2, fd1, fd2, "", "", repo,
+	    cb, dsa ? (void *)dsa : paths, dsa ? 1 : 0);
 done:
 	if (tree1)
 		got_object_tree_close(tree1);
 	if (tree2)
 		got_object_tree_close(tree2);
+	if (fd1 != -1 && close(fd1) == -1 && err == NULL)
+		err = got_error_from_errno("close");
+	if (fd2 != -1 && close(fd2) == -1 && err == NULL)
+		err = got_error_from_errno("close");
+	if (f1 && fclose(f1) == EOF && err == NULL)
+		err = got_error_from_errno("fclose");
+	if (f2 && fclose(f2) == EOF && err == NULL)
+		err = got_error_from_errno("fclose");
 	free(tree_id1);
 	return err;
 }
@@ -4129,9 +4166,9 @@ print_commit(struct got_commit_object *commit, struct 
 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, int show_patch,
-    int diff_context, struct got_reflist_object_id_map *refs_idmap,
-    const char *custom_refs_str)
+    struct got_pathlist_head *changed_paths, struct got_diffstat_cb_arg *dsa,
+    int show_patch, int diff_context,
+    struct got_reflist_object_id_map *refs_idmap, const char *custom_refs_str)
 {
 	const struct got_error *err = NULL;
 	char *id_str, *datestr, *logmsg0, *logmsg, *line;
@@ -4202,10 +4239,29 @@ print_commit(struct got_commit_object *commit, struct 
 
 	if (changed_paths) {
 		struct got_pathlist_entry *pe;
+
 		TAILQ_FOREACH(pe, changed_paths, entry) {
 			struct got_diff_changed_path *cp = pe->data;
-			printf(" %c  %s\n", cp->status, pe->path);
+			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);
 		}
+		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) {
@@ -4225,8 +4281,8 @@ print_commits(struct got_object_id *root_id, struct go
 static const struct got_error *
 print_commits(struct got_object_id *root_id, struct got_object_id *end_id,
     struct got_repository *repo, const char *path, int show_changed_paths,
-    int show_patch, const char *search_pattern, int diff_context, int limit,
-    int log_branches, int reverse_display_order,
+    int show_diffstat, int show_patch, const char *search_pattern,
+    int diff_context, int limit, int log_branches, int reverse_display_order,
     struct got_reflist_object_id_map *refs_idmap, int one_line,
     FILE *tmpfile)
 {
@@ -4256,6 +4312,8 @@ print_commits(struct got_object_id *root_id, struct go
 		goto done;
 	for (;;) {
 		struct got_object_id id;
+		struct got_diffstat_cb_arg dsa = { 0, 0, 0, 0, 0, 0,
+		    &changed_paths, 0, 0, GOT_DIFF_ALGORITHM_PATIENCE };
 
 		if (sigint_received || sigpipe_received)
 			break;
@@ -4272,8 +4330,10 @@ print_commits(struct got_object_id *root_id, struct go
 		if (err)
 			break;
 
-		if (show_changed_paths && !reverse_display_order) {
-			err = get_changed_paths(&changed_paths, commit, repo);
+		if ((show_changed_paths || show_diffstat) &&
+		    !reverse_display_order) {
+			err = get_changed_paths(&changed_paths, commit, repo,
+			    show_diffstat ? &dsa : NULL);
 			if (err)
 				break;
 		}
@@ -4317,8 +4377,10 @@ print_commits(struct got_object_id *root_id, struct go
 				    repo, refs_idmap);
 			else
 				err = print_commit(commit, &id, repo, path,
-				    show_changed_paths ? &changed_paths : NULL,
-				    show_patch, diff_context, refs_idmap, NULL);
+				    (show_changed_paths || show_diffstat) ?
+				    &changed_paths : NULL,
+				    show_diffstat ? &dsa : NULL, show_patch,
+				    diff_context, refs_idmap, NULL);
 			got_object_commit_close(commit);
 			if (err)
 				break;
@@ -4335,13 +4397,16 @@ print_commits(struct got_object_id *root_id, struct go
 	}
 	if (reverse_display_order) {
 		STAILQ_FOREACH(qid, &reversed_commits, entry) {
+			struct got_diffstat_cb_arg dsa = { 0, 0, 0, 0, 0, 0,
+			    &changed_paths, 0, 0, GOT_DIFF_ALGORITHM_PATIENCE };
+
 			err = got_object_open_as_commit(&commit, repo,
 			    &qid->id);
 			if (err)
 				break;
-			if (show_changed_paths) {
+			if (show_changed_paths || show_diffstat) {
 				err = get_changed_paths(&changed_paths,
-				    commit, repo);
+				    commit, repo, show_diffstat ? &dsa : NULL);
 				if (err)
 					break;
 			}
@@ -4350,8 +4415,10 @@ print_commits(struct got_object_id *root_id, struct go
 				    repo, refs_idmap);
 			else
 				err = print_commit(commit, &qid->id, repo, path,
-				    show_changed_paths ? &changed_paths : NULL,
-				    show_patch, diff_context, refs_idmap, NULL);
+				    (show_changed_paths || show_diffstat) ?
+				    &changed_paths : NULL,
+				    show_diffstat ? &dsa : NULL, show_patch,
+				    diff_context, refs_idmap, NULL);
 			got_object_commit_close(commit);
 			if (err)
 				break;
@@ -4416,7 +4483,7 @@ cmd_log(int argc, char *argv[])
 	const char *search_pattern = NULL;
 	int diff_context = -1, ch;
 	int show_changed_paths = 0, show_patch = 0, limit = 0, log_branches = 0;
-	int reverse_display_order = 0, one_line = 0;
+	int show_diffstat = 0, reverse_display_order = 0, one_line = 0;
 	const char *errstr;
 	struct got_reflist_head refs;
 	struct got_reflist_object_id_map *refs_idmap = NULL;
@@ -4434,7 +4501,7 @@ cmd_log(int argc, char *argv[])
 
 	limit = get_default_log_limit();
 
-	while ((ch = getopt(argc, argv, "bC:c:l:PpRr:S:sx:")) != -1) {
+	while ((ch = getopt(argc, argv, "bC:c:dl:PpRr:S:sx:")) != -1) {
 		switch (ch) {
 		case 'b':
 			log_branches = 1;
@@ -4446,6 +4513,9 @@ cmd_log(int argc, char *argv[])
 				errx(1, "number of context lines is %s: %s",
 				    errstr, optarg);
 			break;
+		case 'd':
+			show_diffstat = 1;
+			break;
 		case 'c':
 			start_commit = optarg;
 			break;
@@ -4494,8 +4564,8 @@ cmd_log(int argc, char *argv[])
 	else if (!show_patch)
 		errx(1, "-C requires -p");
 
-	if (one_line && (show_patch || show_changed_paths))
-		errx(1, "cannot use -s with -p or -P");
+	if (one_line && (show_patch || show_changed_paths || show_diffstat))
+		errx(1, "cannot use -s with -d, -p or -P");
 
 	cwd = getcwd(NULL, 0);
 	if (cwd == NULL) {
@@ -4626,9 +4696,9 @@ cmd_log(int argc, char *argv[])
 	}
 
 	error = print_commits(start_id, end_id, repo, path ? path : "",
-	    show_changed_paths, show_patch, search_pattern, diff_context,
-	    limit, log_branches, reverse_display_order, refs_idmap, one_line,
-	    tmpfile);
+	    show_changed_paths, show_diffstat, show_patch, search_pattern,
+	    diff_context, limit, log_branches, reverse_display_order,
+	    refs_idmap, one_line, tmpfile);
 done:
 	free(path);
 	free(repo_path);
@@ -9873,7 +9943,7 @@ print_backup_ref(const char *branch_name, const char *
 	if (asprintf(&custom_refs_str, "formerly %s", branch_name) == -1)
 		return got_error_from_errno("asprintf");
 
-	err = print_commit(old_commit, old_commit_id, repo, NULL, NULL,
+	err = print_commit(old_commit, old_commit_id, repo, NULL, NULL, NULL,
 	    0, 0, refs_idmap, custom_refs_str);
 	if (err)
 		goto done;
blob - 26617c2f087a790bbb5f19d32d57f83246fcb532
blob + 34eea601c7679970c5f4d36a09b48c71943808f0
--- include/got_diff.h
+++ include/got_diff.h
@@ -155,6 +155,8 @@ struct got_diff_changed_path {
  * entries on the path list.
  */
 struct got_diff_changed_path {
+	uint32_t add;	/* number of lines added */
+	uint32_t rm;	/* number of lines removed */
 	/*
 	 * The modification status of this path. It can be GOT_STATUS_ADD,
 	 * GOT_STATUS_DELETE, GOT_STATUS_MODIFY, or GOT_STATUS_MODE_CHANGE.
@@ -166,6 +168,23 @@ const struct got_error *got_diff_tree_collect_changed_
     struct got_object_id *, struct got_object_id *,
     const char *, const char *, mode_t, mode_t, struct got_repository *);
 
+struct got_diffstat_cb_arg {
+	size_t max_path_len;
+	uint32_t ins;
+	uint32_t del;
+	int add_cols;
+	int rm_cols;
+	int nfiles;
+	struct got_pathlist_head *paths;
+	int ignore_ws;
+	int force_text;
+	enum got_diff_algorithm diff_algo;
+};
+const struct got_error *got_diff_tree_compute_diffstat(void *,
+    struct got_blob_object *, struct got_blob_object *, FILE *, FILE *,
+    struct got_object_id *, struct got_object_id *, const char *, const char *,
+    mode_t, mode_t, struct got_repository *);
+
 /*
  * Diff two objects, assuming both objects are blobs. Two const char * diff
  * header labels may be provided which will be used to identify each blob in
blob - 58baf2baa359d6bb49f1d7c1b7bd6af3f15bbb36
blob + d9e4fda12b168d70f0a85a5a60436252147f53b7
--- lib/diff.c
+++ lib/diff.c
@@ -38,6 +38,10 @@ static const struct got_error *
 #include "got_lib_inflate.h"
 #include "got_lib_object.h"
 
+#ifndef MAX
+#define	MAX(_a,_b) ((_a) > (_b) ? (_a) : (_b))
+#endif
+
 static const struct got_error *
 add_line_metadata(struct got_diff_line **lines, size_t *nlines,
     off_t off, uint8_t type)
@@ -606,7 +610,137 @@ const struct got_error *
 	    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,
+    struct got_object_id *id1, struct got_object_id *id2,
+    const char *label1, const char *label2,
+    mode_t mode1, mode_t mode2, struct got_repository *repo)
+{
+	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)
+		return got_error_from_errno("malloc");
+
+	change = malloc(sizeof(*change));
+	if (change == NULL) {
+		err = got_error_from_errno("malloc");
+		goto done;
+	}
+
+	change->add = 0;
+	change->rm = 0;
+	change->status = GOT_STATUS_NO_CHANGE;
+	if (id1 == NULL)
+		change->status = GOT_STATUS_ADD;
+	else if (id2 == NULL)
+		change->status = GOT_STATUS_DELETE;
+	else {
+		if (got_object_id_cmp(id1, id2) != 0)
+			change->status = GOT_STATUS_MODIFY;
+		else if (mode1 != mode2)
+			change->status = GOT_STATUS_MODE_CHANGE;
+	}
+
+	if (f1) {
+		err = got_opentemp_truncate(f1);
+		if (err)
+			goto done;
+	}
+	if (f2) {
+		err = got_opentemp_truncate(f2);
+		if (err)
+			goto done;
+	}
+
+	if (blob1) {
+		err = got_object_blob_dump_to_file(NULL, NULL, NULL, f1,
+		    blob1);
+		if (err)
+			goto done;
+	}
+	if (blob2) {
+		err = got_object_blob_dump_to_file(NULL, NULL, NULL, f2,
+		    blob2);
+		if (err)
+			goto done;
+	}
+
+	err = got_diffreg(&result, f1, f2, a->diff_algo, a->ignore_ws,
+	    a->force_text);
+	if (err)
+		goto done;
+
+	for (i = 0, r = result->result; i < r->chunks.len; ++i) {
+		struct diff_chunk *c;
+		int flags = (r->left->atomizer_flags | r->right->atomizer_flags);
+		int isbin = (flags & DIFF_ATOMIZER_FOUND_BINARY_DATA);
+		int clc, crc;
+
+		if (!isbin || a->force_text) {
+			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;
+
+		free_err = got_diffreg_result_free(result);
+		if (free_err && err == NULL)
+			err = free_err;
+	}
+	if (err) {
+		free(path);
+		free(change);
+	}
+	return err;
+}
+
+const struct got_error *
 got_diff_tree_collect_changed_paths(void *arg, struct got_blob_object *blob1,
     struct got_blob_object *blob2, FILE *f1, FILE *f2,
     struct got_object_id *id1, struct got_object_id *id2,
blob - 03eeb1401485cf8cdcd6d8b24b43fdf4f2e44907
blob + b02b30131130618afb77a5ef237ccb192037e43f
--- tog/tog.1
+++ tog/tog.1
@@ -303,6 +303,8 @@ detected.
 .It Cm a
 Toggle treatment of file contents as ASCII text even if binary data was
 detected.
+.It Cm D
+Display diffstat of changes introduced in the commit.
 .It Cm Down-arrow, j, Ctrl-n
 Scroll down N lines (default: 1).
 .It Cm Up-arrow, k, Ctrl-p
blob - 2f1436265914a5af2afa20029647cb7236df2288
blob + 5752a69c69b09e1feed70bcec38e48ccc7d8cd43
--- tog/tog.c
+++ tog/tog.c
@@ -338,6 +338,7 @@ struct tog_diff_view_state {
 	int diff_context;
 	int ignore_whitespace;
 	int force_text_diff;
+	int show_diffstat;
 	struct got_repository *repo;
 	struct got_diff_line *lines;
 	size_t nlines;
@@ -568,6 +569,7 @@ struct tog_help_view_state {
 	KEY_("A", "Toggle between Myers and Patience diff algorithm"), \
 	KEY_("a", "Toggle treatment of file as ASCII irrespective of binary" \
 	    " data"), \
+	KEY_("D", "Display diffstat of changes introduced in the commit"), \
 	KEY_("(", "Go to the previous file in the diff"), \
 	KEY_(")", "Go to the next file in the diff"), \
 	KEY_("{", "Go to the previous hunk in the diff"), \
@@ -4457,13 +4459,42 @@ get_changed_paths(struct got_pathlist_head *paths,
 
 static const struct got_error *
 get_changed_paths(struct got_pathlist_head *paths,
-    struct got_commit_object *commit, struct got_repository *repo)
+    struct got_commit_object *commit, struct got_repository *repo,
+    struct got_diffstat_cb_arg *dsa)
 {
 	const struct got_error *err = NULL;
 	struct got_object_id *tree_id1 = NULL, *tree_id2 = NULL;
 	struct got_tree_object *tree1 = NULL, *tree2 = NULL;
 	struct got_object_qid *qid;
+	got_diff_blob_cb cb = got_diff_tree_collect_changed_paths;
+	FILE *f1 = NULL, *f2 = NULL;
+	int fd1 = -1, fd2 = -1;
 
+	if (dsa) {
+		cb = got_diff_tree_compute_diffstat;
+
+		f1 = got_opentemp();
+		if (f1 == NULL) {
+			err = got_error_from_errno("got_opentemp");
+			goto done;
+		}
+		f2 = got_opentemp();
+		if (f2 == NULL) {
+			err = got_error_from_errno("got_opentemp");
+			goto done;
+		}
+		fd1 = got_opentempfd();
+		if (fd1 == -1) {
+			err = got_error_from_errno("got_opentempfd");
+			goto done;
+		}
+		fd2 = got_opentempfd();
+		if (fd2 == -1) {
+			err = got_error_from_errno("got_opentempfd");
+			goto done;
+		}
+	}
+
 	qid = STAILQ_FIRST(got_object_commit_get_parent_ids(commit));
 	if (qid != NULL) {
 		struct got_commit_object *pcommit;
@@ -4493,13 +4524,21 @@ get_changed_paths(struct got_pathlist_head *paths,
 	if (err)
 		goto done;
 
-	err = got_diff_tree(tree1, tree2, NULL, NULL, -1, -1, "", "", repo,
-	    got_diff_tree_collect_changed_paths, paths, 0);
+	err = got_diff_tree(tree1, tree2, f1, f2, fd1, fd2, "", "", repo,
+	    cb, dsa ? (void *)dsa : paths, dsa ? 1 : 0);
 done:
 	if (tree1)
 		got_object_tree_close(tree1);
 	if (tree2)
 		got_object_tree_close(tree2);
+	if (fd1 != -1 && close(fd1) == -1 && err == NULL)
+		err = got_error_from_errno("close");
+	if (fd2 != -1 && close(fd2) == -1 && err == NULL)
+		err = got_error_from_errno("close");
+	if (f1 && fclose(f1) == EOF && err == NULL)
+		err = got_error_from_errno("fclose");
+	if (f2 && fclose(f2) == EOF && err == NULL)
+		err = got_error_from_errno("fclose");
 	free(tree_id1);
 	return err;
 }
@@ -4524,7 +4563,8 @@ write_commit_info(struct got_diff_line **lines, size_t
 static const struct got_error *
 write_commit_info(struct got_diff_line **lines, size_t *nlines,
     struct got_object_id *commit_id, struct got_reflist_head *refs,
-    struct got_repository *repo, FILE *outfile)
+    struct got_repository *repo, int show_diffstat, int ignore_ws,
+    int force_text_diff, FILE *outfile)
 {
 	const struct got_error *err = NULL;
 	char datebuf[26], *datestr;
@@ -4535,6 +4575,8 @@ write_commit_info(struct got_diff_line **lines, size_t
 	char *refs_str = NULL;
 	struct got_pathlist_head changed_paths;
 	struct got_pathlist_entry *pe;
+	struct got_diffstat_cb_arg dsa = { 0, 0, 0, 0, 0, 0, &changed_paths,
+	    ignore_ws, force_text_diff, tog_diff_algo };
 	off_t outoff = 0;
 	int n;
 
@@ -4651,12 +4693,28 @@ write_commit_info(struct got_diff_line **lines, size_t
 			goto done;
 	}
 
-	err = get_changed_paths(&changed_paths, commit, repo);
+	err = get_changed_paths(&changed_paths, commit, repo,
+	    show_diffstat ? &dsa : NULL);
 	if (err)
 		goto done;
+
 	TAILQ_FOREACH(pe, &changed_paths, entry) {
 		struct got_diff_changed_path *cp = pe->data;
-		n = fprintf(outfile, "%c  %s\n", cp->status, pe->path);
+		char *stat = NULL;
+
+		if (show_diffstat) {
+			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;
+			}
+		}
+		n = fprintf(outfile, "%c  %s%s\n", cp->status, pe->path,
+		    stat ? stat : "");
+		free(stat);
 		if (n < 0) {
 			err = got_error_from_errno("fprintf");
 			goto done;
@@ -4670,6 +4728,27 @@ write_commit_info(struct got_diff_line **lines, size_t
 		free(pe->data);
 	}
 
+	if (show_diffstat) {
+		fputc('\n', outfile);
+		outoff++;
+		err = add_line_metadata(lines, nlines, outoff,
+		    GOT_DIFF_LINE_NONE);
+		if (err)
+			goto done;
+		n = fprintf(outfile,
+		    "%d file%s changed, %d insertions(+), %d deletions(-)\n",
+		    dsa.nfiles, dsa.nfiles > 1 ? "s" : "", dsa.ins, dsa.del);
+		if (n < 0) {
+			err = got_error_from_errno("fprintf");
+			goto done;
+		}
+		outoff += n;
+		err = add_line_metadata(lines, nlines, outoff,
+		    GOT_DIFF_LINE_NONE);
+		if (err)
+			goto done;
+	}
+
 	fputc('\n', outfile);
 	outoff++;
 	err = add_line_metadata(lines, nlines, outoff, GOT_DIFF_LINE_NONE);
@@ -4744,7 +4823,8 @@ create_diff(struct tog_diff_view_state *s)
 		/* Show commit info if we're diffing to a parent/root commit. */
 		if (s->id1 == NULL) {
 			err = write_commit_info(&s->lines, &s->nlines, s->id2,
-			    refs, s->repo, s->f);
+			    refs, s->repo, s->show_diffstat,
+			    s->ignore_whitespace, s->force_text_diff, s->f);
 			if (err)
 				goto done;
 		} else {
@@ -4753,7 +4833,9 @@ create_diff(struct tog_diff_view_state *s)
 				if (got_object_id_cmp(s->id1, &pid->id) == 0) {
 					err = write_commit_info(&s->lines,
 					    &s->nlines, s->id2, refs, s->repo,
-					    s->f);
+					    s->show_diffstat,
+					    s->ignore_whitespace,
+					    s->force_text_diff, s->f);
 					if (err)
 						goto done;
 					break;
@@ -5209,11 +5291,14 @@ input_diff_view(struct tog_view **new_view, struct tog
 		if (view->x <= 0)
 			view->count = 0;
 		break;
+	case 'D':
 	case 'a':
 	case 'w':
-		if (ch == 'a')
+		if (ch == 'D')
+			s->show_diffstat = !s->show_diffstat;
+		else if (ch == 'a')
 			s->force_text_diff = !s->force_text_diff;
-		if (ch == 'w')
+		else if (ch == 'w')
 			s->ignore_whitespace = !s->ignore_whitespace;
 		err = reset_diff_view(view);
 		break;

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