Subject: [PATCH 2/3] builtin/diff-blob: add "--stdin" option

From: Justin Tobler <[email-masked]>
Date: Thu, 12 Dec 2024 22:23:11 -0600

There is not a way to generate multiple blob diffs from a single
process. Similar to git-diff-tree(1) with its "--stdin" option, it would
be useful if multiple blob pairs could be provided to git-diff-blob(1)
to compute blob diffs for.

Teach git-diff-blob(1) the "--stdin" option to allow a pair of blobs to
be read from each line of stdin instead of relying on the single blob
pair provided as arguments. When this option is specified, each valid
line of input computes a blob diff thus allowing multiple blob diffs in
a single process. A blob may be specified by its ID or a path-scoped
revision that resolve to a blob. When a path-scoped revision is used,
path and mode information is also extracted and presented in the
resulting diff header.

Signed-off-by: Justin Tobler <[email-masked]>
---
 Documentation/git-diff-blob.txt |  6 ++++
 builtin/diff-blob.c             | 64 +++++++++++++++++++++++++++++++++
 t/t4063-diff-blobs.sh           | 14 ++++++++
 3 files changed, 84 insertions(+)

diff --git a/Documentation/git-diff-blob.txt b/Documentation/git-diff-blob.txt
index 732992d1d7..f6ecd522fa 100644
--- a/Documentation/git-diff-blob.txt
+++ b/Documentation/git-diff-blob.txt
@@ -10,6 +10,7 @@ SYNOPSIS
 --------
 [verse]
 'git diff-blob' <blob> <blob>
+'git diff-blob' --stdin
 
 DESCRIPTION
 -----------
@@ -20,6 +21,11 @@ OPTIONS
 <blob>::
 	The id of a blob object or path-scoped revision that resolves to a blob.
 
+--stdin::
+	When `--stdin` is specified, the command does not take <blob> arguments
+	from the command line.  Instead, it reads lines containing two <blob>
+	from its standard input.  (Use a single space as separator.)
+
 include::pretty-formats.txt[]
 
 include::diff-format.txt[]
diff --git a/builtin/diff-blob.c b/builtin/diff-blob.c
index 7cfa4eb436..45edfdd979 100644
--- a/builtin/diff-blob.c
+++ b/builtin/diff-blob.c
@@ -4,9 +4,12 @@
 #include "diffcore.h"
 #include "gettext.h"
 #include "hash.h"
+#include "object-name.h"
 #include "object.h"
 #include "parse-options.h"
 #include "revision.h"
+#include "strbuf.h"
+#include "string-list.h"
 
 static void diff_blobs(struct object_array_entry *old_blob,
 		       struct object_array_entry *new_blob,
@@ -59,18 +62,66 @@ static void diff_blobs(struct object_array_entry *old_blob,
 	diff_flush(opts);
 }
 
+static void parse_blob_stdin(struct object_array *blob_pair,
+			     struct repository *repo, const char *name)
+{
+	int flags = GET_OID_BLOB | GET_OID_RECORD_PATH;
+	struct object_context oc;
+	struct object_id oid;
+	struct object *obj;
+
+	if (get_oid_with_context(repo, name, flags, &oid, &oc))
+		die("invalid object %s given", name);
+
+	obj = parse_object_or_die(&oid, name);
+	if (obj->type != OBJ_BLOB)
+		die("object %s is not a blob", name);
+
+	add_object_array_with_path(obj, name, blob_pair, oc.mode, oc.path);
+	object_context_release(&oc);
+}
+
+static void diff_blob_stdin(struct repository *repo, struct diff_options *opts)
+{
+	struct strbuf sb = STRBUF_INIT;
+	struct string_list_item *item;
+
+	while (strbuf_getline(&sb, stdin) != EOF) {
+		struct object_array blob_pair = OBJECT_ARRAY_INIT;
+		struct string_list list = STRING_LIST_INIT_NODUP;
+
+		if (string_list_split_in_place(&list, sb.buf, " ", -1) != 2)
+			die("two blobs not provided");
+
+		for_each_string_list_item(item, &list) {
+			parse_blob_stdin(&blob_pair, repo, item->string);
+		}
+
+		diff_blobs(&blob_pair.objects[0], &blob_pair.objects[1], opts);
+
+		string_list_clear(&list, 1);
+		object_array_clear(&blob_pair);
+	}
+
+	strbuf_release(&sb);
+}
+
 int cmd_diff_blob(int argc, const char **argv, const char *prefix,
 		  struct repository *repo)
 {
 	struct object_array_entry *old_blob, *new_blob;
 	struct rev_info revs;
+	int read_stdin = 0;
 	int ret;
 
 	const char * const usage[] = {
 		N_("git diff-blob <blob> <blob>"),
+		N_("git diff-blob --stdin"),
 		NULL
 	};
 	struct option options[] = {
+		OPT_BOOL(0, "stdin", &read_stdin,
+			N_("read blob pairs from stdin")),
 		OPT_END()
 	};
 
@@ -93,7 +144,20 @@ int cmd_diff_blob(int argc, const char **argv, const char *prefix,
 		revs.diffopt.output_format = DIFF_FORMAT_PATCH;
 
 	switch (revs.pending.nr) {
+	case 0:
+		if (!read_stdin)
+			usage_with_options(usage, options);
+
+		revs.diffopt.no_free = 1;
+		diff_blob_stdin(repo, &revs.diffopt);
+		revs.diffopt.no_free = 0;
+		diff_free(&revs.diffopt);
+
+		break;
 	case 2:
+		if (read_stdin)
+			usage_with_options(usage, options);
+
 		old_blob = &revs.pending.objects[0];
 		new_blob = &revs.pending.objects[1];
 
diff --git a/t/t4063-diff-blobs.sh b/t/t4063-diff-blobs.sh
index 23615565fe..d7785d4a6e 100755
--- a/t/t4063-diff-blobs.sh
+++ b/t/t4063-diff-blobs.sh
@@ -98,4 +98,18 @@ for cmd in $commands; do
 	'
 done
 
+test_expect_success 'diff-blob --stdin with blob ID' '
+	echo $sha1_one $sha1_two | git diff-blob --full-index --stdin >diff &&
+	check_index $sha1_one $sha1_two &&
+	check_paths $sha1_one $sha1_two &&
+	! grep mode diff
+'
+
+test_expect_success 'diff-blob --stdin with revision' '
+	echo HEAD:one HEAD:two | git diff-blob --full-index --stdin >diff &&
+	check_index $sha1_one $sha1_two &&
+	check_paths one two &&
+	check_mode 100644 100755
+'
+
 test_done
-- 
2.47.1