Volume XXII, number 280Wednesday, October 7, 2026Latest message 1 hour ago

The Git List

News and archive of git@vger.kernel.org, since April 2005

patch, 2 partsname-rev: learn --format=<pretty>

45 messages between Mar 13, 2026 and May 11, 2026, from kristofferhaugsbakk@fastmail.com, Junio C Hamano, Kristoffer Haugsbakk, D. Ben Knoble, Phillip Wood.

Plain Markdown or JSON for tools and agents. Diffs are folded; open one to read it.

kristofferhaugsbakk@fastmail.comMar 13, 2026, 16:03 UTC on lore
From: Kristoffer Haugsbakk <code@khaugsbakk.name>
Topic name: kh/name-rev-pretty-format

Topic summary: Teach git-name-rev(1) a mode to pretty format revisions instead of outputting symbolic names.

(See the second patch for details.)
The first patch is just for `CodingGuidelines`. Unrelated.

I have gone through three approaches: pretty print, `log-tree: log_tree_commit`, and pretty print again. At first I was confused when using `pretty_print_commit` because I couldn’t seem to get the same output as git-log(1):

    # for git-log(1)
    git log --format=<pretty>
    # for git-name-rev(1)
    git rev-list HEAD |
        git name-rev --format=<pretty> --annotate-stdin
Because there were some minor differences for a few things I tried:

• fuller: log has the `commit <commit>` header; pretty does not • oneline: log has the oid; pretty does not

Then I tried `log_tree_commit`. But then I got some things that I didn’t want. This function also didn’t fit in with the git-name-rev(1) processing since it just dumps straight to standard out instead of allowing you to accumulate things in a scratch buffer (strbuf). So then I went back to the `pretty_print_commit` approach.

Notes are handled by reading the display refs. I can imagine that it would be better for this command to use `--[no-]notes` arguments like the ones that git-log(1) has for explicit control (without using env. variables).

[1/2] name-rev: wrap both blocks in braces [2/2] name-rev: learn --format=<pretty>

 Documentation/git-name-rev.adoc |  9 +++-
 builtin/name-rev.c              | 85 ++++++++++++++++++++++++++++-----
 t/t6120-describe.sh             | 58 ++++++++++++++++++++++
 3 files changed, 139 insertions(+), 13 deletions(-)
base-commit: 67006b9db8b772423ad0706029286096307d2567
-- 
2.53.0.32.gf6228eaf9cc
kristofferhaugsbakk@fastmail.comMar 13, 2026, 16:03 UTC in reply to kristofferhaugsbakk@fastmail.com on lore

[PATCH 1/2] name-rev: wrap both blocks in braces

From: Kristoffer Haugsbakk <code@khaugsbakk.name>
See `CodingGuidelines`:
    - When there are multiple arms to a conditional and some of them
      require braces, enclose even a single line block in braces for
      consistency. [...]
Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
---
 builtin/name-rev.c | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)
Show changes to builtin/name-rev.c +5 −5
diff --git a/builtin/name-rev.c b/builtin/name-rev.c
index 6188cf98ce0..9d2774f3723 100644
--- a/builtin/name-rev.c
+++ b/builtin/name-rev.c
@@ -466,9 +466,9 @@ static const char *get_rev_name(const struct object *o, struct strbuf *buf)
 	if (!n)
 		return NULL;
 
-	if (!n->generation)
+	if (!n->generation) {
 		return n->tip_name;
-	else {
+	} else {
 		strbuf_reset(buf);
 		strbuf_addstr(buf, n->tip_name);
 		strbuf_strip_suffix(buf, "^0");
@@ -516,10 +516,10 @@ static void name_rev_line(char *p, struct name_ref_data *data)
 
 	for (p_start = p; *p; p++) {
 #define ishex(x) (isdigit((x)) || ((x) >= 'a' && (x) <= 'f'))
-		if (!ishex(*p))
+		if (!ishex(*p)) {
 			counter = 0;
-		else if (++counter == hexsz &&
-			 !ishex(*(p+1))) {
+		} else if (++counter == hexsz &&
+			   !ishex(*(p + 1))) {
 			struct object_id oid;
 			const char *name = NULL;
 			char c = *(p+1);
-- 
2.53.0.32.gf6228eaf9cc
kristofferhaugsbakk@fastmail.comMar 13, 2026, 16:03 UTC in reply to kristofferhaugsbakk@fastmail.com on lore

[PATCH 2/2] name-rev: learn --format=<pretty>

From: Kristoffer Haugsbakk <code@khaugsbakk.name>

Teach git-name-rev(1) to format the given revisions instead of creating symbolic names.

Sometimes you want to format commits. Most of the time you’re walking the graph, e.g. getting a range of commits like `master..topic`. That’s a job for git-log(1).

But sometimes you might want to format commits that you encounter on demand:

• Full hashes in running text that you might want to pretty-print
• git-last-modified(1) outputs full hashes that you can do the same with
• git-cherry(1) has `-v` for commit subject, but maybe you want
  something else?
But now you can’t use git-log(1), git-show(1), or git-rev-list(1):
• You can’t feed commits piecemeal to these commands, one input for one
  output; they block until standard in is closed
• You can’t feed a list of possibly duplicate commits, like the output
  of git-last-modified(1); they effectively deduplicate the output

Beyond these two points there’s also the input massage problem: you cannot feed mixed input (revisions mixed with arbitrary text).

One might hope that git-cat-file(1) can save us. But it doesn’t support pretty formats.

But there is one command that already both handles revisions as arguments, revisions on standard input, and even revisions mixed in with arbitrary text. Namely git-name-rev(1).

Teach it to work in a format mode where the output for each revision is the pretty output (implies `--name-only`). This can be used to format any revision expression when given as arguments, and all full commit hashes in running text on stdin.

Just bring the hashes (to the pipeline). We will pretty print them.
Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
---
 Documentation/git-name-rev.adoc |  9 +++-
 builtin/name-rev.c              | 75 ++++++++++++++++++++++++++++++---
 t/t6120-describe.sh             | 58 +++++++++++++++++++++++++
 3 files changed, 134 insertions(+), 8 deletions(-)
Show changes to 3 files +134 −8

Documentation/git-name-rev.adoc, builtin/name-rev.c, t/t6120-describe.sh

diff --git a/Documentation/git-name-rev.adoc b/Documentation/git-name-rev.adoc
index d4f1c4d5945..8f050cd4763 100644
--- a/Documentation/git-name-rev.adoc
+++ b/Documentation/git-name-rev.adoc
@@ -9,7 +9,7 @@ git-name-rev - Find symbolic names for given revs
 SYNOPSIS
 --------
 [verse]
-'git name-rev' [--tags] [--refs=<pattern>]
+'git name-rev' [--tags] [--refs=<pattern>] [--format=<pretty>]
 	       ( --all | --annotate-stdin | <commit-ish>... )
 
 DESCRIPTION
@@ -21,6 +21,13 @@ format parsable by 'git rev-parse'.
 OPTIONS
 -------
 
+--format=<pretty>::
+--no-format::
+	Format revisions instead of outputting symbolic names. The
+	default is `--no-format`.
++
+Implies `--name-only`.
+
 --tags::
 	Do not use branch names, but only tags to name the commits
 
diff --git a/builtin/name-rev.c b/builtin/name-rev.c
index 9d2774f3723..30d981104c6 100644
--- a/builtin/name-rev.c
+++ b/builtin/name-rev.c
@@ -18,6 +18,9 @@
 #include "commit-graph.h"
 #include "wildmatch.h"
 #include "mem-pool.h"
+#include "pretty.h"
+#include "revision.h"
+#include "notes.h"
 
 /*
  * One day.  See the 'name a rev shortly after epoch' test in t6120 when
@@ -33,6 +36,11 @@ struct rev_name {
 	int from_tag;
 };
 
+struct pretty_format {
+	struct pretty_print_context ctx;
+	struct userformat_want want;
+};
+
 define_commit_slab(commit_rev_name, struct rev_name);
 
 static timestamp_t generation_cutoff = GENERATION_NUMBER_INFINITY;
@@ -454,7 +462,9 @@ static const char *get_exact_ref_match(const struct object *o)
 }
 
 /* may return a constant string or use "buf" as scratch space */
-static const char *get_rev_name(const struct object *o, struct strbuf *buf)
+static const char *get_rev_name(const struct object *o,
+				struct pretty_format *format_ctx,
+				struct strbuf *buf)
 {
 	struct rev_name *n;
 	const struct commit *c;
@@ -462,6 +472,25 @@ static const char *get_rev_name(const struct object *o, struct strbuf *buf)
 	if (o->type != OBJ_COMMIT)
 		return get_exact_ref_match(o);
 	c = (const struct commit *) o;
+
+	if (format_ctx) {
+		strbuf_reset(buf);
+
+		if (format_ctx->want.notes) {
+			struct strbuf notebuf = STRBUF_INIT;
+
+			format_display_notes(&c->object.oid, &notebuf,
+					     get_log_output_encoding(),
+					     format_ctx->ctx.fmt == CMIT_FMT_USERFORMAT);
+			format_ctx->ctx.notes_message = strbuf_detach(&notebuf, NULL);
+		}
+
+		pretty_print_commit(&format_ctx->ctx, c, buf);
+		free(format_ctx->ctx.notes_message);
+
+		return buf->buf;
+	}
+
 	n = get_commit_rev_name(c);
 	if (!n)
 		return NULL;
@@ -479,6 +508,7 @@ static const char *get_rev_name(const struct object *o, struct strbuf *buf)
 
 static void show_name(const struct object *obj,
 		      const char *caller_name,
+		      struct pretty_format *format_ctx,
 		      int always, int allow_undefined, int name_only)
 {
 	const char *name;
@@ -487,7 +517,7 @@ static void show_name(const struct object *obj,
 
 	if (!name_only)
 		printf("%s ", caller_name ? caller_name : oid_to_hex(oid));
-	name = get_rev_name(obj, &buf);
+	name = get_rev_name(obj, format_ctx, &buf);
 	if (name)
 		printf("%s\n", name);
 	else if (allow_undefined)
@@ -507,7 +537,9 @@ static char const * const name_rev_usage[] = {
 	NULL
 };
 
-static void name_rev_line(char *p, struct name_ref_data *data)
+static void name_rev_line(char *p,
+			  struct name_ref_data *data,
+			  struct pretty_format *format_ctx)
 {
 	struct strbuf buf = STRBUF_INIT;
 	int counter = 0;
@@ -532,7 +564,7 @@ static void name_rev_line(char *p, struct name_ref_data *data)
 				struct object *o =
 					lookup_object(the_repository, &oid);
 				if (o)
-					name = get_rev_name(o, &buf);
+					name = get_rev_name(o, format_ctx, &buf);
 			}
 			*(p+1) = c;
 
@@ -567,6 +599,10 @@ int cmd_name_rev(int argc,
 #endif
 	int all = 0, annotate_stdin = 0, allow_undefined = 1, always = 0, peel_tag = 0;
 	struct name_ref_data data = { 0, 0, STRING_LIST_INIT_NODUP, STRING_LIST_INIT_NODUP };
+	const char *format = NULL;
+	struct rev_info format_rev = REV_INFO_INIT;
+	struct pretty_format *format_ctx = NULL;
+	struct pretty_format format_pp = {0};
 	struct option opts[] = {
 		OPT_BOOL(0, "name-only", &data.name_only, N_("print only ref-based names (no object names)")),
 		OPT_BOOL(0, "tags", &data.tags_only, N_("only use tags to name the commits")),
@@ -584,6 +620,8 @@ int cmd_name_rev(int argc,
 			   PARSE_OPT_HIDDEN),
 #endif /* WITH_BREAKING_CHANGES */
 		OPT_BOOL(0, "annotate-stdin", &annotate_stdin, N_("annotate text from stdin")),
+		OPT_STRING(0, "format", &format, N_("format"),
+			   "pretty-print output instead"),
 		OPT_BOOL(0, "undefined", &allow_undefined, N_("allow to print `undefined` names (default)")),
 		OPT_BOOL(0, "always",     &always,
 			   N_("show abbreviated commit object as fallback")),
@@ -606,6 +644,29 @@ int cmd_name_rev(int argc,
 	}
 #endif
 
+	if (format) {
+		struct pretty_print_context ctx = {0};
+		struct userformat_want want = {0};
+
+		get_commit_format(format, &format_rev);
+		ctx.rev = &format_rev;
+		ctx.fmt = format_rev.commit_format;
+		ctx.abbrev = format_rev.abbrev;
+		ctx.date_mode_explicit = format_rev.date_mode_explicit;
+		ctx.date_mode = format_rev.date_mode;
+		ctx.color = GIT_COLOR_AUTO;
+		format_pp.ctx = ctx;
+
+		userformat_find_requirements(format, &want);
+		if (want.notes)
+			load_display_notes(NULL);
+
+		format_pp.want = want;
+		format_ctx = &format_pp;
+
+		data.name_only = true;
+	}
+
 	if (all + annotate_stdin + !!argc > 1) {
 		error("Specify either a list, or --all, not both!");
 		usage_with_options(name_rev_usage, opts);
@@ -663,7 +724,7 @@ int cmd_name_rev(int argc,
 
 		while (strbuf_getline(&sb, stdin) != EOF) {
 			strbuf_addch(&sb, '\n');
-			name_rev_line(sb.buf, &data);
+			name_rev_line(sb.buf, &data, format_ctx);
 		}
 		strbuf_release(&sb);
 	} else if (all) {
@@ -674,13 +735,13 @@ int cmd_name_rev(int argc,
 			struct object *obj = get_indexed_object(the_repository, i);
 			if (!obj || obj->type != OBJ_COMMIT)
 				continue;
-			show_name(obj, NULL,
+			show_name(obj, NULL, format_ctx,
 				  always, allow_undefined, data.name_only);
 		}
 	} else {
 		int i;
 		for (i = 0; i < revs.nr; i++)
-			show_name(revs.objects[i].item, revs.objects[i].name,
+			show_name(revs.objects[i].item, revs.objects[i].name, format_ctx,
 				  always, allow_undefined, data.name_only);
 	}
 
diff --git a/t/t6120-describe.sh b/t/t6120-describe.sh
index 2c70cc561ad..6dba392d343 100755
--- a/t/t6120-describe.sh
+++ b/t/t6120-describe.sh
@@ -658,6 +658,64 @@ test_expect_success 'name-rev --annotate-stdin works with commitGraph' '
 	)
 '
 
+test_expect_success 'name-rev --format setup' '
+	mkdir repo-format &&
+	git -C repo-format init &&
+	test_commit -C repo-format first &&
+	test_commit -C repo-format second &&
+	test_commit -C repo-format third &&
+	test_commit -C repo-format fourth &&
+	test_commit -C repo-format fifth &&
+	test_commit -C repo-format sixth &&
+	test_commit -C repo-format seventh &&
+	test_commit -C repo-format eighth
+'
+
+test_expect_success 'name-rev --format=%s for argument revs' '
+	cat >expect <<-\EOF &&
+	eighth
+	seventh
+	fifth
+	EOF
+	git -C repo-format name-rev --format=%s \
+		HEAD HEAD~ HEAD~3 >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success '--name-rev --format=<pretty> --annotate-stdin from rev-list same as log' '
+	git -C repo-format log --format=reference >expect &&
+	test_file_not_empty expect &&
+	git -C repo-format rev-list HEAD >list &&
+	git -C repo-format name-rev --format=reference \
+		--annotate-stdin <list >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success '--name-rev --format=<pretty> --annotate-stdin with running text and tree oid' '
+	cmit_oid=$(git -C repo-format rev-parse :/fifth) &&
+	reference=$(git -C repo-format log -n1 --format=reference :/fifth) &&
+	tree=$(git -C repo-format rev-parse HEAD^{tree}) &&
+	cat >expect <<-EOF &&
+	We thought we fixed this in ${reference}.
+	But look at this tree: ${tree}.
+	EOF
+	git -C repo-format name-rev --format=reference --annotate-stdin \
+		>actual <<-EOF &&
+	We thought we fixed this in ${cmit_oid}.
+	But look at this tree: ${tree}.
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success '--name-rev --format=<pretty> with a note' '
+	test_when_finished "git -C repo-format notes remove" &&
+	git -C repo-format notes add -m"Make a note" &&
+	printf "Make a note\n\n\n" >expect &&
+	git -C repo-format name-rev --format="tformat:%N" \
+		HEAD HEAD~ >actual &&
+	test_cmp expect actual
+'
+
 #               B
 #               o
 #  H             \
-- 
2.53.0.32.gf6228eaf9cc
Junio C HamanoMar 14, 2026, 00:22 UTC in reply to kristofferhaugsbakk@fastmail.com on lore

Re: [PATCH 1/2] name-rev: wrap both blocks in braces

kristofferhaugsbakk@fastmail.com writes:
Show 7 quoted lines
> -		else if (++counter == hexsz &&
> -			 !ishex(*(p+1))) {
> +		} else if (++counter == hexsz &&
> +			   !ishex(*(p + 1))) {
>  			struct object_id oid;
>  			const char *name = NULL;
>  			char c = *(p+1);

You are correcting "p+1" to "p + 1" to honor our coding style in a few lines above "while at it", but there are three others in the same block (we can see one of them in the post-context), which means these are now inconsistent. Fixing all of them would make it a far larger change than qualifies as a "while at it" change. Either make it another step that is an unrelated clean up, or leave it as-is.

The primary thrust of this patch does make sense and is executed well.

Thanks.
Junio C HamanoMar 14, 2026, 00:22 UTC in reply to kristofferhaugsbakk@fastmail.com on lore

Re: [PATCH 2/2] name-rev: learn --format=<pretty>

kristofferhaugsbakk@fastmail.com writes:
Show 11 quoted lines
> diff --git a/Documentation/git-name-rev.adoc b/Documentation/git-name-rev.adoc
> index d4f1c4d5945..8f050cd4763 100644
> --- a/Documentation/git-name-rev.adoc
> +++ b/Documentation/git-name-rev.adoc
> @@ -9,7 +9,7 @@ git-name-rev - Find symbolic names for given revs
>  SYNOPSIS
>  --------
>  [verse]
> -'git name-rev' [--tags] [--refs=<pattern>]
> +'git name-rev' [--tags] [--refs=<pattern>] [--format=<pretty>]
>  	       ( --all | --annotate-stdin | <commit-ish>... )

We acquired a new option. Do we need a matching change to the contents of name_rev_usage[] array?

Show 6 quoted lines
> +--format=<pretty>::
> +--no-format::
> +	Format revisions instead of outputting symbolic names. The
> +	default is `--no-format`.
> ++
> +Implies `--name-only`.
If it is implication, would 
    git name-rev --format=reference --no-name-only
do what is naturally expected?
Show 19 quoted lines
> @@ -462,6 +472,25 @@ static const char *get_rev_name(const struct object *o, struct strbuf *buf)
>  	if (o->type != OBJ_COMMIT)
>  		return get_exact_ref_match(o);
>  	c = (const struct commit *) o;
> +
> +	if (format_ctx) {
> +		strbuf_reset(buf);
> +
> +		if (format_ctx->want.notes) {
> +			struct strbuf notebuf = STRBUF_INIT;
> +
> +			format_display_notes(&c->object.oid, &notebuf,
> +					     get_log_output_encoding(),
> +					     format_ctx->ctx.fmt == CMIT_FMT_USERFORMAT);
> +			format_ctx->ctx.notes_message = strbuf_detach(&notebuf, NULL);
> +		}
> +
> +		pretty_print_commit(&format_ctx->ctx, c, buf);
> +		free(format_ctx->ctx.notes_message);

Is free() the expected thing to do here, or FREE_AND_NULL()? Unlike callers like log-tree.c:show_log() where a context is prepared, used once, and then discarded, format_pp is initialized in cmd_name_rev() once and then repeatedly used by show_name() potentially multiple times, so there may be a risk of getting confused by this leftover non-NULL pointer that points at an already free'd piece of memory.

Or there may not be---I did not check, but you as the author must have already checked, hence this question.

Show 51 quoted lines
> +		return buf->buf;
> +	}
> +
>  	n = get_commit_rev_name(c);
>  	if (!n)
>  		return NULL;
> @@ -479,6 +508,7 @@ static const char *get_rev_name(const struct object *o, struct strbuf *buf)
>  
>  static void show_name(const struct object *obj,
>  		      const char *caller_name,
> +		      struct pretty_format *format_ctx,
>  		      int always, int allow_undefined, int name_only)
>  {
>  	const char *name;
> @@ -487,7 +517,7 @@ static void show_name(const struct object *obj,
>  
>  	if (!name_only)
>  		printf("%s ", caller_name ? caller_name : oid_to_hex(oid));
> -	name = get_rev_name(obj, &buf);
> +	name = get_rev_name(obj, format_ctx, &buf);
>  	if (name)
>  		printf("%s\n", name);
>  	else if (allow_undefined)
> @@ -507,7 +537,9 @@ static char const * const name_rev_usage[] = {
>  	NULL
>  };
>  
> -static void name_rev_line(char *p, struct name_ref_data *data)
> +static void name_rev_line(char *p,
> +			  struct name_ref_data *data,
> +			  struct pretty_format *format_ctx)
>  {
>  	struct strbuf buf = STRBUF_INIT;
>  	int counter = 0;
> @@ -532,7 +564,7 @@ static void name_rev_line(char *p, struct name_ref_data *data)
>  				struct object *o =
>  					lookup_object(the_repository, &oid);
>  				if (o)
> -					name = get_rev_name(o, &buf);
> +					name = get_rev_name(o, format_ctx, &buf);
>  			}
>  			*(p+1) = c;
>  
> @@ -567,6 +599,10 @@ int cmd_name_rev(int argc,
>  #endif
>  	int all = 0, annotate_stdin = 0, allow_undefined = 1, always = 0, peel_tag = 0;
>  	struct name_ref_data data = { 0, 0, STRING_LIST_INIT_NODUP, STRING_LIST_INIT_NODUP };
> +	const char *format = NULL;
> +	struct rev_info format_rev = REV_INFO_INIT;
> +	struct pretty_format *format_ctx = NULL;
> +	struct pretty_format format_pp = {0};

Hmph, would we want to use the full init_revisions() instead of static REV_INFO_INIT that initialises a lot more members of the struct properly, most importantly the "repo" member that points at the repostiory to be used?

Show 12 quoted lines
> +	if (format) {
> +		struct pretty_print_context ctx = {0};
> +		struct userformat_want want = {0};
> +
> +		get_commit_format(format, &format_rev);
> +		ctx.rev = &format_rev;
> +		ctx.fmt = format_rev.commit_format;
> +		ctx.abbrev = format_rev.abbrev;
> +		ctx.date_mode_explicit = format_rev.date_mode_explicit;
> +		ctx.date_mode = format_rev.date_mode;
> +		ctx.color = GIT_COLOR_AUTO;
> +		format_pp.ctx = ctx;

Why does this code initialize and assign to a on-stack ctx first and then assign it to format_pp.ctx, instead of working on format_pp.ctx directly?

Show 10 quoted lines
> +		userformat_find_requirements(format, &want);
> +		if (want.notes)
> +			load_display_notes(NULL);
> +
> +		format_pp.want = want;
> +		format_ctx = &format_pp;
> +
> +		data.name_only = true;
> +	}
> +
Kristoffer HaugsbakkMar 17, 2026, 22:07 UTC in reply to Junio C Hamano on lore

Re: [PATCH 2/2] name-rev: learn --format=<pretty>

On Sat, Mar 14, 2026, at 01:22, Junio C Hamano wrote:
Show 16 quoted lines
> kristofferhaugsbakk@fastmail.com writes:
>
>> diff --git a/Documentation/git-name-rev.adoc b/Documentation/git-name-rev.adoc
>> index d4f1c4d5945..8f050cd4763 100644
>> --- a/Documentation/git-name-rev.adoc
>> +++ b/Documentation/git-name-rev.adoc
>> @@ -9,7 +9,7 @@ git-name-rev - Find symbolic names for given revs
>>  SYNOPSIS
>>  --------
>>  [verse]
>> -'git name-rev' [--tags] [--refs=<pattern>]
>> +'git name-rev' [--tags] [--refs=<pattern>] [--format=<pretty>]
>>  	       ( --all | --annotate-stdin | <commit-ish>... )
>
> We acquired a new option.  Do we need a matching change to
> the contents of name_rev_usage[] array?

I looked at it and it seemed that `[<options>]` were supposed to stay inside that placeholder. But since this is a new mode, maybe:

    diff --git builtin/name-rev.c builtin/name-rev.c
    index 6188cf98ce0..13e67a7723c 100644
    --- builtin/name-rev.c
    +++ builtin/name-rev.c
    @@ -504,6 +504,7 @@ static char const * const name_rev_usage[] = {
            N_("git name-rev [<options>] <commit>..."),
            N_("git name-rev [<options>] --all"),
            N_("git name-rev [<options>] --annotate-stdin"),
    +       N_("git name-rev --format=<pretty> ..."),
            NULL
     };
Show 13 quoted lines
>
>> +--format=<pretty>::
>> +--no-format::
>> +	Format revisions instead of outputting symbolic names. The
>> +	default is `--no-format`.
>> ++
>> +Implies `--name-only`.
>
> If it is implication, would
>
>     git name-rev --format=reference --no-name-only
>
> do what is naturally expected?

No it wouldn’t. You’re right, it really locks in that option with no escape hatch.

But in my next version I have switched to a parse-options callback so that `... --format=... --no-name-only` really does turn off `--name-only`.

Show 30 quoted lines
>
>> @@ -462,6 +472,25 @@ static const char *get_rev_name(const struct object *o, struct strbuf *buf)
>>  	if (o->type != OBJ_COMMIT)
>>  		return get_exact_ref_match(o);
>>  	c = (const struct commit *) o;
>> +
>> +	if (format_ctx) {
>> +		strbuf_reset(buf);
>> +
>> +		if (format_ctx->want.notes) {
>> +			struct strbuf notebuf = STRBUF_INIT;
>> +
>> +			format_display_notes(&c->object.oid, &notebuf,
>> +					     get_log_output_encoding(),
>> +					     format_ctx->ctx.fmt == CMIT_FMT_USERFORMAT);
>> +			format_ctx->ctx.notes_message = strbuf_detach(&notebuf, NULL);
>> +		}
>> +
>> +		pretty_print_commit(&format_ctx->ctx, c, buf);
>> +		free(format_ctx->ctx.notes_message);
>
> Is free() the expected thing to do here, or FREE_AND_NULL()?  Unlike
> callers like log-tree.c:show_log() where a context is prepared, used
> once, and then discarded, format_pp is initialized in cmd_name_rev()
> once and then repeatedly used by show_name() potentially multiple
> times, so there may be a risk of getting confused by this leftover
> non-NULL pointer that points at an already free'd piece of memory.
>
> Or there may not be---I did not check, but you as the author must
> have already checked, hence this question.

This is supposed to be tested by `--name-rev --format=<pretty> with a note`; it has a note on the first revision but not the second.

Here we never use this pointer again and we get a fresh pointer from the strbuf before freeing again (whether it gets populated with a pointer or not).

But it does sound better to just null it. There’s no need to have it laying around.

Show 18 quoted lines
>
>> +		return buf->buf;
>> +	}
>> +
>>[snip]
>> @@ -567,6 +599,10 @@ int cmd_name_rev(int argc,
>>  #endif
>>  	int all = 0, annotate_stdin = 0, allow_undefined = 1, always = 0, peel_tag = 0;
>>  	struct name_ref_data data = { 0, 0, STRING_LIST_INIT_NODUP, STRING_LIST_INIT_NODUP };
>> +	const char *format = NULL;
>> +	struct rev_info format_rev = REV_INFO_INIT;
>> +	struct pretty_format *format_ctx = NULL;
>> +	struct pretty_format format_pp = {0};
>
> Hmph, would we want to use the full init_revisions() instead of
> static REV_INFO_INIT that initialises a lot more members of the
> struct properly, most importantly the "repo" member that points at
> the repostiory to be used?

Here I looked at the doc for `pretty.h:get_commit_format` in order to learn what I needed to set up. Since it doesn’t say much I did the least work that I could get away with when it comes to struct initializing. Since it does seem like a struct for a lot of different situations while this is just a formatting situation.

I might have done less research than I ought to.
Show 17 quoted lines
>
>> +	if (format) {
>> +		struct pretty_print_context ctx = {0};
>> +		struct userformat_want want = {0};
>> +
>> +		get_commit_format(format, &format_rev);
>> +		ctx.rev = &format_rev;
>> +		ctx.fmt = format_rev.commit_format;
>> +		ctx.abbrev = format_rev.abbrev;
>> +		ctx.date_mode_explicit = format_rev.date_mode_explicit;
>> +		ctx.date_mode = format_rev.date_mode;
>> +		ctx.color = GIT_COLOR_AUTO;
>> +		format_pp.ctx = ctx;
>
> Why does this code initialize and assign to a on-stack ctx first and
> then assign it to format_pp.ctx, instead of working on format_pp.ctx
> directly?
You’re right. I’ll just assign directly.
Show 11 quoted lines
>
>> +		userformat_find_requirements(format, &want);
>> +		if (want.notes)
>> +			load_display_notes(NULL);
>> +
>> +		format_pp.want = want;
>> +		format_ctx = &format_pp;
>> +
>> +		data.name_only = true;
>> +	}
>> +
Kristoffer HaugsbakkMar 17, 2026, 22:10 UTC in reply to Junio C Hamano on lore

Re: [PATCH 1/2] name-rev: wrap both blocks in braces

On Sat, Mar 14, 2026, at 01:22, Junio C Hamano wrote:
Show 16 quoted lines
> kristofferhaugsbakk@fastmail.com writes:
>
>> -		else if (++counter == hexsz &&
>> -			 !ishex(*(p+1))) {
>> +		} else if (++counter == hexsz &&
>> +			   !ishex(*(p + 1))) {
>>  			struct object_id oid;
>>  			const char *name = NULL;
>>  			char c = *(p+1);
>
> You are correcting "p+1" to "p + 1" to honor our coding style in a
> few lines above "while at it", but there are three others in the
> same block (we can see one of them in the post-context), which means
> these are now inconsistent.  Fixing all of them would make it a far
> larger change than qualifies as a "while at it" change.  Either make
> it another step that is an unrelated clean up, or leave it as-is.

Correct observation. clang-format got ahold of a few stray lines. But I thought it only corrected the indentation, not that it also caught this arithmetic formatting. I’ll remove it in the next version.

Thanks
>
> The primary thrust of this patch does make sense and is executed
> well.
Kristoffer HaugsbakkMar 18, 2026, 15:36 UTC in reply to Kristoffer Haugsbakk on lore

Re: [PATCH 2/2] name-rev: learn --format=<pretty>

On Tue, Mar 17, 2026, at 23:07, Kristoffer Haugsbakk wrote:
Show 11 quoted lines
>>[snip]
>>
>> Or there may not be---I did not check, but you as the author must
>> have already checked, hence this question.
>
> This is supposed to be tested by `--name-rev --format=<pretty> with a
> note`; it has a note on the first revision but not the second.
>
> Here we never use this pointer again and we get a fresh pointer from the
> strbuf before freeing again (whether it gets populated with a pointer or
> not).

Here I meant to write: “whether it gets populated with any note data or not”.

>
> But it does sound better to just null it. There’s no need to have it
> laying around.
>[snip]
kristofferhaugsbakk@fastmail.comMar 20, 2026, 13:09 UTC in reply to kristofferhaugsbakk@fastmail.com on lore

[PATCH v2 0/2] name-rev: learn --format=<pretty>

From: Kristoffer Haugsbakk <code@khaugsbakk.name>
Topic name (applied): kh/name-rev-custom-format

Topic summary: Teach git-name-rev(1) a mode to pretty format revisions instead of outputting symbolic names.

(See the second patch for details.)
The first patch is just for `CodingGuidelines`. Unrelated.
(See the previous cover letter for a slightly different introduction.)

This started because I had a faint wish in the back of my head (that I was probably not going to act on) to implement a third-party git-format-rev(1) command which does what `git name-rev --format=<pretty>` does with this series applied. I thought I could just use one invocation of some git(1) command to feed to stdin and read from stdout. Like git-log(1), or git-rev-list(1). But due to apparent limitations (see patch [2/2] message) there didn’t seem to be anything better than invoking a git(1) command per revision/hash that was encountered in the text. I wanted something more efficient. (I had no need for something more efficient. I just wanted it.)

I then picked git-name-rev(1) since that was the easiest thing to implement for me. Add an option to an existing command.

Right now this is just version 2 and I don’t know if this is a good approach or not. If it ever is accepted (in whatever form) I don’t expect it to be soon.

git(1) already has a lot of commands. This doesn’t expand the footprint. On the other hand, I might be naive about what a proper formatter needs in terms of command options. Right now there is the helper `--notes`. But is more needed?

• --abbrev ? • --date ?

I don’t know. Maybe at some point this would be too much, too crowded, for git-name-rev(1). Then it might make sense to split it out to a separate builtin?

These are some thoughts and context.
§ Changes in v2

Implement `--notes` and respond to reviewer feedback. Details on the second patch.

The first patch now just formats what it intends to (there was stray clang-formatting).

[1/2] name-rev: wrap both blocks in braces [2/2] name-rev: learn --format=<pretty>

 Documentation/git-name-rev.adoc |  10 ++-
 builtin/name-rev.c              | 108 ++++++++++++++++++++++++++++----
 t/t6120-describe.sh             |  96 ++++++++++++++++++++++++++++
 3 files changed, 202 insertions(+), 12 deletions(-)
Interdiff against v1:
Show changes to 3 files +92 −28

Documentation/git-name-rev.adoc, builtin/name-rev.c, t/t6120-describe.sh

diff --git a/Documentation/git-name-rev.adoc b/Documentation/git-name-rev.adoc
index 8f050cd4763..65348690c8c 100644
--- a/Documentation/git-name-rev.adoc
+++ b/Documentation/git-name-rev.adoc
@@ -26,7 +26,8 @@ OPTIONS
 	Format revisions instead of outputting symbolic names. The
 	default is `--no-format`.
 +
-Implies `--name-only`.
+Implies `--name-only`. The negation `--no-format` implies
+`--no-name-only` (the default for the command).
 
 --tags::
 	Do not use branch names, but only tags to name the commits
diff --git a/builtin/name-rev.c b/builtin/name-rev.c
index 30d981104c6..9a008d8b7a8 100644
--- a/builtin/name-rev.c
+++ b/builtin/name-rev.c
@@ -41,6 +41,11 @@ struct pretty_format {
 	struct userformat_want want;
 };
 
+struct format_cb_data {
+    const char *format;
+    int *name_only;
+};
+
 define_commit_slab(commit_rev_name, struct rev_name);
 
 static timestamp_t generation_cutoff = GENERATION_NUMBER_INFINITY;
@@ -486,7 +491,7 @@ static const char *get_rev_name(const struct object *o,
 		}
 
 		pretty_print_commit(&format_ctx->ctx, c, buf);
-		free(format_ctx->ctx.notes_message);
+		FREE_AND_NULL(format_ctx->ctx.notes_message);
 
 		return buf->buf;
 	}
@@ -551,7 +556,7 @@ static void name_rev_line(char *p,
 		if (!ishex(*p)) {
 			counter = 0;
 		} else if (++counter == hexsz &&
-			   !ishex(*(p + 1))) {
+			 !ishex(*(p+1))) {
 			struct object_id oid;
 			const char *name = NULL;
 			char c = *(p+1);
@@ -586,6 +591,16 @@ static void name_rev_line(char *p,
 	strbuf_release(&buf);
 }
 
+static int format_cb(const struct option *option,
+		     const char *arg,
+		     int unset)
+{
+	struct format_cb_data *data = option->value;
+	data->format = arg;
+	*data->name_only = !unset;
+	return 0;
+}
+
 int cmd_name_rev(int argc,
 		 const char **argv,
 		 const char *prefix,
@@ -599,10 +614,12 @@ int cmd_name_rev(int argc,
 #endif
 	int all = 0, annotate_stdin = 0, allow_undefined = 1, always = 0, peel_tag = 0;
 	struct name_ref_data data = { 0, 0, STRING_LIST_INIT_NODUP, STRING_LIST_INIT_NODUP };
-	const char *format = NULL;
+	static struct format_cb_data format_cb_data = { 0 };
+	struct display_notes_opt format_notes_opt;
 	struct rev_info format_rev = REV_INFO_INIT;
 	struct pretty_format *format_ctx = NULL;
-	struct pretty_format format_pp = {0};
+	struct pretty_format format_pp = { 0 };
+	struct string_list notes = STRING_LIST_INIT_NODUP;
 	struct option opts[] = {
 		OPT_BOOL(0, "name-only", &data.name_only, N_("print only ref-based names (no object names)")),
 		OPT_BOOL(0, "tags", &data.tags_only, N_("only use tags to name the commits")),
@@ -620,8 +637,10 @@ int cmd_name_rev(int argc,
 			   PARSE_OPT_HIDDEN),
 #endif /* WITH_BREAKING_CHANGES */
 		OPT_BOOL(0, "annotate-stdin", &annotate_stdin, N_("annotate text from stdin")),
-		OPT_STRING(0, "format", &format, N_("format"),
-			   "pretty-print output instead"),
+		OPT_CALLBACK(0, "format", &format_cb_data, N_("format"),
+			     N_("pretty-print output instead"), format_cb),
+		OPT_STRING_LIST(0, "notes", &notes, N_("notes"),
+				N_("display notes for --format")),
 		OPT_BOOL(0, "undefined", &allow_undefined, N_("allow to print `undefined` names (default)")),
 		OPT_BOOL(0, "always",     &always,
 			   N_("show abbreviated commit object as fallback")),
@@ -630,6 +649,8 @@ int cmd_name_rev(int argc,
 		OPT_END(),
 	};
 
+	init_display_notes(&format_notes_opt);
+	format_cb_data.name_only = &data.name_only;
 	mem_pool_init(&string_pool, 0);
 	init_commit_rev_name(&rev_names);
 	repo_config(the_repository, git_default_config, NULL);
@@ -644,27 +665,29 @@ int cmd_name_rev(int argc,
 	}
 #endif
 
-	if (format) {
-		struct pretty_print_context ctx = {0};
-		struct userformat_want want = {0};
-
-		get_commit_format(format, &format_rev);
-		ctx.rev = &format_rev;
-		ctx.fmt = format_rev.commit_format;
-		ctx.abbrev = format_rev.abbrev;
-		ctx.date_mode_explicit = format_rev.date_mode_explicit;
-		ctx.date_mode = format_rev.date_mode;
-		ctx.color = GIT_COLOR_AUTO;
-		format_pp.ctx = ctx;
-
-		userformat_find_requirements(format, &want);
-		if (want.notes)
-			load_display_notes(NULL);
+	if (format_cb_data.format) {
+		get_commit_format(format_cb_data.format, &format_rev);
+		format_pp.ctx.rev = &format_rev;
+		format_pp.ctx.fmt = format_rev.commit_format;
+		format_pp.ctx.abbrev = format_rev.abbrev;
+		format_pp.ctx.date_mode_explicit = format_rev.date_mode_explicit;
+		format_pp.ctx.date_mode = format_rev.date_mode;
+		format_pp.ctx.color = GIT_COLOR_AUTO;
+
+		userformat_find_requirements(format_cb_data.format,
+					     &format_pp.want);
+		if (format_pp.want.notes) {
+			int ignore_show_notes = 0;
+			struct string_list_item *n;
+
+			for_each_string_list_item(n, &notes)
+				enable_ref_display_notes(&format_notes_opt,
+							 &ignore_show_notes,
+							 n->string);
+			load_display_notes(&format_notes_opt);
+		}
 
-		format_pp.want = want;
 		format_ctx = &format_pp;
-
-		data.name_only = true;
 	}
 
 	if (all + annotate_stdin + !!argc > 1) {
@@ -747,6 +770,8 @@ int cmd_name_rev(int argc,
 
 	string_list_clear(&data.ref_filters, 0);
 	string_list_clear(&data.exclude_filters, 0);
+	string_list_clear(&notes, 0);
+	release_display_notes(&format_notes_opt);
 	mem_pool_discard(&string_pool, 0);
 	object_array_clear(&revs);
 	return 0;
diff --git a/t/t6120-describe.sh b/t/t6120-describe.sh
index 6dba392d343..0b7e9fe396d 100755
--- a/t/t6120-describe.sh
+++ b/t/t6120-describe.sh
@@ -671,6 +671,25 @@ test_expect_success 'name-rev --format setup' '
 	test_commit -C repo-format eighth
 '
 
+test_expect_success 'name-rev --format --no-name-only' '
+	cat >expect <<-\EOF &&
+	HEAD~3 [fifth]
+	HEAD [eighth]
+	HEAD~5 [third]
+	EOF
+	git -C repo-format name-rev --format="[%s]" \
+		--no-name-only HEAD~3 HEAD HEAD~5 >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'name-rev --format --no-format is the same as regular name-rev' '
+	git -C repo-format name-rev HEAD~2 HEAD~3 >expect &&
+	test_file_not_empty expect &&
+	git -C repo-format name-rev --format="huh?" \
+		--no-format HEAD~2 HEAD~3 >actual &&
+	test_cmp expect actual
+'
+
 test_expect_success 'name-rev --format=%s for argument revs' '
 	cat >expect <<-\EOF &&
 	eighth
@@ -682,7 +701,7 @@ test_expect_success 'name-rev --format=%s for argument revs' '
 	test_cmp expect actual
 '
 
-test_expect_success '--name-rev --format=<pretty> --annotate-stdin from rev-list same as log' '
+test_expect_success '--name-rev --format=reference --annotate-stdin from rev-list same as log' '
 	git -C repo-format log --format=reference >expect &&
 	test_file_not_empty expect &&
 	git -C repo-format rev-list HEAD >list &&
@@ -707,7 +726,7 @@ test_expect_success '--name-rev --format=<pretty> --annotate-stdin with running
 	test_cmp expect actual
 '
 
-test_expect_success '--name-rev --format=<pretty> with a note' '
+test_expect_success 'name-rev --format=<pretty> with %N (note)' '
 	test_when_finished "git -C repo-format notes remove" &&
 	git -C repo-format notes add -m"Make a note" &&
 	printf "Make a note\n\n\n" >expect &&
@@ -716,6 +735,25 @@ test_expect_success '--name-rev --format=<pretty> with a note' '
 	test_cmp expect actual
 '
 
+test_expect_success 'name-rev --format=<pretty> --notes<ref>' '
+	# One custom notes ref
+	test_when_finished "git -C repo-format notes remove" &&
+	test_when_finished "git -C repo-format notes --ref=word remove" &&
+	git -C repo-format notes add -m"default" &&
+	git -C repo-format notes --ref=word add -m"custom" &&
+	printf "custom\n\n" >expect &&
+	git -C repo-format name-rev --format="tformat:%N" \
+		--notes=word \
+		HEAD >actual &&
+	test_cmp expect actual &&
+	# Glob all
+	printf "default\ncustom\n\n" >expect &&
+	git -C repo-format name-rev --format="tformat:%N" \
+		--notes=* \
+		HEAD >actual &&
+	test_cmp expect actual
+'
+
 #               B
 #               o
 #  H             \
Range-diff against v1:
1:  6f88b4c96a9 ! 1:  9cb5cfd1ec3 name-rev: wrap both blocks in braces
    @@ builtin/name-rev.c: static void name_rev_line(char *p, struct name_ref_data *dat
     +		if (!ishex(*p)) {
      			counter = 0;
     -		else if (++counter == hexsz &&
    --			 !ishex(*(p+1))) {
     +		} else if (++counter == hexsz &&
    -+			   !ishex(*(p + 1))) {
    + 			 !ishex(*(p+1))) {
      			struct object_id oid;
      			const char *name = NULL;
    - 			char c = *(p+1);
2:  6430627e611 ! 2:  52a52060776 name-rev: learn --format=<pretty>
    @@ Documentation/git-name-rev.adoc: format parsable by 'git rev-parse'.
     +	Format revisions instead of outputting symbolic names. The
     +	default is `--no-format`.
     ++
    -+Implies `--name-only`.
    ++Implies `--name-only`. The negation `--no-format` implies
    ++`--no-name-only` (the default for the command).
     +
      --tags::
      	Do not use branch names, but only tags to name the commits
    @@ builtin/name-rev.c: struct rev_name {
     +	struct pretty_print_context ctx;
     +	struct userformat_want want;
     +};
    ++
    ++struct format_cb_data {
    ++    const char *format;
    ++    int *name_only;
    ++};
     +
      define_commit_slab(commit_rev_name, struct rev_name);
      
    @@ builtin/name-rev.c: static const char *get_rev_name(const struct object *o, stru
     +		}
     +
     +		pretty_print_commit(&format_ctx->ctx, c, buf);
    -+		free(format_ctx->ctx.notes_message);
    ++		FREE_AND_NULL(format_ctx->ctx.notes_message);
     +
     +		return buf->buf;
     +	}
    @@ builtin/name-rev.c: static void name_rev_line(char *p, struct name_ref_data *dat
      			}
      			*(p+1) = c;
      
    +@@ builtin/name-rev.c: static void name_rev_line(char *p, struct name_ref_data *data)
    + 	strbuf_release(&buf);
    + }
    + 
    ++static int format_cb(const struct option *option,
    ++		     const char *arg,
    ++		     int unset)
    ++{
    ++	struct format_cb_data *data = option->value;
    ++	data->format = arg;
    ++	*data->name_only = !unset;
    ++	return 0;
    ++}
    ++
    + int cmd_name_rev(int argc,
    + 		 const char **argv,
    + 		 const char *prefix,
     @@ builtin/name-rev.c: int cmd_name_rev(int argc,
      #endif
      	int all = 0, annotate_stdin = 0, allow_undefined = 1, always = 0, peel_tag = 0;
      	struct name_ref_data data = { 0, 0, STRING_LIST_INIT_NODUP, STRING_LIST_INIT_NODUP };
    -+	const char *format = NULL;
    ++	static struct format_cb_data format_cb_data = { 0 };
    ++	struct display_notes_opt format_notes_opt;
     +	struct rev_info format_rev = REV_INFO_INIT;
     +	struct pretty_format *format_ctx = NULL;
    -+	struct pretty_format format_pp = {0};
    ++	struct pretty_format format_pp = { 0 };
    ++	struct string_list notes = STRING_LIST_INIT_NODUP;
      	struct option opts[] = {
      		OPT_BOOL(0, "name-only", &data.name_only, N_("print only ref-based names (no object names)")),
      		OPT_BOOL(0, "tags", &data.tags_only, N_("only use tags to name the commits")),
    @@ builtin/name-rev.c: int cmd_name_rev(int argc,
      			   PARSE_OPT_HIDDEN),
      #endif /* WITH_BREAKING_CHANGES */
      		OPT_BOOL(0, "annotate-stdin", &annotate_stdin, N_("annotate text from stdin")),
    -+		OPT_STRING(0, "format", &format, N_("format"),
    -+			   "pretty-print output instead"),
    ++		OPT_CALLBACK(0, "format", &format_cb_data, N_("format"),
    ++			     N_("pretty-print output instead"), format_cb),
    ++		OPT_STRING_LIST(0, "notes", &notes, N_("notes"),
    ++				N_("display notes for --format")),
      		OPT_BOOL(0, "undefined", &allow_undefined, N_("allow to print `undefined` names (default)")),
      		OPT_BOOL(0, "always",     &always,
      			   N_("show abbreviated commit object as fallback")),
    +@@ builtin/name-rev.c: int cmd_name_rev(int argc,
    + 		OPT_END(),
    + 	};
    + 
    ++	init_display_notes(&format_notes_opt);
    ++	format_cb_data.name_only = &data.name_only;
    + 	mem_pool_init(&string_pool, 0);
    + 	init_commit_rev_name(&rev_names);
    + 	repo_config(the_repository, git_default_config, NULL);
     @@ builtin/name-rev.c: int cmd_name_rev(int argc,
      	}
      #endif
      
    -+	if (format) {
    -+		struct pretty_print_context ctx = {0};
    -+		struct userformat_want want = {0};
    ++	if (format_cb_data.format) {
    ++		get_commit_format(format_cb_data.format, &format_rev);
    ++		format_pp.ctx.rev = &format_rev;
    ++		format_pp.ctx.fmt = format_rev.commit_format;
    ++		format_pp.ctx.abbrev = format_rev.abbrev;
    ++		format_pp.ctx.date_mode_explicit = format_rev.date_mode_explicit;
    ++		format_pp.ctx.date_mode = format_rev.date_mode;
    ++		format_pp.ctx.color = GIT_COLOR_AUTO;
     +
    -+		get_commit_format(format, &format_rev);
    -+		ctx.rev = &format_rev;
    -+		ctx.fmt = format_rev.commit_format;
    -+		ctx.abbrev = format_rev.abbrev;
    -+		ctx.date_mode_explicit = format_rev.date_mode_explicit;
    -+		ctx.date_mode = format_rev.date_mode;
    -+		ctx.color = GIT_COLOR_AUTO;
    -+		format_pp.ctx = ctx;
    ++		userformat_find_requirements(format_cb_data.format,
    ++					     &format_pp.want);
    ++		if (format_pp.want.notes) {
    ++			int ignore_show_notes = 0;
    ++			struct string_list_item *n;
     +
    -+		userformat_find_requirements(format, &want);
    -+		if (want.notes)
    -+			load_display_notes(NULL);
    ++			for_each_string_list_item(n, &notes)
    ++				enable_ref_display_notes(&format_notes_opt,
    ++							 &ignore_show_notes,
    ++							 n->string);
    ++			load_display_notes(&format_notes_opt);
    ++		}
     +
    -+		format_pp.want = want;
     +		format_ctx = &format_pp;
    -+
    -+		data.name_only = true;
     +	}
     +
      	if (all + annotate_stdin + !!argc > 1) {
    @@ builtin/name-rev.c: int cmd_name_rev(int argc,
      				  always, allow_undefined, data.name_only);
      	}
      
    + 	string_list_clear(&data.ref_filters, 0);
    + 	string_list_clear(&data.exclude_filters, 0);
    ++	string_list_clear(&notes, 0);
    ++	release_display_notes(&format_notes_opt);
    + 	mem_pool_discard(&string_pool, 0);
    + 	object_array_clear(&revs);
    + 	return 0;
     
      ## t/t6120-describe.sh ##
     @@ t/t6120-describe.sh: test_expect_success 'name-rev --annotate-stdin works with commitGraph' '
    @@ t/t6120-describe.sh: test_expect_success 'name-rev --annotate-stdin works with c
     +	test_commit -C repo-format eighth
     +'
     +
    ++test_expect_success 'name-rev --format --no-name-only' '
    ++	cat >expect <<-\EOF &&
    ++	HEAD~3 [fifth]
    ++	HEAD [eighth]
    ++	HEAD~5 [third]
    ++	EOF
    ++	git -C repo-format name-rev --format="[%s]" \
    ++		--no-name-only HEAD~3 HEAD HEAD~5 >actual &&
    ++	test_cmp expect actual
    ++'
    ++
    ++test_expect_success 'name-rev --format --no-format is the same as regular name-rev' '
    ++	git -C repo-format name-rev HEAD~2 HEAD~3 >expect &&
    ++	test_file_not_empty expect &&
    ++	git -C repo-format name-rev --format="huh?" \
    ++		--no-format HEAD~2 HEAD~3 >actual &&
    ++	test_cmp expect actual
    ++'
    ++
     +test_expect_success 'name-rev --format=%s for argument revs' '
     +	cat >expect <<-\EOF &&
     +	eighth
    @@ t/t6120-describe.sh: test_expect_success 'name-rev --annotate-stdin works with c
     +	test_cmp expect actual
     +'
     +
    -+test_expect_success '--name-rev --format=<pretty> --annotate-stdin from rev-list same as log' '
    ++test_expect_success '--name-rev --format=reference --annotate-stdin from rev-list same as log' '
     +	git -C repo-format log --format=reference >expect &&
     +	test_file_not_empty expect &&
     +	git -C repo-format rev-list HEAD >list &&
    @@ t/t6120-describe.sh: test_expect_success 'name-rev --annotate-stdin works with c
     +	test_cmp expect actual
     +'
     +
    -+test_expect_success '--name-rev --format=<pretty> with a note' '
    ++test_expect_success 'name-rev --format=<pretty> with %N (note)' '
     +	test_when_finished "git -C repo-format notes remove" &&
     +	git -C repo-format notes add -m"Make a note" &&
     +	printf "Make a note\n\n\n" >expect &&
    @@ t/t6120-describe.sh: test_expect_success 'name-rev --annotate-stdin works with c
     +		HEAD HEAD~ >actual &&
     +	test_cmp expect actual
     +'
    ++
    ++test_expect_success 'name-rev --format=<pretty> --notes<ref>' '
    ++	# One custom notes ref
    ++	test_when_finished "git -C repo-format notes remove" &&
    ++	test_when_finished "git -C repo-format notes --ref=word remove" &&
    ++	git -C repo-format notes add -m"default" &&
    ++	git -C repo-format notes --ref=word add -m"custom" &&
    ++	printf "custom\n\n" >expect &&
    ++	git -C repo-format name-rev --format="tformat:%N" \
    ++		--notes=word \
    ++		HEAD >actual &&
    ++	test_cmp expect actual &&
    ++	# Glob all
    ++	printf "default\ncustom\n\n" >expect &&
    ++	git -C repo-format name-rev --format="tformat:%N" \
    ++		--notes=* \
    ++		HEAD >actual &&
    ++	test_cmp expect actual
    ++'
     +
      #               B
      #               o

base-commit: 67006b9db8b772423ad0706029286096307d2567
-- 
2.53.0.32.gf6228eaf9cc
kristofferhaugsbakk@fastmail.comMar 20, 2026, 13:09 UTC in reply to kristofferhaugsbakk@fastmail.com on lore

[PATCH v2 1/2] name-rev: wrap both blocks in braces

From: Kristoffer Haugsbakk <code@khaugsbakk.name>
See `CodingGuidelines`:
    - When there are multiple arms to a conditional and some of them
      require braces, enclose even a single line block in braces for
      consistency. [...]
Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
---
Notes (series):
    v2:
    Fix stray formatting of `(p+1)`
 builtin/name-rev.c | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)
Show changes to builtin/name-rev.c +4 −4
diff --git a/builtin/name-rev.c b/builtin/name-rev.c
index 6188cf98ce0..171e7bd0e98 100644
--- a/builtin/name-rev.c
+++ b/builtin/name-rev.c
@@ -466,9 +466,9 @@ static const char *get_rev_name(const struct object *o, struct strbuf *buf)
 	if (!n)
 		return NULL;
 
-	if (!n->generation)
+	if (!n->generation) {
 		return n->tip_name;
-	else {
+	} else {
 		strbuf_reset(buf);
 		strbuf_addstr(buf, n->tip_name);
 		strbuf_strip_suffix(buf, "^0");
@@ -516,9 +516,9 @@ static void name_rev_line(char *p, struct name_ref_data *data)
 
 	for (p_start = p; *p; p++) {
 #define ishex(x) (isdigit((x)) || ((x) >= 'a' && (x) <= 'f'))
-		if (!ishex(*p))
+		if (!ishex(*p)) {
 			counter = 0;
-		else if (++counter == hexsz &&
+		} else if (++counter == hexsz &&
 			 !ishex(*(p+1))) {
 			struct object_id oid;
 			const char *name = NULL;
-- 
2.53.0.32.gf6228eaf9cc
kristofferhaugsbakk@fastmail.comMar 20, 2026, 13:09 UTC in reply to kristofferhaugsbakk@fastmail.com on lore

[PATCH v2 2/2] name-rev: learn --format=<pretty>

From: Kristoffer Haugsbakk <code@khaugsbakk.name>

Teach git-name-rev(1) to format the given revisions instead of creating symbolic names.

Sometimes you want to format commits. Most of the time you’re walking the graph, e.g. getting a range of commits like `master..topic`. That’s a job for git-log(1).

But sometimes you might want to format commits that you encounter on demand:

• Full hashes in running text that you might want to pretty-print
• git-last-modified(1) outputs full hashes that you can do the same with
• git-cherry(1) has `-v` for commit subject, but maybe you want
  something else?
But now you can’t use git-log(1), git-show(1), or git-rev-list(1):
• You can’t feed commits piecemeal to these commands, one input for one
  output; they block until standard in is closed
• You can’t feed a list of possibly duplicate commits, like the output
  of git-last-modified(1); they effectively deduplicate the output

Beyond these two points there’s also the input massage problem: you cannot feed mixed input (revisions mixed with arbitrary text).

One might hope that git-cat-file(1) can save us. But it doesn’t support pretty formats.

But there is one command that already both handles revisions as arguments, revisions on standard input, and even revisions mixed in with arbitrary text. Namely git-name-rev(1).

Teach it to work in a format mode where the output for each revision is the pretty output (implies `--name-only`). This can be used to format any revision expression when given as arguments, and all full commit hashes in running text on stdin.

Just bring the hashes (to the pipeline). We will pretty print them.
Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
---
Notes (series):
    v2:
    • Propely implement “--format implies --name-only”
    • Don’t use a needless intermediary struct
    • Add a new member to `name_rev_usage[]`
    • FREE_AND_NULL notes string. There is no use-after-free but we have no
      reason to leave a freed pointer just laying there
    • Implement `--notes` for `%N` atom use (not just restrict to the default
      notes ref)
    • Previous review pointed out `init_revisions()` but this still just uses
      `REV_INFO_INIT` since it seemed enough. But I have no problem with
      changing it whatsoever. (This series is still very work-in-progress in
      any case.)
    • Tweak test name to mention the specific pretty format (reference); it
      doesn’t generalize to e.g. `oneline` because you get different output in
      that case
    
    Mostly from: https://lore.kernel.org/git/xmqq8qbvz2dm.fsf@gitster.g/
 Documentation/git-name-rev.adoc |  10 +++-
 builtin/name-rev.c              | 100 +++++++++++++++++++++++++++++---
 t/t6120-describe.sh             |  96 ++++++++++++++++++++++++++++++
 3 files changed, 198 insertions(+), 8 deletions(-)
Show changes to 3 files +198 −8

Documentation/git-name-rev.adoc, builtin/name-rev.c, t/t6120-describe.sh

diff --git a/Documentation/git-name-rev.adoc b/Documentation/git-name-rev.adoc
index d4f1c4d5945..65348690c8c 100644
--- a/Documentation/git-name-rev.adoc
+++ b/Documentation/git-name-rev.adoc
@@ -9,7 +9,7 @@ git-name-rev - Find symbolic names for given revs
 SYNOPSIS
 --------
 [verse]
-'git name-rev' [--tags] [--refs=<pattern>]
+'git name-rev' [--tags] [--refs=<pattern>] [--format=<pretty>]
 	       ( --all | --annotate-stdin | <commit-ish>... )
 
 DESCRIPTION
@@ -21,6 +21,14 @@ format parsable by 'git rev-parse'.
 OPTIONS
 -------
 
+--format=<pretty>::
+--no-format::
+	Format revisions instead of outputting symbolic names. The
+	default is `--no-format`.
++
+Implies `--name-only`. The negation `--no-format` implies
+`--no-name-only` (the default for the command).
+
 --tags::
 	Do not use branch names, but only tags to name the commits
 
diff --git a/builtin/name-rev.c b/builtin/name-rev.c
index 171e7bd0e98..9a008d8b7a8 100644
--- a/builtin/name-rev.c
+++ b/builtin/name-rev.c
@@ -18,6 +18,9 @@
 #include "commit-graph.h"
 #include "wildmatch.h"
 #include "mem-pool.h"
+#include "pretty.h"
+#include "revision.h"
+#include "notes.h"
 
 /*
  * One day.  See the 'name a rev shortly after epoch' test in t6120 when
@@ -33,6 +36,16 @@ struct rev_name {
 	int from_tag;
 };
 
+struct pretty_format {
+	struct pretty_print_context ctx;
+	struct userformat_want want;
+};
+
+struct format_cb_data {
+    const char *format;
+    int *name_only;
+};
+
 define_commit_slab(commit_rev_name, struct rev_name);
 
 static timestamp_t generation_cutoff = GENERATION_NUMBER_INFINITY;
@@ -454,7 +467,9 @@ static const char *get_exact_ref_match(const struct object *o)
 }
 
 /* may return a constant string or use "buf" as scratch space */
-static const char *get_rev_name(const struct object *o, struct strbuf *buf)
+static const char *get_rev_name(const struct object *o,
+				struct pretty_format *format_ctx,
+				struct strbuf *buf)
 {
 	struct rev_name *n;
 	const struct commit *c;
@@ -462,6 +477,25 @@ static const char *get_rev_name(const struct object *o, struct strbuf *buf)
 	if (o->type != OBJ_COMMIT)
 		return get_exact_ref_match(o);
 	c = (const struct commit *) o;
+
+	if (format_ctx) {
+		strbuf_reset(buf);
+
+		if (format_ctx->want.notes) {
+			struct strbuf notebuf = STRBUF_INIT;
+
+			format_display_notes(&c->object.oid, &notebuf,
+					     get_log_output_encoding(),
+					     format_ctx->ctx.fmt == CMIT_FMT_USERFORMAT);
+			format_ctx->ctx.notes_message = strbuf_detach(&notebuf, NULL);
+		}
+
+		pretty_print_commit(&format_ctx->ctx, c, buf);
+		FREE_AND_NULL(format_ctx->ctx.notes_message);
+
+		return buf->buf;
+	}
+
 	n = get_commit_rev_name(c);
 	if (!n)
 		return NULL;
@@ -479,6 +513,7 @@ static const char *get_rev_name(const struct object *o, struct strbuf *buf)
 
 static void show_name(const struct object *obj,
 		      const char *caller_name,
+		      struct pretty_format *format_ctx,
 		      int always, int allow_undefined, int name_only)
 {
 	const char *name;
@@ -487,7 +522,7 @@ static void show_name(const struct object *obj,
 
 	if (!name_only)
 		printf("%s ", caller_name ? caller_name : oid_to_hex(oid));
-	name = get_rev_name(obj, &buf);
+	name = get_rev_name(obj, format_ctx, &buf);
 	if (name)
 		printf("%s\n", name);
 	else if (allow_undefined)
@@ -507,7 +542,9 @@ static char const * const name_rev_usage[] = {
 	NULL
 };
 
-static void name_rev_line(char *p, struct name_ref_data *data)
+static void name_rev_line(char *p,
+			  struct name_ref_data *data,
+			  struct pretty_format *format_ctx)
 {
 	struct strbuf buf = STRBUF_INIT;
 	int counter = 0;
@@ -532,7 +569,7 @@ static void name_rev_line(char *p, struct name_ref_data *data)
 				struct object *o =
 					lookup_object(the_repository, &oid);
 				if (o)
-					name = get_rev_name(o, &buf);
+					name = get_rev_name(o, format_ctx, &buf);
 			}
 			*(p+1) = c;
 
@@ -554,6 +591,16 @@ static void name_rev_line(char *p, struct name_ref_data *data)
 	strbuf_release(&buf);
 }
 
+static int format_cb(const struct option *option,
+		     const char *arg,
+		     int unset)
+{
+	struct format_cb_data *data = option->value;
+	data->format = arg;
+	*data->name_only = !unset;
+	return 0;
+}
+
 int cmd_name_rev(int argc,
 		 const char **argv,
 		 const char *prefix,
@@ -567,6 +614,12 @@ int cmd_name_rev(int argc,
 #endif
 	int all = 0, annotate_stdin = 0, allow_undefined = 1, always = 0, peel_tag = 0;
 	struct name_ref_data data = { 0, 0, STRING_LIST_INIT_NODUP, STRING_LIST_INIT_NODUP };
+	static struct format_cb_data format_cb_data = { 0 };
+	struct display_notes_opt format_notes_opt;
+	struct rev_info format_rev = REV_INFO_INIT;
+	struct pretty_format *format_ctx = NULL;
+	struct pretty_format format_pp = { 0 };
+	struct string_list notes = STRING_LIST_INIT_NODUP;
 	struct option opts[] = {
 		OPT_BOOL(0, "name-only", &data.name_only, N_("print only ref-based names (no object names)")),
 		OPT_BOOL(0, "tags", &data.tags_only, N_("only use tags to name the commits")),
@@ -584,6 +637,10 @@ int cmd_name_rev(int argc,
 			   PARSE_OPT_HIDDEN),
 #endif /* WITH_BREAKING_CHANGES */
 		OPT_BOOL(0, "annotate-stdin", &annotate_stdin, N_("annotate text from stdin")),
+		OPT_CALLBACK(0, "format", &format_cb_data, N_("format"),
+			     N_("pretty-print output instead"), format_cb),
+		OPT_STRING_LIST(0, "notes", &notes, N_("notes"),
+				N_("display notes for --format")),
 		OPT_BOOL(0, "undefined", &allow_undefined, N_("allow to print `undefined` names (default)")),
 		OPT_BOOL(0, "always",     &always,
 			   N_("show abbreviated commit object as fallback")),
@@ -592,6 +649,8 @@ int cmd_name_rev(int argc,
 		OPT_END(),
 	};
 
+	init_display_notes(&format_notes_opt);
+	format_cb_data.name_only = &data.name_only;
 	mem_pool_init(&string_pool, 0);
 	init_commit_rev_name(&rev_names);
 	repo_config(the_repository, git_default_config, NULL);
@@ -606,6 +665,31 @@ int cmd_name_rev(int argc,
 	}
 #endif
 
+	if (format_cb_data.format) {
+		get_commit_format(format_cb_data.format, &format_rev);
+		format_pp.ctx.rev = &format_rev;
+		format_pp.ctx.fmt = format_rev.commit_format;
+		format_pp.ctx.abbrev = format_rev.abbrev;
+		format_pp.ctx.date_mode_explicit = format_rev.date_mode_explicit;
+		format_pp.ctx.date_mode = format_rev.date_mode;
+		format_pp.ctx.color = GIT_COLOR_AUTO;
+
+		userformat_find_requirements(format_cb_data.format,
+					     &format_pp.want);
+		if (format_pp.want.notes) {
+			int ignore_show_notes = 0;
+			struct string_list_item *n;
+
+			for_each_string_list_item(n, &notes)
+				enable_ref_display_notes(&format_notes_opt,
+							 &ignore_show_notes,
+							 n->string);
+			load_display_notes(&format_notes_opt);
+		}
+
+		format_ctx = &format_pp;
+	}
+
 	if (all + annotate_stdin + !!argc > 1) {
 		error("Specify either a list, or --all, not both!");
 		usage_with_options(name_rev_usage, opts);
@@ -663,7 +747,7 @@ int cmd_name_rev(int argc,
 
 		while (strbuf_getline(&sb, stdin) != EOF) {
 			strbuf_addch(&sb, '\n');
-			name_rev_line(sb.buf, &data);
+			name_rev_line(sb.buf, &data, format_ctx);
 		}
 		strbuf_release(&sb);
 	} else if (all) {
@@ -674,18 +758,20 @@ int cmd_name_rev(int argc,
 			struct object *obj = get_indexed_object(the_repository, i);
 			if (!obj || obj->type != OBJ_COMMIT)
 				continue;
-			show_name(obj, NULL,
+			show_name(obj, NULL, format_ctx,
 				  always, allow_undefined, data.name_only);
 		}
 	} else {
 		int i;
 		for (i = 0; i < revs.nr; i++)
-			show_name(revs.objects[i].item, revs.objects[i].name,
+			show_name(revs.objects[i].item, revs.objects[i].name, format_ctx,
 				  always, allow_undefined, data.name_only);
 	}
 
 	string_list_clear(&data.ref_filters, 0);
 	string_list_clear(&data.exclude_filters, 0);
+	string_list_clear(&notes, 0);
+	release_display_notes(&format_notes_opt);
 	mem_pool_discard(&string_pool, 0);
 	object_array_clear(&revs);
 	return 0;
diff --git a/t/t6120-describe.sh b/t/t6120-describe.sh
index 2c70cc561ad..0b7e9fe396d 100755
--- a/t/t6120-describe.sh
+++ b/t/t6120-describe.sh
@@ -658,6 +658,102 @@ test_expect_success 'name-rev --annotate-stdin works with commitGraph' '
 	)
 '
 
+test_expect_success 'name-rev --format setup' '
+	mkdir repo-format &&
+	git -C repo-format init &&
+	test_commit -C repo-format first &&
+	test_commit -C repo-format second &&
+	test_commit -C repo-format third &&
+	test_commit -C repo-format fourth &&
+	test_commit -C repo-format fifth &&
+	test_commit -C repo-format sixth &&
+	test_commit -C repo-format seventh &&
+	test_commit -C repo-format eighth
+'
+
+test_expect_success 'name-rev --format --no-name-only' '
+	cat >expect <<-\EOF &&
+	HEAD~3 [fifth]
+	HEAD [eighth]
+	HEAD~5 [third]
+	EOF
+	git -C repo-format name-rev --format="[%s]" \
+		--no-name-only HEAD~3 HEAD HEAD~5 >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'name-rev --format --no-format is the same as regular name-rev' '
+	git -C repo-format name-rev HEAD~2 HEAD~3 >expect &&
+	test_file_not_empty expect &&
+	git -C repo-format name-rev --format="huh?" \
+		--no-format HEAD~2 HEAD~3 >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'name-rev --format=%s for argument revs' '
+	cat >expect <<-\EOF &&
+	eighth
+	seventh
+	fifth
+	EOF
+	git -C repo-format name-rev --format=%s \
+		HEAD HEAD~ HEAD~3 >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success '--name-rev --format=reference --annotate-stdin from rev-list same as log' '
+	git -C repo-format log --format=reference >expect &&
+	test_file_not_empty expect &&
+	git -C repo-format rev-list HEAD >list &&
+	git -C repo-format name-rev --format=reference \
+		--annotate-stdin <list >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success '--name-rev --format=<pretty> --annotate-stdin with running text and tree oid' '
+	cmit_oid=$(git -C repo-format rev-parse :/fifth) &&
+	reference=$(git -C repo-format log -n1 --format=reference :/fifth) &&
+	tree=$(git -C repo-format rev-parse HEAD^{tree}) &&
+	cat >expect <<-EOF &&
+	We thought we fixed this in ${reference}.
+	But look at this tree: ${tree}.
+	EOF
+	git -C repo-format name-rev --format=reference --annotate-stdin \
+		>actual <<-EOF &&
+	We thought we fixed this in ${cmit_oid}.
+	But look at this tree: ${tree}.
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success 'name-rev --format=<pretty> with %N (note)' '
+	test_when_finished "git -C repo-format notes remove" &&
+	git -C repo-format notes add -m"Make a note" &&
+	printf "Make a note\n\n\n" >expect &&
+	git -C repo-format name-rev --format="tformat:%N" \
+		HEAD HEAD~ >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'name-rev --format=<pretty> --notes<ref>' '
+	# One custom notes ref
+	test_when_finished "git -C repo-format notes remove" &&
+	test_when_finished "git -C repo-format notes --ref=word remove" &&
+	git -C repo-format notes add -m"default" &&
+	git -C repo-format notes --ref=word add -m"custom" &&
+	printf "custom\n\n" >expect &&
+	git -C repo-format name-rev --format="tformat:%N" \
+		--notes=word \
+		HEAD >actual &&
+	test_cmp expect actual &&
+	# Glob all
+	printf "default\ncustom\n\n" >expect &&
+	git -C repo-format name-rev --format="tformat:%N" \
+		--notes=* \
+		HEAD >actual &&
+	test_cmp expect actual
+'
+
 #               B
 #               o
 #  H             \
-- 
2.53.0.32.gf6228eaf9cc
D. Ben KnobleMar 20, 2026, 15:25 UTC in reply to kristofferhaugsbakk@fastmail.com on lore

Re: [PATCH v2 2/2] name-rev: learn --format=<pretty>

On Fri, Mar 20, 2026 at 9:13 AM <kristofferhaugsbakk@fastmail.com> wrote:
Show 5 quoted lines
>
> From: Kristoffer Haugsbakk <code@khaugsbakk.name>
>
> Teach git-name-rev(1) to format the given revisions instead of creating
> symbolic names.
[snip]
Show 11 quoted lines
> ---
>
> Notes (series):
>     v2:
>     • Propely implement “--format implies --name-only”
>     • Don’t use a needless intermediary struct
>     • Add a new member to `name_rev_usage[]`
>     • FREE_AND_NULL notes string. There is no use-after-free but we have no
>       reason to leave a freed pointer just laying there
>     • Implement `--notes` for `%N` atom use (not just restrict to the default
>       notes ref)
[snip]
Show 27 quoted lines
> diff --git a/Documentation/git-name-rev.adoc b/Documentation/git-name-rev.adoc
> index d4f1c4d5945..65348690c8c 100644
> --- a/Documentation/git-name-rev.adoc
> +++ b/Documentation/git-name-rev.adoc
> @@ -9,7 +9,7 @@ git-name-rev - Find symbolic names for given revs
>  SYNOPSIS
>  --------
>  [verse]
> -'git name-rev' [--tags] [--refs=<pattern>]
> +'git name-rev' [--tags] [--refs=<pattern>] [--format=<pretty>]
>                ( --all | --annotate-stdin | <commit-ish>... )
>
>  DESCRIPTION
> @@ -21,6 +21,14 @@ format parsable by 'git rev-parse'.
>  OPTIONS
>  -------
>
> +--format=<pretty>::
> +--no-format::
> +       Format revisions instead of outputting symbolic names. The
> +       default is `--no-format`.
> ++
> +Implies `--name-only`. The negation `--no-format` implies
> +`--no-name-only` (the default for the command).
> +
>  --tags::
>         Do not use branch names, but only tags to name the commits
IIUC that this patch also adds --notes, should it be documented here?
Kristoffer HaugsbakkMar 23, 2026, 17:34 UTC in reply to D. Ben Knoble on lore

Re: [PATCH v2 2/2] name-rev: learn --format=<pretty>

On Fri, Mar 20, 2026, at 16:25, D. Ben Knoble wrote:
Show 13 quoted lines
>>[snip]
>> +--format=<pretty>::
>> +--no-format::
>> +       Format revisions instead of outputting symbolic names. The
>> +       default is `--no-format`.
>> ++
>> +Implies `--name-only`. The negation `--no-format` implies
>> +`--no-name-only` (the default for the command).
>> +
>>  --tags::
>>         Do not use branch names, but only tags to name the commits
>
> IIUC that this patch also adds --notes, should it be documented here?

Yes certainly. It dropped from my mind when doing the last session for this version.

Thanks for spotting.
kristofferhaugsbakk@fastmail.comApr 28, 2026, 22:25 UTC in reply to kristofferhaugsbakk@fastmail.com on lore

[PATCH v3 0/5] format-rev: introduce builtin for on-demand pretty formatting

From: Kristoffer Haugsbakk <code@khaugsbakk.name>
(Previous subject: name-rev: learn --format=<pretty>)
Topic name (applied): kh/name-rev-custom-format

Topic summary: Introduce a new builtin for pretty formatting one revision expression per line or commit object names found in running text.

See the last patch for the motivation. In short there isn’t anything that I have found that lets you format however many commits you want through one process (so looping over `git show` is excepted).

The other patches prepare for this change.
§ Changes in v3

Rewrite from a git-name-rev(1) option add-on to a new builtin. I make room in `builtin/name-rev.c` for it.

This was based on this part from the previous cover letter:
    git(1) already has a lot of commands. This doesn’t expand the footprint. On
    the other hand, I might be naive about what a proper formatter needs in
    terms of command options. Right now there is the helper `--notes`. But is
    more needed?
    • --abbrev ?
    • --date ?
    I don’t know. Maybe at some point this would be too much, too crowded, for
    git-name-rev(1). Then it might make sense to split it out to a separate
    builtin?

I did not want to expand this git-name-rev(1) command with more options specific to pretty formatting.

§ Why experimental command?

This command is marked Experimental three times (like git-replay(1)).[1] Not so much because the UI design seems difficult. It’s more so that it can be thrown out if it doesn’t end up being worth having around.

(*Experimental* also implies wholesale trashing. Right? That’s one possible change in behavior.)

Or the behavior here could be moved somewhere else.
† 1: I vaguely recall reading a good argument for emphasizing this on the
     mailing list. I wasn’t able to find back to it right now.
§ Outstanding work

I could look more into how to do the translation strings for “X is require” (arg), I’m probably missing something that already exists. I could probably also link to other commands on the git-format-rev(1) doc. But right now I wanted to get this out there. There might not be any appetite for builtin number 148 for this particular niche anyway.

§ Link to v2
https://lore.kernel.org/git/V2_CV_name-rev_--format.51b@msgid.xyz/

[1/5] name-rev: wrap both blocks in braces [2/5] name-rev: run clang-format before factoring code [3/5] name-rev: factor code for sharing with a new command [4/5] name-rev: make dedicated --annotate-stdin --name-only test [5/5] format-rev: introduce builtin for on-demand pretty formatting

 Documentation/git-format-rev.adoc | 148 +++++++++++++++++
 Makefile                          |   1 +
 builtin.h                         |   1 +
 builtin/name-rev.c                | 264 +++++++++++++++++++++++++++---
 command-list.txt                  |   1 +
 git.c                             |   1 +
 t/t1517-outside-repo.sh           |   3 +-
 t/t6120-describe.sh               | 118 +++++++++++++
 8 files changed, 511 insertions(+), 26 deletions(-)
 create mode 100644 Documentation/git-format-rev.adoc
Interdiff against v2:
Show changes to 9 files +510 −216

Documentation/git-format-rev.adoc, Documentation/git-name-rev.adoc, Makefile, builtin.h, builtin/name-rev.c, command-list.txt, git.c, t/t1517-outside-repo.sh, t/t6120-describe.sh

diff --git a/Documentation/git-format-rev.adoc b/Documentation/git-format-rev.adoc
new file mode 100644
index 00000000000..d960001d750
--- /dev/null
+++ b/Documentation/git-format-rev.adoc
@@ -0,0 +1,148 @@
+git-format-rev(1)
+=================
+
+NAME
+----
+git-format-rev - EXPERIMENTAL: Pretty format revisions on demand
+
+
+SYNOPSIS
+--------
+[synopsis]
+(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> --format=<pretty> [--notes=<ref>]
+
+DESCRIPTION
+-----------
+
+Pretty format revisions from standard input.
+
+THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
+
+OPTIONS
+-------
+
+`--stdin-mode=<mode>`::
+	How to interpret standard input data:
++
+--
+`revs`:: Each line is interpreted as a commit. Any kind of revision
+	expression can be used (see linkgit:gitrevisions[7]). Annotated
+	tags are peeled (see linkgit:gitglossary[7]).
++
+The argument `rev` is also accepted.
+`text`:: Formats all commit object names found in freeform text. These
+	must the full object names, i.e. abbreviated hexidecimal object
+	names will not be interpreted.
+--
+
+`--format=<pretty>`::
+	Pretty format string.
+
+`--notes=<ref>`::
+`--no-notes`::
+	Custom notes ref. Notes are displayed when using the `%N`
+	atom. See linkgit:git-notes[1].
+
+EXAMPLES
+--------
+
+The command linkgit:git-last-modified[1] shows the commit that each file
+was last modified in.
+
+----
+$ git last-modified -- README.md Makefile
+7798034171030be0909c56377a4e0e10e6d2df93	Makefile
+c50fbb2dd225e7e82abba4380423ae105089f4d7	README.md
+----
+
+We can pipe the result to this command in order to replace the object
+name with the commit author.
+
+----
+$ git last-modified -- README.md Makefile |
+    git format-rev --stdin-mode=text --format=%an
+Junio C Hamano	Makefile
+Todd Zullinger	README.md
+----
+
+Another example is _formatting commits in commit messages_. Given this commit message:
+
+----
+Fix off-by-one error
+
+Fix off-by-one error introduced in
+e83c5163316f89bfbde7d9ab23ca2e25604af290.
+
+We thought we fixed this in 5569bf9bbedd63a00780fc5c110e0cfab3aa97b9 but
+that only covered 1/3 of the faulty cases.
+----
+
+We can format the commits and use par(1) to reflow the text, say in a
+`commit-msg` hook:
+
+----
+$ git config set hook.reference-commits.event commit-msg
+$ git config set hook.reference-commits.command reference-commits
+$ cat $(which reference-commits)
+#/bin/sh
+
+msg="$1"
+rewritten=$(mktemp)
+git format-rev --stdin-mode=text --format=reference <"$msg" |
+    par >"$rewritten"
+mv "$rewritten" "$msg"
+----
+
+Which will produce something like this:
+
+----
+Fix off-by-one error
+
+Fix off-by-one error introduced in e83c5163316 (Implement better memory
+allocator, 2005-04-07).
+
+We thought we fixed this in 5569bf9bbed (Fix memory allocator,
+2005-06-22) but that only covered 1/3 of the faulty cases.
+----
+
+DISCUSSION
+----------
+
+This command lets you format any number of revisions in any order
+through one command invocation. Consider the
+linkgit:git-last-modified[1] case from the "EXAMPLES" section above:
+
+1. There might be hundreds of files
+2. Commits can be repeated, i.e. two or more files were last modified in
+   the same commit
+
+Two widely-used commands which pretty formats commits are
+linkgit:git-log[1] and linkgit:git-show[1]. It turns out that they are
+not a good fit for the above use case.
+
+- The output of linkgit:git-last-modified[1] would have to be processed
+  in stages since you need to transform the first column separately and
+  then link the author to the filename. But this is surmountable.
+- You can feed each commit to `git show` or `git show --no-walk -1`. But
+  that means that you need to create a process for each line.
+- Let’s say that you want to use one process, not one per line. So you
+  want to feed all the commits to the command. Now you face the problem
+  that you have to feed all the commits to the commands before you get
+  any output (this is also the case for the `--stdin` modes). In other
+  words, you cannot loop through each line, get the author for the
+  commit, and output the author and the filename. You need to feed all
+  the commits, get back all the output, and match the output with the
+  filename.
+- But the next problem is that commands will deduplicate the input and
+  only output one commit one single time only. Thus you cannot make the
+  output order match the input order, since a commit could have been
+  repeated in the original input.
+
+In short, it is straightforward to use these two commands if you use one
+process per line. It is much more work if you just want to use one
+process, but still doable. In contrast, this problem is just another
+shell pipeline with this command.
+
+GIT
+---
+Part of the linkgit:git[1] suite
diff --git a/Documentation/git-name-rev.adoc b/Documentation/git-name-rev.adoc
index 65348690c8c..d4f1c4d5945 100644
--- a/Documentation/git-name-rev.adoc
+++ b/Documentation/git-name-rev.adoc
@@ -9,7 +9,7 @@ git-name-rev - Find symbolic names for given revs
 SYNOPSIS
 --------
 [verse]
-'git name-rev' [--tags] [--refs=<pattern>] [--format=<pretty>]
+'git name-rev' [--tags] [--refs=<pattern>]
 	       ( --all | --annotate-stdin | <commit-ish>... )
 
 DESCRIPTION
@@ -21,14 +21,6 @@ format parsable by 'git rev-parse'.
 OPTIONS
 -------
 
---format=<pretty>::
---no-format::
-	Format revisions instead of outputting symbolic names. The
-	default is `--no-format`.
-+
-Implies `--name-only`. The negation `--no-format` implies
-`--no-name-only` (the default for the command).
-
 --tags::
 	Do not use branch names, but only tags to name the commits
 
diff --git a/Makefile b/Makefile
index 15b1ded1a0b..cbaf91fd846 100644
--- a/Makefile
+++ b/Makefile
@@ -895,6 +895,7 @@ BUILT_INS += $(patsubst builtin/%.o,git-%$X,$(BUILTIN_OBJS))
 BUILT_INS += git-cherry$X
 BUILT_INS += git-cherry-pick$X
 BUILT_INS += git-format-patch$X
+BUILT_INS += git-format-rev$X
 BUILT_INS += git-fsck-objects$X
 BUILT_INS += git-init$X
 BUILT_INS += git-maintenance$X
diff --git a/builtin.h b/builtin.h
index 235c51f30e5..63813c90125 100644
--- a/builtin.h
+++ b/builtin.h
@@ -189,6 +189,7 @@ int cmd_fmt_merge_msg(int argc, const char **argv, const char *prefix, struct re
 int cmd_for_each_ref(int argc, const char **argv, const char *prefix, struct repository *repo);
 int cmd_for_each_repo(int argc, const char **argv, const char *prefix, struct repository *repo);
 int cmd_format_patch(int argc, const char **argv, const char *prefix, struct repository *repo);
+int cmd_format_rev(int argc, const char **argv, const char *prefix, struct repository *repo);
 int cmd_fsck(int argc, const char **argv, const char *prefix, struct repository *repo);
 int cmd_fsmonitor__daemon(int argc, const char **argv, const char *prefix, struct repository *repo);
 int cmd_gc(int argc, const char **argv, const char *prefix, struct repository *repo);
diff --git a/builtin/name-rev.c b/builtin/name-rev.c
index 9a008d8b7a8..b60cc766279 100644
--- a/builtin/name-rev.c
+++ b/builtin/name-rev.c
@@ -36,16 +36,6 @@ struct rev_name {
 	int from_tag;
 };
 
-struct pretty_format {
-	struct pretty_print_context ctx;
-	struct userformat_want want;
-};
-
-struct format_cb_data {
-    const char *format;
-    int *name_only;
-};
-
 define_commit_slab(commit_rev_name, struct rev_name);
 
 static timestamp_t generation_cutoff = GENERATION_NUMBER_INFINITY;
@@ -285,6 +275,43 @@ struct name_ref_data {
 	struct string_list exclude_filters;
 };
 
+struct pretty_format {
+	struct pretty_print_context ctx;
+	struct userformat_want want;
+};
+
+enum command_type {
+	NAME_REV = 1,
+	FORMAT_REV = 2,
+};
+
+enum stdin_mode {
+    TEXT = 1,
+    REVS = 2,
+};
+
+struct command {
+	enum command_type type;
+	union {
+		int name_only;
+		struct pretty_format *pretty_format;
+	} u;
+};
+
+static void init_name_rev_command(struct command *cmd,
+				  int name_only)
+{
+	cmd->type = NAME_REV;
+	cmd->u.name_only = name_only;
+}
+
+static void init_format_rev_command(struct command *cmd,
+				    struct pretty_format *pretty_format)
+{
+	cmd->type = FORMAT_REV;
+	cmd->u.pretty_format = pretty_format;
+}
+
 static struct tip_table {
 	struct tip_table_entry {
 		struct object_id oid;
@@ -467,9 +494,7 @@ static const char *get_exact_ref_match(const struct object *o)
 }
 
 /* may return a constant string or use "buf" as scratch space */
-static const char *get_rev_name(const struct object *o,
-				struct pretty_format *format_ctx,
-				struct strbuf *buf)
+static const char *get_rev_name(const struct object *o, struct strbuf *buf)
 {
 	struct rev_name *n;
 	const struct commit *c;
@@ -477,25 +502,6 @@ static const char *get_rev_name(const struct object *o,
 	if (o->type != OBJ_COMMIT)
 		return get_exact_ref_match(o);
 	c = (const struct commit *) o;
-
-	if (format_ctx) {
-		strbuf_reset(buf);
-
-		if (format_ctx->want.notes) {
-			struct strbuf notebuf = STRBUF_INIT;
-
-			format_display_notes(&c->object.oid, &notebuf,
-					     get_log_output_encoding(),
-					     format_ctx->ctx.fmt == CMIT_FMT_USERFORMAT);
-			format_ctx->ctx.notes_message = strbuf_detach(&notebuf, NULL);
-		}
-
-		pretty_print_commit(&format_ctx->ctx, c, buf);
-		FREE_AND_NULL(format_ctx->ctx.notes_message);
-
-		return buf->buf;
-	}
-
 	n = get_commit_rev_name(c);
 	if (!n)
 		return NULL;
@@ -511,9 +517,29 @@ static const char *get_rev_name(const struct object *o,
 	}
 }
 
+static const char *get_format_rev(const struct commit *c,
+				  struct pretty_format *format_ctx,
+				  struct strbuf *buf)
+{
+	strbuf_reset(buf);
+
+	if (format_ctx->want.notes) {
+		struct strbuf notebuf = STRBUF_INIT;
+
+		format_display_notes(&c->object.oid, &notebuf,
+				     get_log_output_encoding(),
+				     format_ctx->ctx.fmt == CMIT_FMT_USERFORMAT);
+		format_ctx->ctx.notes_message = strbuf_detach(&notebuf, NULL);
+	}
+
+	pretty_print_commit(&format_ctx->ctx, c, buf);
+	FREE_AND_NULL(format_ctx->ctx.notes_message);
+
+	return buf->buf;
+}
+
 static void show_name(const struct object *obj,
 		      const char *caller_name,
-		      struct pretty_format *format_ctx,
 		      int always, int allow_undefined, int name_only)
 {
 	const char *name;
@@ -522,7 +548,7 @@ static void show_name(const struct object *obj,
 
 	if (!name_only)
 		printf("%s ", caller_name ? caller_name : oid_to_hex(oid));
-	name = get_rev_name(obj, format_ctx, &buf);
+	name = get_rev_name(obj, &buf);
 	if (name)
 		printf("%s\n", name);
 	else if (allow_undefined)
@@ -542,9 +568,7 @@ static char const * const name_rev_usage[] = {
 	NULL
 };
 
-static void name_rev_line(char *p,
-			  struct name_ref_data *data,
-			  struct pretty_format *format_ctx)
+static void name_rev_line(char *p, struct command *cmd)
 {
 	struct strbuf buf = STRBUF_INIT;
 	int counter = 0;
@@ -553,33 +577,54 @@ static void name_rev_line(char *p,
 
 	for (p_start = p; *p; p++) {
 #define ishex(x) (isdigit((x)) || ((x) >= 'a' && (x) <= 'f'))
+	start:
 		if (!ishex(*p)) {
 			counter = 0;
 		} else if (++counter == hexsz &&
-			 !ishex(*(p+1))) {
+			   !ishex(*(p + 1))) {
 			struct object_id oid;
 			const char *name = NULL;
-			char c = *(p+1);
+			char c = *(p + 1);
 			int p_len = p - p_start + 1;
+			struct object *o = NULL;
+			int oid_ret = 1;
 
 			counter = 0;
 
-			*(p+1) = 0;
-			if (!repo_get_oid(the_repository, p - (hexsz - 1), &oid)) {
-				struct object *o =
-					lookup_object(the_repository, &oid);
+			*(p + 1) = 0;
+			oid_ret = repo_get_oid(the_repository, p - (hexsz - 1), &oid);
+
+			switch (cmd->type) {
+			case NAME_REV:
+				if (!oid_ret)
+					o = lookup_object(the_repository, &oid);
 				if (o)
-					name = get_rev_name(o, format_ctx, &buf);
+					name = get_rev_name(o, &buf);
+				*(p + 1) = c;
+				if (!name)
+					goto start;
+				if (cmd->u.name_only)
+					printf("%.*s%s", p_len - hexsz, p_start, name);
+				else
+					printf("%.*s (%s)", p_len, p_start, name);
+				break;
+			case FORMAT_REV:
+				if (!oid_ret)
+					o = parse_object(the_repository, &oid);
+				if (o && o->type == OBJ_COMMIT)
+					name = get_format_rev((const struct commit *)o,
+							      cmd->u.pretty_format,
+							      &buf);
+				*(p + 1) = c;
+				if (name)
+					printf("%.*s%s", p_len - hexsz, p_start, name);
+				else
+					printf("%.*s", p_len, p_start);
+				break;
+			default:
+				BUG("uncovered case: %d", cmd->type);
 			}
-			*(p+1) = c;
 
-			if (!name)
-				continue;
-
-			if (data->name_only)
-				printf("%.*s%s", p_len - hexsz, p_start, name);
-			else
-				printf("%.*s (%s)", p_len, p_start, name);
 			p_start = p + 1;
 		}
 	}
@@ -591,16 +636,6 @@ static void name_rev_line(char *p,
 	strbuf_release(&buf);
 }
 
-static int format_cb(const struct option *option,
-		     const char *arg,
-		     int unset)
-{
-	struct format_cb_data *data = option->value;
-	data->format = arg;
-	*data->name_only = !unset;
-	return 0;
-}
-
 int cmd_name_rev(int argc,
 		 const char **argv,
 		 const char *prefix,
@@ -614,19 +649,14 @@ int cmd_name_rev(int argc,
 #endif
 	int all = 0, annotate_stdin = 0, allow_undefined = 1, always = 0, peel_tag = 0;
 	struct name_ref_data data = { 0, 0, STRING_LIST_INIT_NODUP, STRING_LIST_INIT_NODUP };
-	static struct format_cb_data format_cb_data = { 0 };
-	struct display_notes_opt format_notes_opt;
-	struct rev_info format_rev = REV_INFO_INIT;
-	struct pretty_format *format_ctx = NULL;
-	struct pretty_format format_pp = { 0 };
-	struct string_list notes = STRING_LIST_INIT_NODUP;
+	struct command cmd;
 	struct option opts[] = {
 		OPT_BOOL(0, "name-only", &data.name_only, N_("print only ref-based names (no object names)")),
 		OPT_BOOL(0, "tags", &data.tags_only, N_("only use tags to name the commits")),
 		OPT_STRING_LIST(0, "refs", &data.ref_filters, N_("pattern"),
-				   N_("only use refs matching <pattern>")),
+				N_("only use refs matching <pattern>")),
 		OPT_STRING_LIST(0, "exclude", &data.exclude_filters, N_("pattern"),
-				   N_("ignore refs matching <pattern>")),
+				N_("ignore refs matching <pattern>")),
 		OPT_GROUP(""),
 		OPT_BOOL(0, "all", &all, N_("list all commits reachable from all refs")),
 #ifndef WITH_BREAKING_CHANGES
@@ -637,24 +667,19 @@ int cmd_name_rev(int argc,
 			   PARSE_OPT_HIDDEN),
 #endif /* WITH_BREAKING_CHANGES */
 		OPT_BOOL(0, "annotate-stdin", &annotate_stdin, N_("annotate text from stdin")),
-		OPT_CALLBACK(0, "format", &format_cb_data, N_("format"),
-			     N_("pretty-print output instead"), format_cb),
-		OPT_STRING_LIST(0, "notes", &notes, N_("notes"),
-				N_("display notes for --format")),
 		OPT_BOOL(0, "undefined", &allow_undefined, N_("allow to print `undefined` names (default)")),
-		OPT_BOOL(0, "always",     &always,
-			   N_("show abbreviated commit object as fallback")),
+		OPT_BOOL(0, "always", &always,
+			 N_("show abbreviated commit object as fallback")),
 		OPT_HIDDEN_BOOL(0, "peel-tag", &peel_tag,
-			   N_("dereference tags in the input (internal use)")),
+				N_("dereference tags in the input (internal use)")),
 		OPT_END(),
 	};
 
-	init_display_notes(&format_notes_opt);
-	format_cb_data.name_only = &data.name_only;
 	mem_pool_init(&string_pool, 0);
 	init_commit_rev_name(&rev_names);
 	repo_config(the_repository, git_default_config, NULL);
 	argc = parse_options(argc, argv, prefix, opts, name_rev_usage, 0);
+	init_name_rev_command(&cmd, data.name_only);
 
 #ifndef WITH_BREAKING_CHANGES
 	if (transform_stdin) {
@@ -665,31 +690,6 @@ int cmd_name_rev(int argc,
 	}
 #endif
 
-	if (format_cb_data.format) {
-		get_commit_format(format_cb_data.format, &format_rev);
-		format_pp.ctx.rev = &format_rev;
-		format_pp.ctx.fmt = format_rev.commit_format;
-		format_pp.ctx.abbrev = format_rev.abbrev;
-		format_pp.ctx.date_mode_explicit = format_rev.date_mode_explicit;
-		format_pp.ctx.date_mode = format_rev.date_mode;
-		format_pp.ctx.color = GIT_COLOR_AUTO;
-
-		userformat_find_requirements(format_cb_data.format,
-					     &format_pp.want);
-		if (format_pp.want.notes) {
-			int ignore_show_notes = 0;
-			struct string_list_item *n;
-
-			for_each_string_list_item(n, &notes)
-				enable_ref_display_notes(&format_notes_opt,
-							 &ignore_show_notes,
-							 n->string);
-			load_display_notes(&format_notes_opt);
-		}
-
-		format_ctx = &format_pp;
-	}
-
 	if (all + annotate_stdin + !!argc > 1) {
 		error("Specify either a list, or --all, not both!");
 		usage_with_options(name_rev_usage, opts);
@@ -747,7 +747,7 @@ int cmd_name_rev(int argc,
 
 		while (strbuf_getline(&sb, stdin) != EOF) {
 			strbuf_addch(&sb, '\n');
-			name_rev_line(sb.buf, &data, format_ctx);
+			name_rev_line(sb.buf, &cmd);
 		}
 		strbuf_release(&sb);
 	} else if (all) {
@@ -758,21 +758,149 @@ int cmd_name_rev(int argc,
 			struct object *obj = get_indexed_object(the_repository, i);
 			if (!obj || obj->type != OBJ_COMMIT)
 				continue;
-			show_name(obj, NULL, format_ctx,
+			show_name(obj, NULL,
 				  always, allow_undefined, data.name_only);
 		}
 	} else {
 		int i;
 		for (i = 0; i < revs.nr; i++)
-			show_name(revs.objects[i].item, revs.objects[i].name, format_ctx,
+			show_name(revs.objects[i].item, revs.objects[i].name,
 				  always, allow_undefined, data.name_only);
 	}
 
 	string_list_clear(&data.ref_filters, 0);
 	string_list_clear(&data.exclude_filters, 0);
-	string_list_clear(&notes, 0);
-	release_display_notes(&format_notes_opt);
 	mem_pool_discard(&string_pool, 0);
 	object_array_clear(&revs);
 	return 0;
 }
+
+static enum stdin_mode parse_stdin_mode(const char *stdin_mode)
+{
+	if (!strcmp(stdin_mode, "text"))
+		return TEXT;
+	else if (!strcmp(stdin_mode, "revs") ||
+		 !strcmp(stdin_mode, "rev"))
+		return REVS;
+	else
+		die(_("'%s' needs to be either text, revs, or rev"),
+		    "--stdin-mode");
+}
+
+static char const *const format_rev_usage[] = {
+	N_("(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> --format=<pretty> [--notes=<ref>]"),
+	NULL
+};
+
+int cmd_format_rev(int argc,
+		   const char **argv,
+		   const char *prefix,
+		   struct repository *repo UNUSED)
+{
+	const char *format = NULL;
+	enum stdin_mode stdin_mode;
+	const char *stdin_mode_arg = NULL;
+	struct display_notes_opt format_notes_opt;
+	struct rev_info format_rev = REV_INFO_INIT;
+	struct pretty_format format_pp = { 0 };
+	struct string_list notes = STRING_LIST_INIT_NODUP;
+	struct strbuf scratch_buf = STRBUF_INIT;
+	struct command cmd;
+	struct option opts[] = {
+		OPT_STRING(0, "format", &format, N_("format"),
+			   N_("pretty format to use")),
+		OPT_STRING(0, "stdin-mode", &stdin_mode_arg, N_("stdin-mode"),
+			   N_("how revs are processed")),
+		OPT_STRING_LIST(0, "notes", &notes, N_("notes"),
+				N_("display notes for pretty format")),
+		OPT_END(),
+	};
+
+	argc = parse_options(argc, argv, prefix, opts, format_rev_usage, 0);
+
+	if (argc > 0) {
+		error(_("too many arguments"));
+		usage_with_options(format_rev_usage, opts);
+	}
+
+	if (!format)
+		die(_("'%s' is required"), "--format");
+	if (!stdin_mode_arg)
+		die(_("'%s' is required"), "--stdin-mode");
+
+	init_display_notes(&format_notes_opt);
+	stdin_mode = parse_stdin_mode(stdin_mode_arg);
+
+	get_commit_format(format, &format_rev);
+	format_pp.ctx.rev = &format_rev;
+	format_pp.ctx.fmt = format_rev.commit_format;
+	format_pp.ctx.abbrev = format_rev.abbrev;
+	format_pp.ctx.date_mode_explicit = format_rev.date_mode_explicit;
+	format_pp.ctx.date_mode = format_rev.date_mode;
+	format_pp.ctx.color = GIT_COLOR_AUTO;
+
+	userformat_find_requirements(format,
+				     &format_pp.want);
+	if (format_pp.want.notes) {
+		int ignore_show_notes = 0;
+		struct string_list_item *n;
+
+		for_each_string_list_item(n, &notes)
+			enable_ref_display_notes(&format_notes_opt,
+						 &ignore_show_notes,
+						 n->string);
+		load_display_notes(&format_notes_opt);
+	}
+
+	init_format_rev_command(&cmd, &format_pp);
+
+	switch (stdin_mode) {
+	case TEXT:
+		while (strbuf_getline(&scratch_buf, stdin) != EOF) {
+			strbuf_addch(&scratch_buf, '\n');
+			name_rev_line(scratch_buf.buf, &cmd);
+		}
+		break;
+	case REVS:
+		while (strbuf_getline(&scratch_buf, stdin) != EOF) {
+			struct object_id oid;
+			struct object *object;
+			struct object *peeled;
+			struct commit *commit;
+
+			if (repo_get_oid(the_repository, scratch_buf.buf, &oid)) {
+				fprintf(stderr, "Could not get sha1 for %s. Skipping.\n",
+					scratch_buf.buf);
+				continue;
+			}
+
+			object = parse_object(the_repository, &oid);
+			if (!object) {
+				fprintf(stderr, "Could not get object for %s. Skipping.\n",
+					scratch_buf.buf);
+				continue;
+			}
+
+			peeled = deref_tag(the_repository, object, scratch_buf.buf, 0);
+			if (peeled && peeled->type == OBJ_COMMIT)
+				commit = (struct commit *)peeled;
+			if (!commit) {
+				fprintf(stderr, "Could not get commit for %s. Skipping.\n",
+					*argv);
+				continue;
+			}
+
+			get_format_rev(commit, &format_pp, &scratch_buf);
+			printf("%s\n", scratch_buf.buf);
+			strbuf_release(&scratch_buf);
+		}
+		break;
+	default:
+		BUG("uncovered case: %d", stdin_mode);
+	}
+
+	strbuf_release(&scratch_buf);
+	string_list_clear(&notes, 0);
+	release_display_notes(&format_notes_opt);
+	return 0;
+}
diff --git a/command-list.txt b/command-list.txt
index f9005cf4597..df729872dca 100644
--- a/command-list.txt
+++ b/command-list.txt
@@ -108,6 +108,7 @@ git-fmt-merge-msg                       purehelpers
 git-for-each-ref                        plumbinginterrogators
 git-for-each-repo                       plumbinginterrogators
 git-format-patch                        mainporcelain
+git-format-rev                          plumbinginterrogators
 git-fsck                                ancillaryinterrogators          complete
 git-gc                                  mainporcelain
 git-get-tar-commit-id                   plumbinginterrogators
diff --git a/git.c b/git.c
index 2b212e6675d..af5b0422b00 100644
--- a/git.c
+++ b/git.c
@@ -578,6 +578,7 @@ static struct cmd_struct commands[] = {
 	{ "for-each-ref", cmd_for_each_ref, RUN_SETUP },
 	{ "for-each-repo", cmd_for_each_repo, RUN_SETUP_GENTLY },
 	{ "format-patch", cmd_format_patch, RUN_SETUP },
+	{ "format-rev", cmd_format_rev, RUN_SETUP },
 	{ "fsck", cmd_fsck, RUN_SETUP },
 	{ "fsck-objects", cmd_fsck, RUN_SETUP },
 	{ "fsmonitor--daemon", cmd_fsmonitor__daemon, RUN_SETUP },
diff --git a/t/t1517-outside-repo.sh b/t/t1517-outside-repo.sh
index c824c1a25cf..360a9323343 100755
--- a/t/t1517-outside-repo.sh
+++ b/t/t1517-outside-repo.sh
@@ -114,7 +114,8 @@ do
 	archimport | citool | credential-netrc | credential-libsecret | \
 	credential-osxkeychain | cvsexportcommit | cvsimport | cvsserver | \
 	daemon | \
-	difftool--helper | filter-branch | fsck-objects | get-tar-commit-id | \
+	difftool--helper | filter-branch | format-rev | fsck-objects | \
+	get-tar-commit-id | \
 	gui | gui--askpass | \
 	http-backend | http-fetch | http-push | init-db | \
 	merge-octopus | merge-one-file | merge-resolve | mergetool | \
diff --git a/t/t6120-describe.sh b/t/t6120-describe.sh
index 0b7e9fe396d..725f7d81b6b 100755
--- a/t/t6120-describe.sh
+++ b/t/t6120-describe.sh
@@ -298,6 +298,20 @@ test_expect_success 'name-rev --annotate-stdin' '
 	test_cmp expect actual
 '
 
+test_expect_success 'name-rev --annotate-stdin --name-only' '
+	>expect.unsorted &&
+	for rev in $(git rev-list --all)
+	do
+		name=$(git name-rev --name-only $rev) &&
+		echo "$name" >>expect.unsorted || return 1
+	done &&
+	sort <expect.unsorted >expect &&
+	git name-rev --annotate-stdin --name-only \
+		<list >actual.unsorted &&
+	sort <actual.unsorted >actual &&
+	test_cmp expect actual
+'
+
 test_expect_success 'name-rev --stdin deprecated' '
 	git rev-list --all >list &&
 	if ! test_have_prereq WITH_BREAKING_CHANGES
@@ -658,102 +672,6 @@ test_expect_success 'name-rev --annotate-stdin works with commitGraph' '
 	)
 '
 
-test_expect_success 'name-rev --format setup' '
-	mkdir repo-format &&
-	git -C repo-format init &&
-	test_commit -C repo-format first &&
-	test_commit -C repo-format second &&
-	test_commit -C repo-format third &&
-	test_commit -C repo-format fourth &&
-	test_commit -C repo-format fifth &&
-	test_commit -C repo-format sixth &&
-	test_commit -C repo-format seventh &&
-	test_commit -C repo-format eighth
-'
-
-test_expect_success 'name-rev --format --no-name-only' '
-	cat >expect <<-\EOF &&
-	HEAD~3 [fifth]
-	HEAD [eighth]
-	HEAD~5 [third]
-	EOF
-	git -C repo-format name-rev --format="[%s]" \
-		--no-name-only HEAD~3 HEAD HEAD~5 >actual &&
-	test_cmp expect actual
-'
-
-test_expect_success 'name-rev --format --no-format is the same as regular name-rev' '
-	git -C repo-format name-rev HEAD~2 HEAD~3 >expect &&
-	test_file_not_empty expect &&
-	git -C repo-format name-rev --format="huh?" \
-		--no-format HEAD~2 HEAD~3 >actual &&
-	test_cmp expect actual
-'
-
-test_expect_success 'name-rev --format=%s for argument revs' '
-	cat >expect <<-\EOF &&
-	eighth
-	seventh
-	fifth
-	EOF
-	git -C repo-format name-rev --format=%s \
-		HEAD HEAD~ HEAD~3 >actual &&
-	test_cmp expect actual
-'
-
-test_expect_success '--name-rev --format=reference --annotate-stdin from rev-list same as log' '
-	git -C repo-format log --format=reference >expect &&
-	test_file_not_empty expect &&
-	git -C repo-format rev-list HEAD >list &&
-	git -C repo-format name-rev --format=reference \
-		--annotate-stdin <list >actual &&
-	test_cmp expect actual
-'
-
-test_expect_success '--name-rev --format=<pretty> --annotate-stdin with running text and tree oid' '
-	cmit_oid=$(git -C repo-format rev-parse :/fifth) &&
-	reference=$(git -C repo-format log -n1 --format=reference :/fifth) &&
-	tree=$(git -C repo-format rev-parse HEAD^{tree}) &&
-	cat >expect <<-EOF &&
-	We thought we fixed this in ${reference}.
-	But look at this tree: ${tree}.
-	EOF
-	git -C repo-format name-rev --format=reference --annotate-stdin \
-		>actual <<-EOF &&
-	We thought we fixed this in ${cmit_oid}.
-	But look at this tree: ${tree}.
-	EOF
-	test_cmp expect actual
-'
-
-test_expect_success 'name-rev --format=<pretty> with %N (note)' '
-	test_when_finished "git -C repo-format notes remove" &&
-	git -C repo-format notes add -m"Make a note" &&
-	printf "Make a note\n\n\n" >expect &&
-	git -C repo-format name-rev --format="tformat:%N" \
-		HEAD HEAD~ >actual &&
-	test_cmp expect actual
-'
-
-test_expect_success 'name-rev --format=<pretty> --notes<ref>' '
-	# One custom notes ref
-	test_when_finished "git -C repo-format notes remove" &&
-	test_when_finished "git -C repo-format notes --ref=word remove" &&
-	git -C repo-format notes add -m"default" &&
-	git -C repo-format notes --ref=word add -m"custom" &&
-	printf "custom\n\n" >expect &&
-	git -C repo-format name-rev --format="tformat:%N" \
-		--notes=word \
-		HEAD >actual &&
-	test_cmp expect actual &&
-	# Glob all
-	printf "default\ncustom\n\n" >expect &&
-	git -C repo-format name-rev --format="tformat:%N" \
-		--notes=* \
-		HEAD >actual &&
-	test_cmp expect actual
-'
-
 #               B
 #               o
 #  H             \
@@ -883,4 +801,108 @@ test_expect_success 'do not be fooled by invalid describe format ' '
 	test_must_fail git cat-file -t "refs/tags/super-invalid/./../...../ ~^:/?*[////\\\\\\&}/busted.lock-42-g"$(cat out)
 '
 
+test_expect_success 'name-rev --format setup' '
+	mkdir repo-format &&
+	git -C repo-format init &&
+	test_commit -C repo-format first &&
+	test_commit -C repo-format second &&
+	test_commit -C repo-format third &&
+	test_commit -C repo-format fourth &&
+	test_commit -C repo-format fifth &&
+	test_commit -C repo-format sixth &&
+	test_commit -C repo-format seventh &&
+	test_commit -C repo-format eighth
+'
+
+test_expect_success 'format-rev --stdin-mode=revs' '
+	cat >expect <<-\EOF &&
+	eighth
+	seventh
+	fifth
+	EOF
+	git -C repo-format format-rev --stdin-mode=revs \
+		--format=%s >actual <<-\EOF &&
+	HEAD
+	HEAD~
+	HEAD~3
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success 'format-rev --stdin-mode=text from rev-list same as log' '
+	git -C repo-format log --format=reference >expect &&
+	test_file_not_empty expect &&
+	git -C repo-format rev-list HEAD >list &&
+	git -C repo-format format-rev --stdin-mode=text \
+		--format=reference <list >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'format-rev --stdin-mode=text with running text and tree oid' '
+	cmit_oid=$(git -C repo-format rev-parse :/fifth) &&
+	reference=$(git -C repo-format log -n1 --format=reference :/fifth) &&
+	tree=$(git -C repo-format rev-parse HEAD^{tree}) &&
+	cat >expect <<-EOF &&
+	We thought we fixed this in ${reference}.
+	But look at this tree: ${tree}.
+	EOF
+	git -C repo-format format-rev --stdin-mode=text --format=reference \
+		>actual <<-EOF &&
+	We thought we fixed this in ${cmit_oid}.
+	But look at this tree: ${tree}.
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success 'format-rev with %N (note)' '
+	test_when_finished "git -C repo-format notes remove" &&
+	git -C repo-format notes add -m"Make a note" &&
+	printf "Make a note\n\n\n" >expect &&
+	git -C repo-format format-rev --stdin-mode=revs \
+		--format="tformat:%N" \
+		>actual <<-\EOF &&
+	HEAD
+	HEAD~
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success 'format-rev --notes<ref> (custom notes ref)' '
+	# One custom notes ref
+	test_when_finished "git -C repo-format notes remove" &&
+	test_when_finished "git -C repo-format notes --ref=word remove" &&
+	git -C repo-format notes add -m"default" &&
+	git -C repo-format notes --ref=word add -m"custom" &&
+	printf "custom\n\n" >expect &&
+	git -C repo-format format-rev --stdin-mode=revs \
+		--format="tformat:%N" \
+		--notes=word \
+		>actual <<-\EOF &&
+	HEAD
+	EOF
+	test_cmp expect actual &&
+	# Glob all
+	printf "default\ncustom\n\n" >expect &&
+	git -C repo-format format-rev --stdin-mode=revs \
+		--format="tformat:%N" \
+		--notes=* >actual <<-\EOF &&
+	HEAD
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success 'format-rev --stdin-mode=revs on annotated tag peels to commit' '
+	test_when_finished "git -C repo-format tag -d version" &&
+	git -C repo-format tag -a -m"new version" version &&
+	cat >expect <<-\EOF &&
+	eighth
+	EOF
+	git -C repo-format format-rev --stdin-mode=revs \
+		--format=%s \
+		--notes=* >actual <<-\EOF &&
+	version
+	EOF
+	test_cmp expect actual
+'
+
 test_done
Range-diff against v2:
1:  9cb5cfd1ec3 = 1:  9cb5cfd1ec3 name-rev: wrap both blocks in braces
-:  ----------- > 2:  14900271321 name-rev: run clang-format before factoring code
-:  ----------- > 3:  1f17f0b0090 name-rev: factor code for sharing with a new command
-:  ----------- > 4:  40989b3672a name-rev: make dedicated --annotate-stdin --name-only test
2:  52a52060776 ! 5:  bb54e8f753e name-rev: learn --format=<pretty>
    @@ Metadata
     Author: Kristoffer Haugsbakk <code@khaugsbakk.name>
     
      ## Commit message ##
    -    name-rev: learn --format=<pretty>
    +    format-rev: introduce builtin for on-demand pretty formatting
     
    -    Teach git-name-rev(1) to format the given revisions instead of creating
    -    symbolic names.
    +    Introduce a new builtin for pretty formatting one revision expression
    +    per line or commit object names found in running text.
     
    -    Sometimes you want to format commits. Most of the time you’re walking
    -    the graph, e.g. getting a range of commits like `master..topic`. That’s
    -    a job for git-log(1).
    +    Sometimes you want to format commits. Most of the time you’re
    +    walking the graph, e.g. getting a range of commits like
    +    `master..topic`. That’s a job for git-log(1).
     
    -    But sometimes you might want to format commits that you encounter
    +    But there are times when you want to format commits that you encounter
         on demand:
     
         • Full hashes in running text that you might want to pretty-print
    -    • git-last-modified(1) outputs full hashes that you can do the same with
    +    • git-last-modified(1) outputs full hashes that you can do the same
    +      with
         • git-cherry(1) has `-v` for commit subject, but maybe you want
           something else?
     
         But now you can’t use git-log(1), git-show(1), or git-rev-list(1):
     
    -    • You can’t feed commits piecemeal to these commands, one input for one
    -      output; they block until standard in is closed
    +    • You can’t feed commits piecemeal to these commands, one input
    +      for one output; they block until standard in is closed
         • You can’t feed a list of possibly duplicate commits, like the output
           of git-last-modified(1); they effectively deduplicate the output
     
         Beyond these two points there’s also the input massage problem: you
         cannot feed mixed input (revisions mixed with arbitrary text).
     
    -    One might hope that git-cat-file(1) can save us. But it doesn’t support
    -    pretty formats.
    +    One might hope that git-cat-file(1) can save us. But it doesn’t
    +    support pretty formats.
     
         But there is one command that already both handles revisions as
    -    arguments, revisions on standard input, and even revisions mixed
    -    in with arbitrary text. Namely git-name-rev(1).
    +    arguments, revisions on standard input, and even revisions mixed in
    +    with arbitrary text. Namely git-name-rev(1): the command for outputting
    +    symbolic names for commits.
     
    -    Teach it to work in a format mode where the output for each revision is
    -    the pretty output (implies `--name-only`). This can be used to format
    -    any revision expression when given as arguments, and all full commit
    -    hashes in running text on stdin.
    +    We made some room in `builtin/name-rev.c` two commits ago. Let’s
    +    now add this new git-format-rev(1) command. Taking inspiration from
    +    git-name-rev(1), there are two modes:
     
    -    Just bring the hashes (to the pipeline). We will pretty print them.
    +    • revs: like git-name-rev(1) in argv mode, but one revision per line
    +      on standard in
    +    • text: like git-name-rev(1) with `--annotate-stdin`
    +
    +    ***
    +
    +    We need to add this command to the exception list in
    +    `t/t1517-outside-repo.sh` because it uses “EXPERIMENTAL!”
    +    in the usage line.
     
         Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
     
    - ## Documentation/git-name-rev.adoc ##
    -@@ Documentation/git-name-rev.adoc: git-name-rev - Find symbolic names for given revs
    - SYNOPSIS
    - --------
    - [verse]
    --'git name-rev' [--tags] [--refs=<pattern>]
    -+'git name-rev' [--tags] [--refs=<pattern>] [--format=<pretty>]
    - 	       ( --all | --annotate-stdin | <commit-ish>... )
    - 
    - DESCRIPTION
    -@@ Documentation/git-name-rev.adoc: format parsable by 'git rev-parse'.
    - OPTIONS
    - -------
    - 
    -+--format=<pretty>::
    -+--no-format::
    -+	Format revisions instead of outputting symbolic names. The
    -+	default is `--no-format`.
    + ## Documentation/git-format-rev.adoc (new) ##
    +@@
    ++git-format-rev(1)
    ++=================
    ++
    ++NAME
    ++----
    ++git-format-rev - EXPERIMENTAL: Pretty format revisions on demand
    ++
    ++
    ++SYNOPSIS
    ++--------
    ++[synopsis]
    ++(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> --format=<pretty> [--notes=<ref>]
    ++
    ++DESCRIPTION
    ++-----------
    ++
    ++Pretty format revisions from standard input.
    ++
    ++THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
    ++
    ++OPTIONS
    ++-------
    ++
    ++`--stdin-mode=<mode>`::
    ++	How to interpret standard input data:
    +++
    ++--
    ++`revs`:: Each line is interpreted as a commit. Any kind of revision
    ++	expression can be used (see linkgit:gitrevisions[7]). Annotated
    ++	tags are peeled (see linkgit:gitglossary[7]).
     ++
    -+Implies `--name-only`. The negation `--no-format` implies
    -+`--no-name-only` (the default for the command).
    ++The argument `rev` is also accepted.
    ++`text`:: Formats all commit object names found in freeform text. These
    ++	must the full object names, i.e. abbreviated hexidecimal object
    ++	names will not be interpreted.
    ++--
     +
    - --tags::
    - 	Do not use branch names, but only tags to name the commits
    - 
    ++`--format=<pretty>`::
    ++	Pretty format string.
    ++
    ++`--notes=<ref>`::
    ++`--no-notes`::
    ++	Custom notes ref. Notes are displayed when using the `%N`
    ++	atom. See linkgit:git-notes[1].
    ++
    ++EXAMPLES
    ++--------
    ++
    ++The command linkgit:git-last-modified[1] shows the commit that each file
    ++was last modified in.
    ++
    ++----
    ++$ git last-modified -- README.md Makefile
    ++7798034171030be0909c56377a4e0e10e6d2df93	Makefile
    ++c50fbb2dd225e7e82abba4380423ae105089f4d7	README.md
    ++----
    ++
    ++We can pipe the result to this command in order to replace the object
    ++name with the commit author.
    ++
    ++----
    ++$ git last-modified -- README.md Makefile |
    ++    git format-rev --stdin-mode=text --format=%an
    ++Junio C Hamano	Makefile
    ++Todd Zullinger	README.md
    ++----
    ++
    ++Another example is _formatting commits in commit messages_. Given this commit message:
    ++
    ++----
    ++Fix off-by-one error
    ++
    ++Fix off-by-one error introduced in
    ++e83c5163316f89bfbde7d9ab23ca2e25604af290.
    ++
    ++We thought we fixed this in 5569bf9bbedd63a00780fc5c110e0cfab3aa97b9 but
    ++that only covered 1/3 of the faulty cases.
    ++----
    ++
    ++We can format the commits and use par(1) to reflow the text, say in a
    ++`commit-msg` hook:
    ++
    ++----
    ++$ git config set hook.reference-commits.event commit-msg
    ++$ git config set hook.reference-commits.command reference-commits
    ++$ cat $(which reference-commits)
    ++#/bin/sh
    ++
    ++msg="$1"
    ++rewritten=$(mktemp)
    ++git format-rev --stdin-mode=text --format=reference <"$msg" |
    ++    par >"$rewritten"
    ++mv "$rewritten" "$msg"
    ++----
    ++
    ++Which will produce something like this:
    ++
    ++----
    ++Fix off-by-one error
    ++
    ++Fix off-by-one error introduced in e83c5163316 (Implement better memory
    ++allocator, 2005-04-07).
    ++
    ++We thought we fixed this in 5569bf9bbed (Fix memory allocator,
    ++2005-06-22) but that only covered 1/3 of the faulty cases.
    ++----
    ++
    ++DISCUSSION
    ++----------
    ++
    ++This command lets you format any number of revisions in any order
    ++through one command invocation. Consider the
    ++linkgit:git-last-modified[1] case from the "EXAMPLES" section above:
    ++
    ++1. There might be hundreds of files
    ++2. Commits can be repeated, i.e. two or more files were last modified in
    ++   the same commit
    ++
    ++Two widely-used commands which pretty formats commits are
    ++linkgit:git-log[1] and linkgit:git-show[1]. It turns out that they are
    ++not a good fit for the above use case.
    ++
    ++- The output of linkgit:git-last-modified[1] would have to be processed
    ++  in stages since you need to transform the first column separately and
    ++  then link the author to the filename. But this is surmountable.
    ++- You can feed each commit to `git show` or `git show --no-walk -1`. But
    ++  that means that you need to create a process for each line.
    ++- Let’s say that you want to use one process, not one per line. So you
    ++  want to feed all the commits to the command. Now you face the problem
    ++  that you have to feed all the commits to the commands before you get
    ++  any output (this is also the case for the `--stdin` modes). In other
    ++  words, you cannot loop through each line, get the author for the
    ++  commit, and output the author and the filename. You need to feed all
    ++  the commits, get back all the output, and match the output with the
    ++  filename.
    ++- But the next problem is that commands will deduplicate the input and
    ++  only output one commit one single time only. Thus you cannot make the
    ++  output order match the input order, since a commit could have been
    ++  repeated in the original input.
    ++
    ++In short, it is straightforward to use these two commands if you use one
    ++process per line. It is much more work if you just want to use one
    ++process, but still doable. In contrast, this problem is just another
    ++shell pipeline with this command.
    ++
    ++GIT
    ++---
    ++Part of the linkgit:git[1] suite
    +
    + ## Makefile ##
    +@@ Makefile: BUILT_INS += $(patsubst builtin/%.o,git-%$X,$(BUILTIN_OBJS))
    + BUILT_INS += git-cherry$X
    + BUILT_INS += git-cherry-pick$X
    + BUILT_INS += git-format-patch$X
    ++BUILT_INS += git-format-rev$X
    + BUILT_INS += git-fsck-objects$X
    + BUILT_INS += git-init$X
    + BUILT_INS += git-maintenance$X
    +
    + ## builtin.h ##
    +@@ builtin.h: int cmd_fmt_merge_msg(int argc, const char **argv, const char *prefix, struct re
    + int cmd_for_each_ref(int argc, const char **argv, const char *prefix, struct repository *repo);
    + int cmd_for_each_repo(int argc, const char **argv, const char *prefix, struct repository *repo);
    + int cmd_format_patch(int argc, const char **argv, const char *prefix, struct repository *repo);
    ++int cmd_format_rev(int argc, const char **argv, const char *prefix, struct repository *repo);
    + int cmd_fsck(int argc, const char **argv, const char *prefix, struct repository *repo);
    + int cmd_fsmonitor__daemon(int argc, const char **argv, const char *prefix, struct repository *repo);
    + int cmd_gc(int argc, const char **argv, const char *prefix, struct repository *repo);
     
      ## builtin/name-rev.c ##
     @@
    @@ builtin/name-rev.c
      
      /*
       * One day.  See the 'name a rev shortly after epoch' test in t6120 when
    -@@ builtin/name-rev.c: struct rev_name {
    - 	int from_tag;
    +@@ builtin/name-rev.c: struct name_ref_data {
    + 	struct string_list exclude_filters;
      };
      
     +struct pretty_format {
    @@ builtin/name-rev.c: struct rev_name {
     +	struct userformat_want want;
     +};
     +
    -+struct format_cb_data {
    -+    const char *format;
    -+    int *name_only;
    + enum command_type {
    + 	NAME_REV = 1,
    ++	FORMAT_REV = 2,
     +};
     +
    - define_commit_slab(commit_rev_name, struct rev_name);
    ++enum stdin_mode {
    ++    TEXT = 1,
    ++    REVS = 2,
    + };
    + 
    + struct command {
    + 	enum command_type type;
    + 	union {
    + 		int name_only;
    ++		struct pretty_format *pretty_format;
    + 	} u;
    + };
      
    - static timestamp_t generation_cutoff = GENERATION_NUMBER_INFINITY;
    -@@ builtin/name-rev.c: static const char *get_exact_ref_match(const struct object *o)
    +@@ builtin/name-rev.c: static void init_name_rev_command(struct command *cmd,
    + 	cmd->u.name_only = name_only;
      }
      
    - /* may return a constant string or use "buf" as scratch space */
    --static const char *get_rev_name(const struct object *o, struct strbuf *buf)
    -+static const char *get_rev_name(const struct object *o,
    -+				struct pretty_format *format_ctx,
    -+				struct strbuf *buf)
    - {
    - 	struct rev_name *n;
    - 	const struct commit *c;
    -@@ builtin/name-rev.c: static const char *get_rev_name(const struct object *o, struct strbuf *buf)
    - 	if (o->type != OBJ_COMMIT)
    - 		return get_exact_ref_match(o);
    - 	c = (const struct commit *) o;
    ++static void init_format_rev_command(struct command *cmd,
    ++				    struct pretty_format *pretty_format)
    ++{
    ++	cmd->type = FORMAT_REV;
    ++	cmd->u.pretty_format = pretty_format;
    ++}
     +
    -+	if (format_ctx) {
    -+		strbuf_reset(buf);
    + static struct tip_table {
    + 	struct tip_table_entry {
    + 		struct object_id oid;
    +@@ builtin/name-rev.c: static const char *get_rev_name(const struct object *o, struct strbuf *buf)
    + 	}
    + }
    + 
    ++static const char *get_format_rev(const struct commit *c,
    ++				  struct pretty_format *format_ctx,
    ++				  struct strbuf *buf)
    ++{
    ++	strbuf_reset(buf);
     +
    -+		if (format_ctx->want.notes) {
    -+			struct strbuf notebuf = STRBUF_INIT;
    ++	if (format_ctx->want.notes) {
    ++		struct strbuf notebuf = STRBUF_INIT;
     +
    -+			format_display_notes(&c->object.oid, &notebuf,
    -+					     get_log_output_encoding(),
    -+					     format_ctx->ctx.fmt == CMIT_FMT_USERFORMAT);
    -+			format_ctx->ctx.notes_message = strbuf_detach(&notebuf, NULL);
    -+		}
    ++		format_display_notes(&c->object.oid, &notebuf,
    ++				     get_log_output_encoding(),
    ++				     format_ctx->ctx.fmt == CMIT_FMT_USERFORMAT);
    ++		format_ctx->ctx.notes_message = strbuf_detach(&notebuf, NULL);
    ++	}
     +
    -+		pretty_print_commit(&format_ctx->ctx, c, buf);
    -+		FREE_AND_NULL(format_ctx->ctx.notes_message);
    ++	pretty_print_commit(&format_ctx->ctx, c, buf);
    ++	FREE_AND_NULL(format_ctx->ctx.notes_message);
     +
    -+		return buf->buf;
    -+	}
    ++	return buf->buf;
    ++}
     +
    - 	n = get_commit_rev_name(c);
    - 	if (!n)
    - 		return NULL;
    -@@ builtin/name-rev.c: static const char *get_rev_name(const struct object *o, struct strbuf *buf)
    - 
      static void show_name(const struct object *obj,
      		      const char *caller_name,
    -+		      struct pretty_format *format_ctx,
      		      int always, int allow_undefined, int name_only)
    - {
    - 	const char *name;
    -@@ builtin/name-rev.c: static void show_name(const struct object *obj,
    - 
    - 	if (!name_only)
    - 		printf("%s ", caller_name ? caller_name : oid_to_hex(oid));
    --	name = get_rev_name(obj, &buf);
    -+	name = get_rev_name(obj, format_ctx, &buf);
    - 	if (name)
    - 		printf("%s\n", name);
    - 	else if (allow_undefined)
    -@@ builtin/name-rev.c: static char const * const name_rev_usage[] = {
    - 	NULL
    - };
    - 
    --static void name_rev_line(char *p, struct name_ref_data *data)
    -+static void name_rev_line(char *p,
    -+			  struct name_ref_data *data,
    -+			  struct pretty_format *format_ctx)
    - {
    - 	struct strbuf buf = STRBUF_INIT;
    - 	int counter = 0;
    -@@ builtin/name-rev.c: static void name_rev_line(char *p, struct name_ref_data *data)
    - 				struct object *o =
    - 					lookup_object(the_repository, &oid);
    - 				if (o)
    --					name = get_rev_name(o, &buf);
    -+					name = get_rev_name(o, format_ctx, &buf);
    +@@ builtin/name-rev.c: static void name_rev_line(char *p, struct command *cmd)
    + 				else
    + 					printf("%.*s (%s)", p_len, p_start, name);
    + 				break;
    ++			case FORMAT_REV:
    ++				if (!oid_ret)
    ++					o = parse_object(the_repository, &oid);
    ++				if (o && o->type == OBJ_COMMIT)
    ++					name = get_format_rev((const struct commit *)o,
    ++							      cmd->u.pretty_format,
    ++							      &buf);
    ++				*(p + 1) = c;
    ++				if (name)
    ++					printf("%.*s%s", p_len - hexsz, p_start, name);
    ++				else
    ++					printf("%.*s", p_len, p_start);
    ++				break;
    + 			default:
    + 				BUG("uncovered case: %d", cmd->type);
      			}
    - 			*(p+1) = c;
    - 
    -@@ builtin/name-rev.c: static void name_rev_line(char *p, struct name_ref_data *data)
    - 	strbuf_release(&buf);
    +@@ builtin/name-rev.c: int cmd_name_rev(int argc,
    + 	object_array_clear(&revs);
    + 	return 0;
      }
    - 
    -+static int format_cb(const struct option *option,
    -+		     const char *arg,
    -+		     int unset)
    ++
    ++static enum stdin_mode parse_stdin_mode(const char *stdin_mode)
     +{
    -+	struct format_cb_data *data = option->value;
    -+	data->format = arg;
    -+	*data->name_only = !unset;
    -+	return 0;
    ++	if (!strcmp(stdin_mode, "text"))
    ++		return TEXT;
    ++	else if (!strcmp(stdin_mode, "revs") ||
    ++		 !strcmp(stdin_mode, "rev"))
    ++		return REVS;
    ++	else
    ++		die(_("'%s' needs to be either text, revs, or rev"),
    ++		    "--stdin-mode");
     +}
     +
    - int cmd_name_rev(int argc,
    - 		 const char **argv,
    - 		 const char *prefix,
    -@@ builtin/name-rev.c: int cmd_name_rev(int argc,
    - #endif
    - 	int all = 0, annotate_stdin = 0, allow_undefined = 1, always = 0, peel_tag = 0;
    - 	struct name_ref_data data = { 0, 0, STRING_LIST_INIT_NODUP, STRING_LIST_INIT_NODUP };
    -+	static struct format_cb_data format_cb_data = { 0 };
    ++static char const *const format_rev_usage[] = {
    ++	N_("(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> --format=<pretty> [--notes=<ref>]"),
    ++	NULL
    ++};
    ++
    ++int cmd_format_rev(int argc,
    ++		   const char **argv,
    ++		   const char *prefix,
    ++		   struct repository *repo UNUSED)
    ++{
    ++	const char *format = NULL;
    ++	enum stdin_mode stdin_mode;
    ++	const char *stdin_mode_arg = NULL;
     +	struct display_notes_opt format_notes_opt;
     +	struct rev_info format_rev = REV_INFO_INIT;
    -+	struct pretty_format *format_ctx = NULL;
     +	struct pretty_format format_pp = { 0 };
     +	struct string_list notes = STRING_LIST_INIT_NODUP;
    - 	struct option opts[] = {
    - 		OPT_BOOL(0, "name-only", &data.name_only, N_("print only ref-based names (no object names)")),
    - 		OPT_BOOL(0, "tags", &data.tags_only, N_("only use tags to name the commits")),
    -@@ builtin/name-rev.c: int cmd_name_rev(int argc,
    - 			   PARSE_OPT_HIDDEN),
    - #endif /* WITH_BREAKING_CHANGES */
    - 		OPT_BOOL(0, "annotate-stdin", &annotate_stdin, N_("annotate text from stdin")),
    -+		OPT_CALLBACK(0, "format", &format_cb_data, N_("format"),
    -+			     N_("pretty-print output instead"), format_cb),
    ++	struct strbuf scratch_buf = STRBUF_INIT;
    ++	struct command cmd;
    ++	struct option opts[] = {
    ++		OPT_STRING(0, "format", &format, N_("format"),
    ++			   N_("pretty format to use")),
    ++		OPT_STRING(0, "stdin-mode", &stdin_mode_arg, N_("stdin-mode"),
    ++			   N_("how revs are processed")),
     +		OPT_STRING_LIST(0, "notes", &notes, N_("notes"),
    -+				N_("display notes for --format")),
    - 		OPT_BOOL(0, "undefined", &allow_undefined, N_("allow to print `undefined` names (default)")),
    - 		OPT_BOOL(0, "always",     &always,
    - 			   N_("show abbreviated commit object as fallback")),
    -@@ builtin/name-rev.c: int cmd_name_rev(int argc,
    - 		OPT_END(),
    - 	};
    - 
    ++				N_("display notes for pretty format")),
    ++		OPT_END(),
    ++	};
    ++
    ++	argc = parse_options(argc, argv, prefix, opts, format_rev_usage, 0);
    ++
    ++	if (argc > 0) {
    ++		error(_("too many arguments"));
    ++		usage_with_options(format_rev_usage, opts);
    ++	}
    ++
    ++	if (!format)
    ++		die(_("'%s' is required"), "--format");
    ++	if (!stdin_mode_arg)
    ++		die(_("'%s' is required"), "--stdin-mode");
    ++
     +	init_display_notes(&format_notes_opt);
    -+	format_cb_data.name_only = &data.name_only;
    - 	mem_pool_init(&string_pool, 0);
    - 	init_commit_rev_name(&rev_names);
    - 	repo_config(the_repository, git_default_config, NULL);
    -@@ builtin/name-rev.c: int cmd_name_rev(int argc,
    - 	}
    - #endif
    - 
    -+	if (format_cb_data.format) {
    -+		get_commit_format(format_cb_data.format, &format_rev);
    -+		format_pp.ctx.rev = &format_rev;
    -+		format_pp.ctx.fmt = format_rev.commit_format;
    -+		format_pp.ctx.abbrev = format_rev.abbrev;
    -+		format_pp.ctx.date_mode_explicit = format_rev.date_mode_explicit;
    -+		format_pp.ctx.date_mode = format_rev.date_mode;
    -+		format_pp.ctx.color = GIT_COLOR_AUTO;
    -+
    -+		userformat_find_requirements(format_cb_data.format,
    -+					     &format_pp.want);
    -+		if (format_pp.want.notes) {
    -+			int ignore_show_notes = 0;
    -+			struct string_list_item *n;
    -+
    -+			for_each_string_list_item(n, &notes)
    -+				enable_ref_display_notes(&format_notes_opt,
    -+							 &ignore_show_notes,
    -+							 n->string);
    -+			load_display_notes(&format_notes_opt);
    ++	stdin_mode = parse_stdin_mode(stdin_mode_arg);
    ++
    ++	get_commit_format(format, &format_rev);
    ++	format_pp.ctx.rev = &format_rev;
    ++	format_pp.ctx.fmt = format_rev.commit_format;
    ++	format_pp.ctx.abbrev = format_rev.abbrev;
    ++	format_pp.ctx.date_mode_explicit = format_rev.date_mode_explicit;
    ++	format_pp.ctx.date_mode = format_rev.date_mode;
    ++	format_pp.ctx.color = GIT_COLOR_AUTO;
    ++
    ++	userformat_find_requirements(format,
    ++				     &format_pp.want);
    ++	if (format_pp.want.notes) {
    ++		int ignore_show_notes = 0;
    ++		struct string_list_item *n;
    ++
    ++		for_each_string_list_item(n, &notes)
    ++			enable_ref_display_notes(&format_notes_opt,
    ++						 &ignore_show_notes,
    ++						 n->string);
    ++		load_display_notes(&format_notes_opt);
    ++	}
    ++
    ++	init_format_rev_command(&cmd, &format_pp);
    ++
    ++	switch (stdin_mode) {
    ++	case TEXT:
    ++		while (strbuf_getline(&scratch_buf, stdin) != EOF) {
    ++			strbuf_addch(&scratch_buf, '\n');
    ++			name_rev_line(scratch_buf.buf, &cmd);
     +		}
    ++		break;
    ++	case REVS:
    ++		while (strbuf_getline(&scratch_buf, stdin) != EOF) {
    ++			struct object_id oid;
    ++			struct object *object;
    ++			struct object *peeled;
    ++			struct commit *commit;
    ++
    ++			if (repo_get_oid(the_repository, scratch_buf.buf, &oid)) {
    ++				fprintf(stderr, "Could not get sha1 for %s. Skipping.\n",
    ++					scratch_buf.buf);
    ++				continue;
    ++			}
    ++
    ++			object = parse_object(the_repository, &oid);
    ++			if (!object) {
    ++				fprintf(stderr, "Could not get object for %s. Skipping.\n",
    ++					scratch_buf.buf);
    ++				continue;
    ++			}
     +
    -+		format_ctx = &format_pp;
    ++			peeled = deref_tag(the_repository, object, scratch_buf.buf, 0);
    ++			if (peeled && peeled->type == OBJ_COMMIT)
    ++				commit = (struct commit *)peeled;
    ++			if (!commit) {
    ++				fprintf(stderr, "Could not get commit for %s. Skipping.\n",
    ++					*argv);
    ++				continue;
    ++			}
    ++
    ++			get_format_rev(commit, &format_pp, &scratch_buf);
    ++			printf("%s\n", scratch_buf.buf);
    ++			strbuf_release(&scratch_buf);
    ++		}
    ++		break;
    ++	default:
    ++		BUG("uncovered case: %d", stdin_mode);
     +	}
     +
    - 	if (all + annotate_stdin + !!argc > 1) {
    - 		error("Specify either a list, or --all, not both!");
    - 		usage_with_options(name_rev_usage, opts);
    -@@ builtin/name-rev.c: int cmd_name_rev(int argc,
    - 
    - 		while (strbuf_getline(&sb, stdin) != EOF) {
    - 			strbuf_addch(&sb, '\n');
    --			name_rev_line(sb.buf, &data);
    -+			name_rev_line(sb.buf, &data, format_ctx);
    - 		}
    - 		strbuf_release(&sb);
    - 	} else if (all) {
    -@@ builtin/name-rev.c: int cmd_name_rev(int argc,
    - 			struct object *obj = get_indexed_object(the_repository, i);
    - 			if (!obj || obj->type != OBJ_COMMIT)
    - 				continue;
    --			show_name(obj, NULL,
    -+			show_name(obj, NULL, format_ctx,
    - 				  always, allow_undefined, data.name_only);
    - 		}
    - 	} else {
    - 		int i;
    - 		for (i = 0; i < revs.nr; i++)
    --			show_name(revs.objects[i].item, revs.objects[i].name,
    -+			show_name(revs.objects[i].item, revs.objects[i].name, format_ctx,
    - 				  always, allow_undefined, data.name_only);
    - 	}
    - 
    - 	string_list_clear(&data.ref_filters, 0);
    - 	string_list_clear(&data.exclude_filters, 0);
    ++	strbuf_release(&scratch_buf);
     +	string_list_clear(&notes, 0);
     +	release_display_notes(&format_notes_opt);
    - 	mem_pool_discard(&string_pool, 0);
    - 	object_array_clear(&revs);
    - 	return 0;
    ++	return 0;
    ++}
    +
    + ## command-list.txt ##
    +@@ command-list.txt: git-fmt-merge-msg                       purehelpers
    + git-for-each-ref                        plumbinginterrogators
    + git-for-each-repo                       plumbinginterrogators
    + git-format-patch                        mainporcelain
    ++git-format-rev                          plumbinginterrogators
    + git-fsck                                ancillaryinterrogators          complete
    + git-gc                                  mainporcelain
    + git-get-tar-commit-id                   plumbinginterrogators
    +
    + ## git.c ##
    +@@ git.c: static struct cmd_struct commands[] = {
    + 	{ "for-each-ref", cmd_for_each_ref, RUN_SETUP },
    + 	{ "for-each-repo", cmd_for_each_repo, RUN_SETUP_GENTLY },
    + 	{ "format-patch", cmd_format_patch, RUN_SETUP },
    ++	{ "format-rev", cmd_format_rev, RUN_SETUP },
    + 	{ "fsck", cmd_fsck, RUN_SETUP },
    + 	{ "fsck-objects", cmd_fsck, RUN_SETUP },
    + 	{ "fsmonitor--daemon", cmd_fsmonitor__daemon, RUN_SETUP },
    +
    + ## t/t1517-outside-repo.sh ##
    +@@ t/t1517-outside-repo.sh: do
    + 	archimport | citool | credential-netrc | credential-libsecret | \
    + 	credential-osxkeychain | cvsexportcommit | cvsimport | cvsserver | \
    + 	daemon | \
    +-	difftool--helper | filter-branch | fsck-objects | get-tar-commit-id | \
    ++	difftool--helper | filter-branch | format-rev | fsck-objects | \
    ++	get-tar-commit-id | \
    + 	gui | gui--askpass | \
    + 	http-backend | http-fetch | http-push | init-db | \
    + 	merge-octopus | merge-one-file | merge-resolve | mergetool | \
     
      ## t/t6120-describe.sh ##
    -@@ t/t6120-describe.sh: test_expect_success 'name-rev --annotate-stdin works with commitGraph' '
    - 	)
    +@@ t/t6120-describe.sh: test_expect_success 'do not be fooled by invalid describe format ' '
    + 	test_must_fail git cat-file -t "refs/tags/super-invalid/./../...../ ~^:/?*[////\\\\\\&}/busted.lock-42-g"$(cat out)
      '
      
     +test_expect_success 'name-rev --format setup' '
    @@ t/t6120-describe.sh: test_expect_success 'name-rev --annotate-stdin works with c
     +	test_commit -C repo-format eighth
     +'
     +
    -+test_expect_success 'name-rev --format --no-name-only' '
    -+	cat >expect <<-\EOF &&
    -+	HEAD~3 [fifth]
    -+	HEAD [eighth]
    -+	HEAD~5 [third]
    -+	EOF
    -+	git -C repo-format name-rev --format="[%s]" \
    -+		--no-name-only HEAD~3 HEAD HEAD~5 >actual &&
    -+	test_cmp expect actual
    -+'
    -+
    -+test_expect_success 'name-rev --format --no-format is the same as regular name-rev' '
    -+	git -C repo-format name-rev HEAD~2 HEAD~3 >expect &&
    -+	test_file_not_empty expect &&
    -+	git -C repo-format name-rev --format="huh?" \
    -+		--no-format HEAD~2 HEAD~3 >actual &&
    -+	test_cmp expect actual
    -+'
    -+
    -+test_expect_success 'name-rev --format=%s for argument revs' '
    ++test_expect_success 'format-rev --stdin-mode=revs' '
     +	cat >expect <<-\EOF &&
     +	eighth
     +	seventh
     +	fifth
     +	EOF
    -+	git -C repo-format name-rev --format=%s \
    -+		HEAD HEAD~ HEAD~3 >actual &&
    ++	git -C repo-format format-rev --stdin-mode=revs \
    ++		--format=%s >actual <<-\EOF &&
    ++	HEAD
    ++	HEAD~
    ++	HEAD~3
    ++	EOF
     +	test_cmp expect actual
     +'
     +
    -+test_expect_success '--name-rev --format=reference --annotate-stdin from rev-list same as log' '
    ++test_expect_success 'format-rev --stdin-mode=text from rev-list same as log' '
     +	git -C repo-format log --format=reference >expect &&
     +	test_file_not_empty expect &&
     +	git -C repo-format rev-list HEAD >list &&
    -+	git -C repo-format name-rev --format=reference \
    -+		--annotate-stdin <list >actual &&
    ++	git -C repo-format format-rev --stdin-mode=text \
    ++		--format=reference <list >actual &&
     +	test_cmp expect actual
     +'
     +
    -+test_expect_success '--name-rev --format=<pretty> --annotate-stdin with running text and tree oid' '
    ++test_expect_success 'format-rev --stdin-mode=text with running text and tree oid' '
     +	cmit_oid=$(git -C repo-format rev-parse :/fifth) &&
     +	reference=$(git -C repo-format log -n1 --format=reference :/fifth) &&
     +	tree=$(git -C repo-format rev-parse HEAD^{tree}) &&
    @@ t/t6120-describe.sh: test_expect_success 'name-rev --annotate-stdin works with c
     +	We thought we fixed this in ${reference}.
     +	But look at this tree: ${tree}.
     +	EOF
    -+	git -C repo-format name-rev --format=reference --annotate-stdin \
    ++	git -C repo-format format-rev --stdin-mode=text --format=reference \
     +		>actual <<-EOF &&
     +	We thought we fixed this in ${cmit_oid}.
     +	But look at this tree: ${tree}.
    @@ t/t6120-describe.sh: test_expect_success 'name-rev --annotate-stdin works with c
     +	test_cmp expect actual
     +'
     +
    -+test_expect_success 'name-rev --format=<pretty> with %N (note)' '
    ++test_expect_success 'format-rev with %N (note)' '
     +	test_when_finished "git -C repo-format notes remove" &&
     +	git -C repo-format notes add -m"Make a note" &&
     +	printf "Make a note\n\n\n" >expect &&
    -+	git -C repo-format name-rev --format="tformat:%N" \
    -+		HEAD HEAD~ >actual &&
    ++	git -C repo-format format-rev --stdin-mode=revs \
    ++		--format="tformat:%N" \
    ++		>actual <<-\EOF &&
    ++	HEAD
    ++	HEAD~
    ++	EOF
     +	test_cmp expect actual
     +'
     +
    -+test_expect_success 'name-rev --format=<pretty> --notes<ref>' '
    ++test_expect_success 'format-rev --notes<ref> (custom notes ref)' '
     +	# One custom notes ref
     +	test_when_finished "git -C repo-format notes remove" &&
     +	test_when_finished "git -C repo-format notes --ref=word remove" &&
     +	git -C repo-format notes add -m"default" &&
     +	git -C repo-format notes --ref=word add -m"custom" &&
     +	printf "custom\n\n" >expect &&
    -+	git -C repo-format name-rev --format="tformat:%N" \
    ++	git -C repo-format format-rev --stdin-mode=revs \
    ++		--format="tformat:%N" \
     +		--notes=word \
    -+		HEAD >actual &&
    ++		>actual <<-\EOF &&
    ++	HEAD
    ++	EOF
     +	test_cmp expect actual &&
     +	# Glob all
     +	printf "default\ncustom\n\n" >expect &&
    -+	git -C repo-format name-rev --format="tformat:%N" \
    -+		--notes=* \
    -+		HEAD >actual &&
    ++	git -C repo-format format-rev --stdin-mode=revs \
    ++		--format="tformat:%N" \
    ++		--notes=* >actual <<-\EOF &&
    ++	HEAD
    ++	EOF
    ++	test_cmp expect actual
    ++'
    ++
    ++test_expect_success 'format-rev --stdin-mode=revs on annotated tag peels to commit' '
    ++	test_when_finished "git -C repo-format tag -d version" &&
    ++	git -C repo-format tag -a -m"new version" version &&
    ++	cat >expect <<-\EOF &&
    ++	eighth
    ++	EOF
    ++	git -C repo-format format-rev --stdin-mode=revs \
    ++		--format=%s \
    ++		--notes=* >actual <<-\EOF &&
    ++	version
    ++	EOF
     +	test_cmp expect actual
     +'
     +
    - #               B
    - #               o
    - #  H             \
    + test_done

base-commit: 67006b9db8b772423ad0706029286096307d2567
-- 
2.54.0.13.g9c7419e39f8
kristofferhaugsbakk@fastmail.comApr 28, 2026, 22:25 UTC in reply to kristofferhaugsbakk@fastmail.com on lore

[PATCH v3 1/5] name-rev: wrap both blocks in braces

From: Kristoffer Haugsbakk <code@khaugsbakk.name>
See `CodingGuidelines`:
    - When there are multiple arms to a conditional and some of them
      require braces, enclose even a single line block in braces for
      consistency. [...]
Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
---
Notes (series):
    v2:
    Fix stray formatting of `(p+1)`
 builtin/name-rev.c | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)
Show changes to builtin/name-rev.c +4 −4
diff --git a/builtin/name-rev.c b/builtin/name-rev.c
index 6188cf98ce0..171e7bd0e98 100644
--- a/builtin/name-rev.c
+++ b/builtin/name-rev.c
@@ -466,9 +466,9 @@ static const char *get_rev_name(const struct object *o, struct strbuf *buf)
 	if (!n)
 		return NULL;
 
-	if (!n->generation)
+	if (!n->generation) {
 		return n->tip_name;
-	else {
+	} else {
 		strbuf_reset(buf);
 		strbuf_addstr(buf, n->tip_name);
 		strbuf_strip_suffix(buf, "^0");
@@ -516,9 +516,9 @@ static void name_rev_line(char *p, struct name_ref_data *data)
 
 	for (p_start = p; *p; p++) {
 #define ishex(x) (isdigit((x)) || ((x) >= 'a' && (x) <= 'f'))
-		if (!ishex(*p))
+		if (!ishex(*p)) {
 			counter = 0;
-		else if (++counter == hexsz &&
+		} else if (++counter == hexsz &&
 			 !ishex(*(p+1))) {
 			struct object_id oid;
 			const char *name = NULL;
-- 
2.54.0.13.g9c7419e39f8
kristofferhaugsbakk@fastmail.comApr 28, 2026, 22:25 UTC in reply to kristofferhaugsbakk@fastmail.com on lore

[PATCH v3 2/5] name-rev: run clang-format before factoring code

From: Kristoffer Haugsbakk <code@khaugsbakk.name>

We are about to move code around to prepare for adding a new command. Let’s deal with clang-format changes first in the affected areas.

Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
---
 builtin/name-rev.c | 18 +++++++++---------
 1 file changed, 9 insertions(+), 9 deletions(-)
Show changes to builtin/name-rev.c +9 −9
diff --git a/builtin/name-rev.c b/builtin/name-rev.c
index 171e7bd0e98..6357eaa76d0 100644
--- a/builtin/name-rev.c
+++ b/builtin/name-rev.c
@@ -519,22 +519,22 @@ static void name_rev_line(char *p, struct name_ref_data *data)
 		if (!ishex(*p)) {
 			counter = 0;
 		} else if (++counter == hexsz &&
-			 !ishex(*(p+1))) {
+			   !ishex(*(p + 1))) {
 			struct object_id oid;
 			const char *name = NULL;
-			char c = *(p+1);
+			char c = *(p + 1);
 			int p_len = p - p_start + 1;
 
 			counter = 0;
 
-			*(p+1) = 0;
+			*(p + 1) = 0;
 			if (!repo_get_oid(the_repository, p - (hexsz - 1), &oid)) {
 				struct object *o =
 					lookup_object(the_repository, &oid);
 				if (o)
 					name = get_rev_name(o, &buf);
 			}
-			*(p+1) = c;
+			*(p + 1) = c;
 
 			if (!name)
 				continue;
@@ -571,9 +571,9 @@ int cmd_name_rev(int argc,
 		OPT_BOOL(0, "name-only", &data.name_only, N_("print only ref-based names (no object names)")),
 		OPT_BOOL(0, "tags", &data.tags_only, N_("only use tags to name the commits")),
 		OPT_STRING_LIST(0, "refs", &data.ref_filters, N_("pattern"),
-				   N_("only use refs matching <pattern>")),
+				N_("only use refs matching <pattern>")),
 		OPT_STRING_LIST(0, "exclude", &data.exclude_filters, N_("pattern"),
-				   N_("ignore refs matching <pattern>")),
+				N_("ignore refs matching <pattern>")),
 		OPT_GROUP(""),
 		OPT_BOOL(0, "all", &all, N_("list all commits reachable from all refs")),
 #ifndef WITH_BREAKING_CHANGES
@@ -585,10 +585,10 @@ int cmd_name_rev(int argc,
 #endif /* WITH_BREAKING_CHANGES */
 		OPT_BOOL(0, "annotate-stdin", &annotate_stdin, N_("annotate text from stdin")),
 		OPT_BOOL(0, "undefined", &allow_undefined, N_("allow to print `undefined` names (default)")),
-		OPT_BOOL(0, "always",     &always,
-			   N_("show abbreviated commit object as fallback")),
+		OPT_BOOL(0, "always", &always,
+			 N_("show abbreviated commit object as fallback")),
 		OPT_HIDDEN_BOOL(0, "peel-tag", &peel_tag,
-			   N_("dereference tags in the input (internal use)")),
+				N_("dereference tags in the input (internal use)")),
 		OPT_END(),
 	};
 
-- 
2.54.0.13.g9c7419e39f8
kristofferhaugsbakk@fastmail.comApr 28, 2026, 22:25 UTC in reply to kristofferhaugsbakk@fastmail.com on lore

[PATCH v3 3/5] name-rev: factor code for sharing with a new command

From: Kristoffer Haugsbakk <code@khaugsbakk.name>

We are about to introduce a new command git-format-rev(1) to this file. Let’s factor some code so that we can share it with the new command.

We want to be able to format commits found in freeform text, and git-name-rev(1) already has a function for that but for symbolic names. Let’s use a tagged union for the command-specific payload.

No functional changes.
Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
---
 builtin/name-rev.c | 54 +++++++++++++++++++++++++++++++++++-----------
 1 file changed, 41 insertions(+), 13 deletions(-)
Show changes to builtin/name-rev.c +41 −13
diff --git a/builtin/name-rev.c b/builtin/name-rev.c
index 6357eaa76d0..dc4136f4de3 100644
--- a/builtin/name-rev.c
+++ b/builtin/name-rev.c
@@ -272,6 +272,24 @@ struct name_ref_data {
 	struct string_list exclude_filters;
 };
 
+enum command_type {
+	NAME_REV = 1,
+};
+
+struct command {
+	enum command_type type;
+	union {
+		int name_only;
+	} u;
+};
+
+static void init_name_rev_command(struct command *cmd,
+				  int name_only)
+{
+	cmd->type = NAME_REV;
+	cmd->u.name_only = name_only;
+}
+
 static struct tip_table {
 	struct tip_table_entry {
 		struct object_id oid;
@@ -507,7 +525,7 @@ static char const * const name_rev_usage[] = {
 	NULL
 };
 
-static void name_rev_line(char *p, struct name_ref_data *data)
+static void name_rev_line(char *p, struct command *cmd)
 {
 	struct strbuf buf = STRBUF_INIT;
 	int counter = 0;
@@ -516,6 +534,7 @@ static void name_rev_line(char *p, struct name_ref_data *data)
 
 	for (p_start = p; *p; p++) {
 #define ishex(x) (isdigit((x)) || ((x) >= 'a' && (x) <= 'f'))
+	start:
 		if (!ishex(*p)) {
 			counter = 0;
 		} else if (++counter == hexsz &&
@@ -524,25 +543,32 @@ static void name_rev_line(char *p, struct name_ref_data *data)
 			const char *name = NULL;
 			char c = *(p + 1);
 			int p_len = p - p_start + 1;
+			struct object *o = NULL;
+			int oid_ret = 1;
 
 			counter = 0;
 
 			*(p + 1) = 0;
-			if (!repo_get_oid(the_repository, p - (hexsz - 1), &oid)) {
-				struct object *o =
-					lookup_object(the_repository, &oid);
+			oid_ret = repo_get_oid(the_repository, p - (hexsz - 1), &oid);
+
+			switch (cmd->type) {
+			case NAME_REV:
+				if (!oid_ret)
+					o = lookup_object(the_repository, &oid);
 				if (o)
 					name = get_rev_name(o, &buf);
+				*(p + 1) = c;
+				if (!name)
+					goto start;
+				if (cmd->u.name_only)
+					printf("%.*s%s", p_len - hexsz, p_start, name);
+				else
+					printf("%.*s (%s)", p_len, p_start, name);
+				break;
+			default:
+				BUG("uncovered case: %d", cmd->type);
 			}
-			*(p + 1) = c;
-
-			if (!name)
-				continue;
 
-			if (data->name_only)
-				printf("%.*s%s", p_len - hexsz, p_start, name);
-			else
-				printf("%.*s (%s)", p_len, p_start, name);
 			p_start = p + 1;
 		}
 	}
@@ -567,6 +593,7 @@ int cmd_name_rev(int argc,
 #endif
 	int all = 0, annotate_stdin = 0, allow_undefined = 1, always = 0, peel_tag = 0;
 	struct name_ref_data data = { 0, 0, STRING_LIST_INIT_NODUP, STRING_LIST_INIT_NODUP };
+	struct command cmd;
 	struct option opts[] = {
 		OPT_BOOL(0, "name-only", &data.name_only, N_("print only ref-based names (no object names)")),
 		OPT_BOOL(0, "tags", &data.tags_only, N_("only use tags to name the commits")),
@@ -596,6 +623,7 @@ int cmd_name_rev(int argc,
 	init_commit_rev_name(&rev_names);
 	repo_config(the_repository, git_default_config, NULL);
 	argc = parse_options(argc, argv, prefix, opts, name_rev_usage, 0);
+	init_name_rev_command(&cmd, data.name_only);
 
 #ifndef WITH_BREAKING_CHANGES
 	if (transform_stdin) {
@@ -663,7 +691,7 @@ int cmd_name_rev(int argc,
 
 		while (strbuf_getline(&sb, stdin) != EOF) {
 			strbuf_addch(&sb, '\n');
-			name_rev_line(sb.buf, &data);
+			name_rev_line(sb.buf, &cmd);
 		}
 		strbuf_release(&sb);
 	} else if (all) {
-- 
2.54.0.13.g9c7419e39f8
kristofferhaugsbakk@fastmail.comApr 28, 2026, 22:25 UTC in reply to kristofferhaugsbakk@fastmail.com on lore

[PATCH v3 4/5] name-rev: make dedicated --annotate-stdin --name-only test

From: Kristoffer Haugsbakk <code@khaugsbakk.name>
The previous commit split the `--name-only` handling:
1. `--annotate-stdin`: uses the new `struct command`
2. The rest: uses `struct name_ref_data`

But there is no dedicated test for the option combination in (1). That means that the following tests will fail if you neglect to set `command.u.name_only` properly:

    name-rev --annotate-stdin works with commitGraph
    name-rev --annotate-stdin works with non-monotonic timestamps

even though it has nothing to do with what these tests are supposed to test.

Let’s add another regression test now that it is relevant.
Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
---
 t/t6120-describe.sh | 14 ++++++++++++++
 1 file changed, 14 insertions(+)
Show changes to t/t6120-describe.sh +14 −0
diff --git a/t/t6120-describe.sh b/t/t6120-describe.sh
index 2c70cc561ad..62789f76381 100755
--- a/t/t6120-describe.sh
+++ b/t/t6120-describe.sh
@@ -298,6 +298,20 @@ test_expect_success 'name-rev --annotate-stdin' '
 	test_cmp expect actual
 '
 
+test_expect_success 'name-rev --annotate-stdin --name-only' '
+	>expect.unsorted &&
+	for rev in $(git rev-list --all)
+	do
+		name=$(git name-rev --name-only $rev) &&
+		echo "$name" >>expect.unsorted || return 1
+	done &&
+	sort <expect.unsorted >expect &&
+	git name-rev --annotate-stdin --name-only \
+		<list >actual.unsorted &&
+	sort <actual.unsorted >actual &&
+	test_cmp expect actual
+'
+
 test_expect_success 'name-rev --stdin deprecated' '
 	git rev-list --all >list &&
 	if ! test_have_prereq WITH_BREAKING_CHANGES
-- 
2.54.0.13.g9c7419e39f8
kristofferhaugsbakk@fastmail.comApr 28, 2026, 22:25 UTC in reply to kristofferhaugsbakk@fastmail.com on lore

[PATCH v3 5/5] format-rev: introduce builtin for on-demand pretty formatting

From: Kristoffer Haugsbakk <code@khaugsbakk.name>

Introduce a new builtin for pretty formatting one revision expression per line or commit object names found in running text.

Sometimes you want to format commits. Most of the time you’re walking the graph, e.g. getting a range of commits like `master..topic`. That’s a job for git-log(1).

But there are times when you want to format commits that you encounter on demand:

• Full hashes in running text that you might want to pretty-print
• git-last-modified(1) outputs full hashes that you can do the same
  with
• git-cherry(1) has `-v` for commit subject, but maybe you want
  something else?
But now you can’t use git-log(1), git-show(1), or git-rev-list(1):
• You can’t feed commits piecemeal to these commands, one input
  for one output; they block until standard in is closed
• You can’t feed a list of possibly duplicate commits, like the output
  of git-last-modified(1); they effectively deduplicate the output

Beyond these two points there’s also the input massage problem: you cannot feed mixed input (revisions mixed with arbitrary text).

One might hope that git-cat-file(1) can save us. But it doesn’t support pretty formats.

But there is one command that already both handles revisions as arguments, revisions on standard input, and even revisions mixed in with arbitrary text. Namely git-name-rev(1): the command for outputting symbolic names for commits.

We made some room in `builtin/name-rev.c` two commits ago. Let’s now add this new git-format-rev(1) command. Taking inspiration from git-name-rev(1), there are two modes:

• revs: like git-name-rev(1) in argv mode, but one revision per line
  on standard in
• text: like git-name-rev(1) with `--annotate-stdin`
***

We need to add this command to the exception list in `t/t1517-outside-repo.sh` because it uses “EXPERIMENTAL!” in the usage line.

Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
---
Notes (series):
    v3:
    • And don’t forget to document --notes this time
    
          https://lore.kernel.org/git/CALnO6CB5WOTp_e7Kv3CrEbQ+3XE-gDxNVHf7qATBEbyKWfxpLg@mail.gmail.com/
 Documentation/git-format-rev.adoc | 148 ++++++++++++++++++++++++
 Makefile                          |   1 +
 builtin.h                         |   1 +
 builtin/name-rev.c                | 186 ++++++++++++++++++++++++++++++
 command-list.txt                  |   1 +
 git.c                             |   1 +
 t/t1517-outside-repo.sh           |   3 +-
 t/t6120-describe.sh               | 104 +++++++++++++++++
 8 files changed, 444 insertions(+), 1 deletion(-)
 create mode 100644 Documentation/git-format-rev.adoc
Show changes to 8 files +444 −1

Documentation/git-format-rev.adoc, Makefile, builtin.h, builtin/name-rev.c, command-list.txt, git.c, t/t1517-outside-repo.sh, t/t6120-describe.sh

diff --git a/Documentation/git-format-rev.adoc b/Documentation/git-format-rev.adoc
new file mode 100644
index 00000000000..d960001d750
--- /dev/null
+++ b/Documentation/git-format-rev.adoc
@@ -0,0 +1,148 @@
+git-format-rev(1)
+=================
+
+NAME
+----
+git-format-rev - EXPERIMENTAL: Pretty format revisions on demand
+
+
+SYNOPSIS
+--------
+[synopsis]
+(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> --format=<pretty> [--notes=<ref>]
+
+DESCRIPTION
+-----------
+
+Pretty format revisions from standard input.
+
+THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
+
+OPTIONS
+-------
+
+`--stdin-mode=<mode>`::
+	How to interpret standard input data:
++
+--
+`revs`:: Each line is interpreted as a commit. Any kind of revision
+	expression can be used (see linkgit:gitrevisions[7]). Annotated
+	tags are peeled (see linkgit:gitglossary[7]).
++
+The argument `rev` is also accepted.
+`text`:: Formats all commit object names found in freeform text. These
+	must the full object names, i.e. abbreviated hexidecimal object
+	names will not be interpreted.
+--
+
+`--format=<pretty>`::
+	Pretty format string.
+
+`--notes=<ref>`::
+`--no-notes`::
+	Custom notes ref. Notes are displayed when using the `%N`
+	atom. See linkgit:git-notes[1].
+
+EXAMPLES
+--------
+
+The command linkgit:git-last-modified[1] shows the commit that each file
+was last modified in.
+
+----
+$ git last-modified -- README.md Makefile
+7798034171030be0909c56377a4e0e10e6d2df93	Makefile
+c50fbb2dd225e7e82abba4380423ae105089f4d7	README.md
+----
+
+We can pipe the result to this command in order to replace the object
+name with the commit author.
+
+----
+$ git last-modified -- README.md Makefile |
+    git format-rev --stdin-mode=text --format=%an
+Junio C Hamano	Makefile
+Todd Zullinger	README.md
+----
+
+Another example is _formatting commits in commit messages_. Given this commit message:
+
+----
+Fix off-by-one error
+
+Fix off-by-one error introduced in
+e83c5163316f89bfbde7d9ab23ca2e25604af290.
+
+We thought we fixed this in 5569bf9bbedd63a00780fc5c110e0cfab3aa97b9 but
+that only covered 1/3 of the faulty cases.
+----
+
+We can format the commits and use par(1) to reflow the text, say in a
+`commit-msg` hook:
+
+----
+$ git config set hook.reference-commits.event commit-msg
+$ git config set hook.reference-commits.command reference-commits
+$ cat $(which reference-commits)
+#/bin/sh
+
+msg="$1"
+rewritten=$(mktemp)
+git format-rev --stdin-mode=text --format=reference <"$msg" |
+    par >"$rewritten"
+mv "$rewritten" "$msg"
+----
+
+Which will produce something like this:
+
+----
+Fix off-by-one error
+
+Fix off-by-one error introduced in e83c5163316 (Implement better memory
+allocator, 2005-04-07).
+
+We thought we fixed this in 5569bf9bbed (Fix memory allocator,
+2005-06-22) but that only covered 1/3 of the faulty cases.
+----
+
+DISCUSSION
+----------
+
+This command lets you format any number of revisions in any order
+through one command invocation. Consider the
+linkgit:git-last-modified[1] case from the "EXAMPLES" section above:
+
+1. There might be hundreds of files
+2. Commits can be repeated, i.e. two or more files were last modified in
+   the same commit
+
+Two widely-used commands which pretty formats commits are
+linkgit:git-log[1] and linkgit:git-show[1]. It turns out that they are
+not a good fit for the above use case.
+
+- The output of linkgit:git-last-modified[1] would have to be processed
+  in stages since you need to transform the first column separately and
+  then link the author to the filename. But this is surmountable.
+- You can feed each commit to `git show` or `git show --no-walk -1`. But
+  that means that you need to create a process for each line.
+- Let’s say that you want to use one process, not one per line. So you
+  want to feed all the commits to the command. Now you face the problem
+  that you have to feed all the commits to the commands before you get
+  any output (this is also the case for the `--stdin` modes). In other
+  words, you cannot loop through each line, get the author for the
+  commit, and output the author and the filename. You need to feed all
+  the commits, get back all the output, and match the output with the
+  filename.
+- But the next problem is that commands will deduplicate the input and
+  only output one commit one single time only. Thus you cannot make the
+  output order match the input order, since a commit could have been
+  repeated in the original input.
+
+In short, it is straightforward to use these two commands if you use one
+process per line. It is much more work if you just want to use one
+process, but still doable. In contrast, this problem is just another
+shell pipeline with this command.
+
+GIT
+---
+Part of the linkgit:git[1] suite
diff --git a/Makefile b/Makefile
index 15b1ded1a0b..cbaf91fd846 100644
--- a/Makefile
+++ b/Makefile
@@ -895,6 +895,7 @@ BUILT_INS += $(patsubst builtin/%.o,git-%$X,$(BUILTIN_OBJS))
 BUILT_INS += git-cherry$X
 BUILT_INS += git-cherry-pick$X
 BUILT_INS += git-format-patch$X
+BUILT_INS += git-format-rev$X
 BUILT_INS += git-fsck-objects$X
 BUILT_INS += git-init$X
 BUILT_INS += git-maintenance$X
diff --git a/builtin.h b/builtin.h
index 235c51f30e5..63813c90125 100644
--- a/builtin.h
+++ b/builtin.h
@@ -189,6 +189,7 @@ int cmd_fmt_merge_msg(int argc, const char **argv, const char *prefix, struct re
 int cmd_for_each_ref(int argc, const char **argv, const char *prefix, struct repository *repo);
 int cmd_for_each_repo(int argc, const char **argv, const char *prefix, struct repository *repo);
 int cmd_format_patch(int argc, const char **argv, const char *prefix, struct repository *repo);
+int cmd_format_rev(int argc, const char **argv, const char *prefix, struct repository *repo);
 int cmd_fsck(int argc, const char **argv, const char *prefix, struct repository *repo);
 int cmd_fsmonitor__daemon(int argc, const char **argv, const char *prefix, struct repository *repo);
 int cmd_gc(int argc, const char **argv, const char *prefix, struct repository *repo);
diff --git a/builtin/name-rev.c b/builtin/name-rev.c
index dc4136f4de3..b60cc766279 100644
--- a/builtin/name-rev.c
+++ b/builtin/name-rev.c
@@ -18,6 +18,9 @@
 #include "commit-graph.h"
 #include "wildmatch.h"
 #include "mem-pool.h"
+#include "pretty.h"
+#include "revision.h"
+#include "notes.h"
 
 /*
  * One day.  See the 'name a rev shortly after epoch' test in t6120 when
@@ -272,14 +275,26 @@ struct name_ref_data {
 	struct string_list exclude_filters;
 };
 
+struct pretty_format {
+	struct pretty_print_context ctx;
+	struct userformat_want want;
+};
+
 enum command_type {
 	NAME_REV = 1,
+	FORMAT_REV = 2,
+};
+
+enum stdin_mode {
+    TEXT = 1,
+    REVS = 2,
 };
 
 struct command {
 	enum command_type type;
 	union {
 		int name_only;
+		struct pretty_format *pretty_format;
 	} u;
 };
 
@@ -290,6 +305,13 @@ static void init_name_rev_command(struct command *cmd,
 	cmd->u.name_only = name_only;
 }
 
+static void init_format_rev_command(struct command *cmd,
+				    struct pretty_format *pretty_format)
+{
+	cmd->type = FORMAT_REV;
+	cmd->u.pretty_format = pretty_format;
+}
+
 static struct tip_table {
 	struct tip_table_entry {
 		struct object_id oid;
@@ -495,6 +517,27 @@ static const char *get_rev_name(const struct object *o, struct strbuf *buf)
 	}
 }
 
+static const char *get_format_rev(const struct commit *c,
+				  struct pretty_format *format_ctx,
+				  struct strbuf *buf)
+{
+	strbuf_reset(buf);
+
+	if (format_ctx->want.notes) {
+		struct strbuf notebuf = STRBUF_INIT;
+
+		format_display_notes(&c->object.oid, &notebuf,
+				     get_log_output_encoding(),
+				     format_ctx->ctx.fmt == CMIT_FMT_USERFORMAT);
+		format_ctx->ctx.notes_message = strbuf_detach(&notebuf, NULL);
+	}
+
+	pretty_print_commit(&format_ctx->ctx, c, buf);
+	FREE_AND_NULL(format_ctx->ctx.notes_message);
+
+	return buf->buf;
+}
+
 static void show_name(const struct object *obj,
 		      const char *caller_name,
 		      int always, int allow_undefined, int name_only)
@@ -565,6 +608,19 @@ static void name_rev_line(char *p, struct command *cmd)
 				else
 					printf("%.*s (%s)", p_len, p_start, name);
 				break;
+			case FORMAT_REV:
+				if (!oid_ret)
+					o = parse_object(the_repository, &oid);
+				if (o && o->type == OBJ_COMMIT)
+					name = get_format_rev((const struct commit *)o,
+							      cmd->u.pretty_format,
+							      &buf);
+				*(p + 1) = c;
+				if (name)
+					printf("%.*s%s", p_len - hexsz, p_start, name);
+				else
+					printf("%.*s", p_len, p_start);
+				break;
 			default:
 				BUG("uncovered case: %d", cmd->type);
 			}
@@ -718,3 +774,133 @@ int cmd_name_rev(int argc,
 	object_array_clear(&revs);
 	return 0;
 }
+
+static enum stdin_mode parse_stdin_mode(const char *stdin_mode)
+{
+	if (!strcmp(stdin_mode, "text"))
+		return TEXT;
+	else if (!strcmp(stdin_mode, "revs") ||
+		 !strcmp(stdin_mode, "rev"))
+		return REVS;
+	else
+		die(_("'%s' needs to be either text, revs, or rev"),
+		    "--stdin-mode");
+}
+
+static char const *const format_rev_usage[] = {
+	N_("(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> --format=<pretty> [--notes=<ref>]"),
+	NULL
+};
+
+int cmd_format_rev(int argc,
+		   const char **argv,
+		   const char *prefix,
+		   struct repository *repo UNUSED)
+{
+	const char *format = NULL;
+	enum stdin_mode stdin_mode;
+	const char *stdin_mode_arg = NULL;
+	struct display_notes_opt format_notes_opt;
+	struct rev_info format_rev = REV_INFO_INIT;
+	struct pretty_format format_pp = { 0 };
+	struct string_list notes = STRING_LIST_INIT_NODUP;
+	struct strbuf scratch_buf = STRBUF_INIT;
+	struct command cmd;
+	struct option opts[] = {
+		OPT_STRING(0, "format", &format, N_("format"),
+			   N_("pretty format to use")),
+		OPT_STRING(0, "stdin-mode", &stdin_mode_arg, N_("stdin-mode"),
+			   N_("how revs are processed")),
+		OPT_STRING_LIST(0, "notes", &notes, N_("notes"),
+				N_("display notes for pretty format")),
+		OPT_END(),
+	};
+
+	argc = parse_options(argc, argv, prefix, opts, format_rev_usage, 0);
+
+	if (argc > 0) {
+		error(_("too many arguments"));
+		usage_with_options(format_rev_usage, opts);
+	}
+
+	if (!format)
+		die(_("'%s' is required"), "--format");
+	if (!stdin_mode_arg)
+		die(_("'%s' is required"), "--stdin-mode");
+
+	init_display_notes(&format_notes_opt);
+	stdin_mode = parse_stdin_mode(stdin_mode_arg);
+
+	get_commit_format(format, &format_rev);
+	format_pp.ctx.rev = &format_rev;
+	format_pp.ctx.fmt = format_rev.commit_format;
+	format_pp.ctx.abbrev = format_rev.abbrev;
+	format_pp.ctx.date_mode_explicit = format_rev.date_mode_explicit;
+	format_pp.ctx.date_mode = format_rev.date_mode;
+	format_pp.ctx.color = GIT_COLOR_AUTO;
+
+	userformat_find_requirements(format,
+				     &format_pp.want);
+	if (format_pp.want.notes) {
+		int ignore_show_notes = 0;
+		struct string_list_item *n;
+
+		for_each_string_list_item(n, &notes)
+			enable_ref_display_notes(&format_notes_opt,
+						 &ignore_show_notes,
+						 n->string);
+		load_display_notes(&format_notes_opt);
+	}
+
+	init_format_rev_command(&cmd, &format_pp);
+
+	switch (stdin_mode) {
+	case TEXT:
+		while (strbuf_getline(&scratch_buf, stdin) != EOF) {
+			strbuf_addch(&scratch_buf, '\n');
+			name_rev_line(scratch_buf.buf, &cmd);
+		}
+		break;
+	case REVS:
+		while (strbuf_getline(&scratch_buf, stdin) != EOF) {
+			struct object_id oid;
+			struct object *object;
+			struct object *peeled;
+			struct commit *commit;
+
+			if (repo_get_oid(the_repository, scratch_buf.buf, &oid)) {
+				fprintf(stderr, "Could not get sha1 for %s. Skipping.\n",
+					scratch_buf.buf);
+				continue;
+			}
+
+			object = parse_object(the_repository, &oid);
+			if (!object) {
+				fprintf(stderr, "Could not get object for %s. Skipping.\n",
+					scratch_buf.buf);
+				continue;
+			}
+
+			peeled = deref_tag(the_repository, object, scratch_buf.buf, 0);
+			if (peeled && peeled->type == OBJ_COMMIT)
+				commit = (struct commit *)peeled;
+			if (!commit) {
+				fprintf(stderr, "Could not get commit for %s. Skipping.\n",
+					*argv);
+				continue;
+			}
+
+			get_format_rev(commit, &format_pp, &scratch_buf);
+			printf("%s\n", scratch_buf.buf);
+			strbuf_release(&scratch_buf);
+		}
+		break;
+	default:
+		BUG("uncovered case: %d", stdin_mode);
+	}
+
+	strbuf_release(&scratch_buf);
+	string_list_clear(&notes, 0);
+	release_display_notes(&format_notes_opt);
+	return 0;
+}
diff --git a/command-list.txt b/command-list.txt
index f9005cf4597..df729872dca 100644
--- a/command-list.txt
+++ b/command-list.txt
@@ -108,6 +108,7 @@ git-fmt-merge-msg                       purehelpers
 git-for-each-ref                        plumbinginterrogators
 git-for-each-repo                       plumbinginterrogators
 git-format-patch                        mainporcelain
+git-format-rev                          plumbinginterrogators
 git-fsck                                ancillaryinterrogators          complete
 git-gc                                  mainporcelain
 git-get-tar-commit-id                   plumbinginterrogators
diff --git a/git.c b/git.c
index 2b212e6675d..af5b0422b00 100644
--- a/git.c
+++ b/git.c
@@ -578,6 +578,7 @@ static struct cmd_struct commands[] = {
 	{ "for-each-ref", cmd_for_each_ref, RUN_SETUP },
 	{ "for-each-repo", cmd_for_each_repo, RUN_SETUP_GENTLY },
 	{ "format-patch", cmd_format_patch, RUN_SETUP },
+	{ "format-rev", cmd_format_rev, RUN_SETUP },
 	{ "fsck", cmd_fsck, RUN_SETUP },
 	{ "fsck-objects", cmd_fsck, RUN_SETUP },
 	{ "fsmonitor--daemon", cmd_fsmonitor__daemon, RUN_SETUP },
diff --git a/t/t1517-outside-repo.sh b/t/t1517-outside-repo.sh
index c824c1a25cf..360a9323343 100755
--- a/t/t1517-outside-repo.sh
+++ b/t/t1517-outside-repo.sh
@@ -114,7 +114,8 @@ do
 	archimport | citool | credential-netrc | credential-libsecret | \
 	credential-osxkeychain | cvsexportcommit | cvsimport | cvsserver | \
 	daemon | \
-	difftool--helper | filter-branch | fsck-objects | get-tar-commit-id | \
+	difftool--helper | filter-branch | format-rev | fsck-objects | \
+	get-tar-commit-id | \
 	gui | gui--askpass | \
 	http-backend | http-fetch | http-push | init-db | \
 	merge-octopus | merge-one-file | merge-resolve | mergetool | \
diff --git a/t/t6120-describe.sh b/t/t6120-describe.sh
index 62789f76381..725f7d81b6b 100755
--- a/t/t6120-describe.sh
+++ b/t/t6120-describe.sh
@@ -801,4 +801,108 @@ test_expect_success 'do not be fooled by invalid describe format ' '
 	test_must_fail git cat-file -t "refs/tags/super-invalid/./../...../ ~^:/?*[////\\\\\\&}/busted.lock-42-g"$(cat out)
 '
 
+test_expect_success 'name-rev --format setup' '
+	mkdir repo-format &&
+	git -C repo-format init &&
+	test_commit -C repo-format first &&
+	test_commit -C repo-format second &&
+	test_commit -C repo-format third &&
+	test_commit -C repo-format fourth &&
+	test_commit -C repo-format fifth &&
+	test_commit -C repo-format sixth &&
+	test_commit -C repo-format seventh &&
+	test_commit -C repo-format eighth
+'
+
+test_expect_success 'format-rev --stdin-mode=revs' '
+	cat >expect <<-\EOF &&
+	eighth
+	seventh
+	fifth
+	EOF
+	git -C repo-format format-rev --stdin-mode=revs \
+		--format=%s >actual <<-\EOF &&
+	HEAD
+	HEAD~
+	HEAD~3
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success 'format-rev --stdin-mode=text from rev-list same as log' '
+	git -C repo-format log --format=reference >expect &&
+	test_file_not_empty expect &&
+	git -C repo-format rev-list HEAD >list &&
+	git -C repo-format format-rev --stdin-mode=text \
+		--format=reference <list >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'format-rev --stdin-mode=text with running text and tree oid' '
+	cmit_oid=$(git -C repo-format rev-parse :/fifth) &&
+	reference=$(git -C repo-format log -n1 --format=reference :/fifth) &&
+	tree=$(git -C repo-format rev-parse HEAD^{tree}) &&
+	cat >expect <<-EOF &&
+	We thought we fixed this in ${reference}.
+	But look at this tree: ${tree}.
+	EOF
+	git -C repo-format format-rev --stdin-mode=text --format=reference \
+		>actual <<-EOF &&
+	We thought we fixed this in ${cmit_oid}.
+	But look at this tree: ${tree}.
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success 'format-rev with %N (note)' '
+	test_when_finished "git -C repo-format notes remove" &&
+	git -C repo-format notes add -m"Make a note" &&
+	printf "Make a note\n\n\n" >expect &&
+	git -C repo-format format-rev --stdin-mode=revs \
+		--format="tformat:%N" \
+		>actual <<-\EOF &&
+	HEAD
+	HEAD~
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success 'format-rev --notes<ref> (custom notes ref)' '
+	# One custom notes ref
+	test_when_finished "git -C repo-format notes remove" &&
+	test_when_finished "git -C repo-format notes --ref=word remove" &&
+	git -C repo-format notes add -m"default" &&
+	git -C repo-format notes --ref=word add -m"custom" &&
+	printf "custom\n\n" >expect &&
+	git -C repo-format format-rev --stdin-mode=revs \
+		--format="tformat:%N" \
+		--notes=word \
+		>actual <<-\EOF &&
+	HEAD
+	EOF
+	test_cmp expect actual &&
+	# Glob all
+	printf "default\ncustom\n\n" >expect &&
+	git -C repo-format format-rev --stdin-mode=revs \
+		--format="tformat:%N" \
+		--notes=* >actual <<-\EOF &&
+	HEAD
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success 'format-rev --stdin-mode=revs on annotated tag peels to commit' '
+	test_when_finished "git -C repo-format tag -d version" &&
+	git -C repo-format tag -a -m"new version" version &&
+	cat >expect <<-\EOF &&
+	eighth
+	EOF
+	git -C repo-format format-rev --stdin-mode=revs \
+		--format=%s \
+		--notes=* >actual <<-\EOF &&
+	version
+	EOF
+	test_cmp expect actual
+'
+
 test_done
-- 
2.54.0.13.g9c7419e39f8
Kristoffer HaugsbakkApr 29, 2026, 13:41 UTC in reply to kristofferhaugsbakk@fastmail.com on lore

Re: [PATCH v3 5/5] format-rev: introduce builtin for on-demand pretty formatting

On Wed, Apr 29, 2026, at 00:25, kristofferhaugsbakk@fastmail.com wrote:
> From: Kristoffer Haugsbakk <code@khaugsbakk.name>
>[snip]
CI returned an error.
    builtin/name-rev.c:893:25: error: ‘commit’ may be used uninitialized in this function [-Werror=maybe-uninitialized]
      893 |                         get_format_rev(commit, &format_pp, &scratch_buf);
          |                         ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Kristoffer HaugsbakkApr 30, 2026, 06:23 UTC in reply to Kristoffer Haugsbakk on lore

Re: [PATCH v3 5/5] format-rev: introduce builtin for on-demand pretty formatting

On Wed, Apr 29, 2026, at 15:41, Kristoffer Haugsbakk wrote:
Show 12 quoted lines
> On Wed, Apr 29, 2026, at 00:25, kristofferhaugsbakk@fastmail.com wrote:
>> From: Kristoffer Haugsbakk <code@khaugsbakk.name>
>>[snip]
>
> CI returned an error.
>
>     builtin/name-rev.c:893:25: error: ‘commit’ may be used
> uninitialized in this function [-Werror=maybe-uninitialized]
>       893 |                         get_format_rev(commit, &format_pp,
> &scratch_buf);
>           |
> ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Now Im using `config.mak.dev`.
Kristoffer HaugsbakkApr 30, 2026, 09:21 UTC in reply to kristofferhaugsbakk@fastmail.com on lore

Re: [PATCH v3 5/5] format-rev: introduce builtin for on-demand pretty formatting

On Wed, Apr 29, 2026, at 00:25, kristofferhaugsbakk@fastmail.com wrote:
Show 8 quoted lines
> From: Kristoffer Haugsbakk <code@khaugsbakk.name>
>[snip]
> +			peeled = deref_tag(the_repository, object, scratch_buf.buf, 0);
> +			if (peeled && peeled->type == OBJ_COMMIT)
> +				commit = (struct commit *)peeled;
> +			if (!commit) {
> +				fprintf(stderr, "Could not get commit for %s. Skipping.\n",
> +					*argv);
s/*argv/scratch_buf.buf/
> +				continue;
> +			}
>[snip]
Phillip WoodApr 30, 2026, 13:54 UTC in reply to kristofferhaugsbakk@fastmail.com on lore

Re: [PATCH v3 3/5] name-rev: factor code for sharing with a new command

Hi Kristoffer
On 28/04/2026 23:25, kristofferhaugsbakk@fastmail.com wrote:
Show 24 quoted lines
> From: Kristoffer Haugsbakk <code@khaugsbakk.name>
> 
> @@ -516,6 +534,7 @@ static void name_rev_line(char *p, struct name_ref_data *data)
>   
>   	for (p_start = p; *p; p++) {
>   #define ishex(x) (isdigit((x)) || ((x) >= 'a' && (x) <= 'f'))
> +	start:
>   		if (!ishex(*p)) {
>   			counter = 0;
>   		} else if (++counter == hexsz &&
> @@ -524,25 +543,32 @@ static void name_rev_line(char *p, struct name_ref_data *data)
>   			const char *name = NULL;
>   			char c = *(p + 1);
>   			int p_len = p - p_start + 1;
> +			struct object *o = NULL;
> +			int oid_ret = 1;
>   
>   			counter = 0;
>   
>   			*(p + 1) = 0;
> -			if (!repo_get_oid(the_repository, p - (hexsz - 1), &oid)) {
> -				struct object *o =
> -					lookup_object(the_repository, &oid);
> +			oid_ret = repo_get_oid(the_repository, p - (hexsz - 1), &oid);

It would be safer to restore *(p + 1) here rather that relying on each case block to do it.

			*(p + 1) = c;
Show 10 quoted lines
> +
> +			switch (cmd->type) {
> +			case NAME_REV:
> +				if (!oid_ret)
> +					o = lookup_object(the_repository, &oid);
>   				if (o)
>   					name = get_rev_name(o, &buf);
> +				*(p + 1) = c;
> +				if (!name)
> +					goto start;

The pre-image uses "continue" which will increment p - why the change in behavior?

Thanks
Phillip
Show 45 quoted lines
> +				if (cmd->u.name_only)
> +					printf("%.*s%s", p_len - hexsz, p_start, name);
> +				else
> +					printf("%.*s (%s)", p_len, p_start, name);
> +				break;
> +			default:
> +				BUG("uncovered case: %d", cmd->type);
>   			}
> -			*(p + 1) = c;
> -
> -			if (!name)
> -				continue;
>   
> -			if (data->name_only)
> -				printf("%.*s%s", p_len - hexsz, p_start, name);
> -			else
> -				printf("%.*s (%s)", p_len, p_start, name);
>   			p_start = p + 1;
>   		}
>   	}
> @@ -567,6 +593,7 @@ int cmd_name_rev(int argc,
>   #endif
>   	int all = 0, annotate_stdin = 0, allow_undefined = 1, always = 0, peel_tag = 0;
>   	struct name_ref_data data = { 0, 0, STRING_LIST_INIT_NODUP, STRING_LIST_INIT_NODUP };
> +	struct command cmd;
>   	struct option opts[] = {
>   		OPT_BOOL(0, "name-only", &data.name_only, N_("print only ref-based names (no object names)")),
>   		OPT_BOOL(0, "tags", &data.tags_only, N_("only use tags to name the commits")),
> @@ -596,6 +623,7 @@ int cmd_name_rev(int argc,
>   	init_commit_rev_name(&rev_names);
>   	repo_config(the_repository, git_default_config, NULL);
>   	argc = parse_options(argc, argv, prefix, opts, name_rev_usage, 0);
> +	init_name_rev_command(&cmd, data.name_only);
>   
>   #ifndef WITH_BREAKING_CHANGES
>   	if (transform_stdin) {
> @@ -663,7 +691,7 @@ int cmd_name_rev(int argc,
>   
>   		while (strbuf_getline(&sb, stdin) != EOF) {
>   			strbuf_addch(&sb, '\n');
> -			name_rev_line(sb.buf, &data);
> +			name_rev_line(sb.buf, &cmd);
>   		}
>   		strbuf_release(&sb);
>   	} else if (all) {
Phillip WoodMay 1, 2026, 10:16 UTC in reply to kristofferhaugsbakk@fastmail.com on lore

Re: [PATCH v3 5/5] format-rev: introduce builtin for on-demand pretty formatting

Hi Kristoffer
On 28/04/2026 23:25, kristofferhaugsbakk@fastmail.com wrote:
Show 22 quoted lines
> From: Kristoffer Haugsbakk <code@khaugsbakk.name>
> 
> Introduce a new builtin for pretty formatting one revision expression
> per line or commit object names found in running text.
> 
> Sometimes you want to format commits. Most of the time you’re
> walking the graph, e.g. getting a range of commits like
> `master..topic`. That’s a job for git-log(1).
> 
> But there are times when you want to format commits that you encounter
> on demand:
> 
> • Full hashes in running text that you might want to pretty-print
> • git-last-modified(1) outputs full hashes that you can do the same
>    with
> • git-cherry(1) has `-v` for commit subject, but maybe you want
>    something else?
> 
> But now you can’t use git-log(1), git-show(1), or git-rev-list(1):
> 
> • You can’t feed commits piecemeal to these commands, one input
>    for one output; they block until standard in is closed

So you can feed them piecemeal but you don't get any output until you close stdin. That can be helpful as it means the calling process can write to "git log --stdin" and then read the output without worrying about getting deadlocked. The Implementation below works fine if there are separate processes or threads writing to and reading from "git format-rev", but if we want a single process to be able to read from and write to "git format-rev --stdin-mode=text" there will need to be a way to delimit message boundaries so that git knows where the input message ends and the caller knows where the response ends. We'll also need to be careful about flushing the output at the end of a processed message.

For "--stdin-mode=revs" the caller cannot know how many lines the output will span because formats like %(trailers) will produce a variable number of lines depending on which trailers are present. It is also possible for a rev name to span more than one line. The following example finds the most recent commit that mentions 'cherry-pick' in the subject line

:/^[^ ]cherry-pick

so we need a way to delimit the input and output records there as well. I think the functionality implemented here is useful (transforming the output of 'git blame' or 'git-last-modified' are convicing examples) and it is probably better to do it as a command rather than adding a "--format" option to name-rev.

> • You can’t feed a list of possibly duplicate commits, like the output
>    of git-last-modified(1); they effectively deduplicate the output
That is definitely a problem
> Beyond these two points there’s also the input massage problem: you
s/massagge/message/?
Thanks
Phillip
Show 610 quoted lines
> cannot feed mixed input (revisions mixed with arbitrary text).
> 
> One might hope that git-cat-file(1) can save us. But it doesn’t
> support pretty formats.
> 
> But there is one command that already both handles revisions as
> arguments, revisions on standard input, and even revisions mixed in
> with arbitrary text. Namely git-name-rev(1): the command for outputting
> symbolic names for commits.
> 
> We made some room in `builtin/name-rev.c` two commits ago. Let’s
> now add this new git-format-rev(1) command. Taking inspiration from
> git-name-rev(1), there are two modes:
> 
> • revs: like git-name-rev(1) in argv mode, but one revision per line
>    on standard in
> • text: like git-name-rev(1) with `--annotate-stdin`
> 
> ***
> 
> We need to add this command to the exception list in
> `t/t1517-outside-repo.sh` because it uses “EXPERIMENTAL!”
> in the usage line.
> 
> Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
> ---
> 
> Notes (series):
>      v3:
>      • And don’t forget to document --notes this time
>      
>            https://lore.kernel.org/git/CALnO6CB5WOTp_e7Kv3CrEbQ+3XE-gDxNVHf7qATBEbyKWfxpLg@mail.gmail.com/
> 
>   Documentation/git-format-rev.adoc | 148 ++++++++++++++++++++++++
>   Makefile                          |   1 +
>   builtin.h                         |   1 +
>   builtin/name-rev.c                | 186 ++++++++++++++++++++++++++++++
>   command-list.txt                  |   1 +
>   git.c                             |   1 +
>   t/t1517-outside-repo.sh           |   3 +-
>   t/t6120-describe.sh               | 104 +++++++++++++++++
>   8 files changed, 444 insertions(+), 1 deletion(-)
>   create mode 100644 Documentation/git-format-rev.adoc
> 
> diff --git a/Documentation/git-format-rev.adoc b/Documentation/git-format-rev.adoc
> new file mode 100644
> index 00000000000..d960001d750
> --- /dev/null
> +++ b/Documentation/git-format-rev.adoc
> @@ -0,0 +1,148 @@
> +git-format-rev(1)
> +=================
> +
> +NAME
> +----
> +git-format-rev - EXPERIMENTAL: Pretty format revisions on demand
> +
> +
> +SYNOPSIS
> +--------
> +[synopsis]
> +(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> --format=<pretty> [--notes=<ref>]
> +
> +DESCRIPTION
> +-----------
> +
> +Pretty format revisions from standard input.
> +
> +THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
> +
> +OPTIONS
> +-------
> +
> +`--stdin-mode=<mode>`::
> +	How to interpret standard input data:
> ++
> +--
> +`revs`:: Each line is interpreted as a commit. Any kind of revision
> +	expression can be used (see linkgit:gitrevisions[7]). Annotated
> +	tags are peeled (see linkgit:gitglossary[7]).
> ++
> +The argument `rev` is also accepted.
> +`text`:: Formats all commit object names found in freeform text. These
> +	must the full object names, i.e. abbreviated hexidecimal object
> +	names will not be interpreted.
> +--
> +
> +`--format=<pretty>`::
> +	Pretty format string.
> +
> +`--notes=<ref>`::
> +`--no-notes`::
> +	Custom notes ref. Notes are displayed when using the `%N`
> +	atom. See linkgit:git-notes[1].
> +
> +EXAMPLES
> +--------
> +
> +The command linkgit:git-last-modified[1] shows the commit that each file
> +was last modified in.
> +
> +----
> +$ git last-modified -- README.md Makefile
> +7798034171030be0909c56377a4e0e10e6d2df93	Makefile
> +c50fbb2dd225e7e82abba4380423ae105089f4d7	README.md
> +----
> +
> +We can pipe the result to this command in order to replace the object
> +name with the commit author.
> +
> +----
> +$ git last-modified -- README.md Makefile |
> +    git format-rev --stdin-mode=text --format=%an
> +Junio C Hamano	Makefile
> +Todd Zullinger	README.md
> +----
> +
> +Another example is _formatting commits in commit messages_. Given this commit message:
> +
> +----
> +Fix off-by-one error
> +
> +Fix off-by-one error introduced in
> +e83c5163316f89bfbde7d9ab23ca2e25604af290.
> +
> +We thought we fixed this in 5569bf9bbedd63a00780fc5c110e0cfab3aa97b9 but
> +that only covered 1/3 of the faulty cases.
> +----
> +
> +We can format the commits and use par(1) to reflow the text, say in a
> +`commit-msg` hook:
> +
> +----
> +$ git config set hook.reference-commits.event commit-msg
> +$ git config set hook.reference-commits.command reference-commits
> +$ cat $(which reference-commits)
> +#/bin/sh
> +
> +msg="$1"
> +rewritten=$(mktemp)
> +git format-rev --stdin-mode=text --format=reference <"$msg" |
> +    par >"$rewritten"
> +mv "$rewritten" "$msg"
> +----
> +
> +Which will produce something like this:
> +
> +----
> +Fix off-by-one error
> +
> +Fix off-by-one error introduced in e83c5163316 (Implement better memory
> +allocator, 2005-04-07).
> +
> +We thought we fixed this in 5569bf9bbed (Fix memory allocator,
> +2005-06-22) but that only covered 1/3 of the faulty cases.
> +----
> +
> +DISCUSSION
> +----------
> +
> +This command lets you format any number of revisions in any order
> +through one command invocation. Consider the
> +linkgit:git-last-modified[1] case from the "EXAMPLES" section above:
> +
> +1. There might be hundreds of files
> +2. Commits can be repeated, i.e. two or more files were last modified in
> +   the same commit
> +
> +Two widely-used commands which pretty formats commits are
> +linkgit:git-log[1] and linkgit:git-show[1]. It turns out that they are
> +not a good fit for the above use case.
> +
> +- The output of linkgit:git-last-modified[1] would have to be processed
> +  in stages since you need to transform the first column separately and
> +  then link the author to the filename. But this is surmountable.
> +- You can feed each commit to `git show` or `git show --no-walk -1`. But
> +  that means that you need to create a process for each line.
> +- Let’s say that you want to use one process, not one per line. So you
> +  want to feed all the commits to the command. Now you face the problem
> +  that you have to feed all the commits to the commands before you get
> +  any output (this is also the case for the `--stdin` modes). In other
> +  words, you cannot loop through each line, get the author for the
> +  commit, and output the author and the filename. You need to feed all
> +  the commits, get back all the output, and match the output with the
> +  filename.
> +- But the next problem is that commands will deduplicate the input and
> +  only output one commit one single time only. Thus you cannot make the
> +  output order match the input order, since a commit could have been
> +  repeated in the original input.
> +
> +In short, it is straightforward to use these two commands if you use one
> +process per line. It is much more work if you just want to use one
> +process, but still doable. In contrast, this problem is just another
> +shell pipeline with this command.
> +
> +GIT
> +---
> +Part of the linkgit:git[1] suite
> diff --git a/Makefile b/Makefile
> index 15b1ded1a0b..cbaf91fd846 100644
> --- a/Makefile
> +++ b/Makefile
> @@ -895,6 +895,7 @@ BUILT_INS += $(patsubst builtin/%.o,git-%$X,$(BUILTIN_OBJS))
>   BUILT_INS += git-cherry$X
>   BUILT_INS += git-cherry-pick$X
>   BUILT_INS += git-format-patch$X
> +BUILT_INS += git-format-rev$X
>   BUILT_INS += git-fsck-objects$X
>   BUILT_INS += git-init$X
>   BUILT_INS += git-maintenance$X
> diff --git a/builtin.h b/builtin.h
> index 235c51f30e5..63813c90125 100644
> --- a/builtin.h
> +++ b/builtin.h
> @@ -189,6 +189,7 @@ int cmd_fmt_merge_msg(int argc, const char **argv, const char *prefix, struct re
>   int cmd_for_each_ref(int argc, const char **argv, const char *prefix, struct repository *repo);
>   int cmd_for_each_repo(int argc, const char **argv, const char *prefix, struct repository *repo);
>   int cmd_format_patch(int argc, const char **argv, const char *prefix, struct repository *repo);
> +int cmd_format_rev(int argc, const char **argv, const char *prefix, struct repository *repo);
>   int cmd_fsck(int argc, const char **argv, const char *prefix, struct repository *repo);
>   int cmd_fsmonitor__daemon(int argc, const char **argv, const char *prefix, struct repository *repo);
>   int cmd_gc(int argc, const char **argv, const char *prefix, struct repository *repo);
> diff --git a/builtin/name-rev.c b/builtin/name-rev.c
> index dc4136f4de3..b60cc766279 100644
> --- a/builtin/name-rev.c
> +++ b/builtin/name-rev.c
> @@ -18,6 +18,9 @@
>   #include "commit-graph.h"
>   #include "wildmatch.h"
>   #include "mem-pool.h"
> +#include "pretty.h"
> +#include "revision.h"
> +#include "notes.h"
>   
>   /*
>    * One day.  See the 'name a rev shortly after epoch' test in t6120 when
> @@ -272,14 +275,26 @@ struct name_ref_data {
>   	struct string_list exclude_filters;
>   };
>   
> +struct pretty_format {
> +	struct pretty_print_context ctx;
> +	struct userformat_want want;
> +};
> +
>   enum command_type {
>   	NAME_REV = 1,
> +	FORMAT_REV = 2,
> +};
> +
> +enum stdin_mode {
> +    TEXT = 1,
> +    REVS = 2,
>   };
>   
>   struct command {
>   	enum command_type type;
>   	union {
>   		int name_only;
> +		struct pretty_format *pretty_format;
>   	} u;
>   };
>   
> @@ -290,6 +305,13 @@ static void init_name_rev_command(struct command *cmd,
>   	cmd->u.name_only = name_only;
>   }
>   
> +static void init_format_rev_command(struct command *cmd,
> +				    struct pretty_format *pretty_format)
> +{
> +	cmd->type = FORMAT_REV;
> +	cmd->u.pretty_format = pretty_format;
> +}
> +
>   static struct tip_table {
>   	struct tip_table_entry {
>   		struct object_id oid;
> @@ -495,6 +517,27 @@ static const char *get_rev_name(const struct object *o, struct strbuf *buf)
>   	}
>   }
>   
> +static const char *get_format_rev(const struct commit *c,
> +				  struct pretty_format *format_ctx,
> +				  struct strbuf *buf)
> +{
> +	strbuf_reset(buf);
> +
> +	if (format_ctx->want.notes) {
> +		struct strbuf notebuf = STRBUF_INIT;
> +
> +		format_display_notes(&c->object.oid, &notebuf,
> +				     get_log_output_encoding(),
> +				     format_ctx->ctx.fmt == CMIT_FMT_USERFORMAT);
> +		format_ctx->ctx.notes_message = strbuf_detach(&notebuf, NULL);
> +	}
> +
> +	pretty_print_commit(&format_ctx->ctx, c, buf);
> +	FREE_AND_NULL(format_ctx->ctx.notes_message);
> +
> +	return buf->buf;
> +}
> +
>   static void show_name(const struct object *obj,
>   		      const char *caller_name,
>   		      int always, int allow_undefined, int name_only)
> @@ -565,6 +608,19 @@ static void name_rev_line(char *p, struct command *cmd)
>   				else
>   					printf("%.*s (%s)", p_len, p_start, name);
>   				break;
> +			case FORMAT_REV:
> +				if (!oid_ret)
> +					o = parse_object(the_repository, &oid);
> +				if (o && o->type == OBJ_COMMIT)
> +					name = get_format_rev((const struct commit *)o,
> +							      cmd->u.pretty_format,
> +							      &buf);
> +				*(p + 1) = c;
> +				if (name)
> +					printf("%.*s%s", p_len - hexsz, p_start, name);
> +				else
> +					printf("%.*s", p_len, p_start);
> +				break;
>   			default:
>   				BUG("uncovered case: %d", cmd->type);
>   			}
> @@ -718,3 +774,133 @@ int cmd_name_rev(int argc,
>   	object_array_clear(&revs);
>   	return 0;
>   }
> +
> +static enum stdin_mode parse_stdin_mode(const char *stdin_mode)
> +{
> +	if (!strcmp(stdin_mode, "text"))
> +		return TEXT;
> +	else if (!strcmp(stdin_mode, "revs") ||
> +		 !strcmp(stdin_mode, "rev"))
> +		return REVS;
> +	else
> +		die(_("'%s' needs to be either text, revs, or rev"),
> +		    "--stdin-mode");
> +}
> +
> +static char const *const format_rev_usage[] = {
> +	N_("(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> --format=<pretty> [--notes=<ref>]"),
> +	NULL
> +};
> +
> +int cmd_format_rev(int argc,
> +		   const char **argv,
> +		   const char *prefix,
> +		   struct repository *repo UNUSED)
> +{
> +	const char *format = NULL;
> +	enum stdin_mode stdin_mode;
> +	const char *stdin_mode_arg = NULL;
> +	struct display_notes_opt format_notes_opt;
> +	struct rev_info format_rev = REV_INFO_INIT;
> +	struct pretty_format format_pp = { 0 };
> +	struct string_list notes = STRING_LIST_INIT_NODUP;
> +	struct strbuf scratch_buf = STRBUF_INIT;
> +	struct command cmd;
> +	struct option opts[] = {
> +		OPT_STRING(0, "format", &format, N_("format"),
> +			   N_("pretty format to use")),
> +		OPT_STRING(0, "stdin-mode", &stdin_mode_arg, N_("stdin-mode"),
> +			   N_("how revs are processed")),
> +		OPT_STRING_LIST(0, "notes", &notes, N_("notes"),
> +				N_("display notes for pretty format")),
> +		OPT_END(),
> +	};
> +
> +	argc = parse_options(argc, argv, prefix, opts, format_rev_usage, 0);
> +
> +	if (argc > 0) {
> +		error(_("too many arguments"));
> +		usage_with_options(format_rev_usage, opts);
> +	}
> +
> +	if (!format)
> +		die(_("'%s' is required"), "--format");
> +	if (!stdin_mode_arg)
> +		die(_("'%s' is required"), "--stdin-mode");
> +
> +	init_display_notes(&format_notes_opt);
> +	stdin_mode = parse_stdin_mode(stdin_mode_arg);
> +
> +	get_commit_format(format, &format_rev);
> +	format_pp.ctx.rev = &format_rev;
> +	format_pp.ctx.fmt = format_rev.commit_format;
> +	format_pp.ctx.abbrev = format_rev.abbrev;
> +	format_pp.ctx.date_mode_explicit = format_rev.date_mode_explicit;
> +	format_pp.ctx.date_mode = format_rev.date_mode;
> +	format_pp.ctx.color = GIT_COLOR_AUTO;
> +
> +	userformat_find_requirements(format,
> +				     &format_pp.want);
> +	if (format_pp.want.notes) {
> +		int ignore_show_notes = 0;
> +		struct string_list_item *n;
> +
> +		for_each_string_list_item(n, &notes)
> +			enable_ref_display_notes(&format_notes_opt,
> +						 &ignore_show_notes,
> +						 n->string);
> +		load_display_notes(&format_notes_opt);
> +	}
> +
> +	init_format_rev_command(&cmd, &format_pp);
> +
> +	switch (stdin_mode) {
> +	case TEXT:
> +		while (strbuf_getline(&scratch_buf, stdin) != EOF) {
> +			strbuf_addch(&scratch_buf, '\n');
> +			name_rev_line(scratch_buf.buf, &cmd);
> +		}
> +		break;
> +	case REVS:
> +		while (strbuf_getline(&scratch_buf, stdin) != EOF) {
> +			struct object_id oid;
> +			struct object *object;
> +			struct object *peeled;
> +			struct commit *commit;
> +
> +			if (repo_get_oid(the_repository, scratch_buf.buf, &oid)) {
> +				fprintf(stderr, "Could not get sha1 for %s. Skipping.\n",
> +					scratch_buf.buf);
> +				continue;
> +			}
> +
> +			object = parse_object(the_repository, &oid);
> +			if (!object) {
> +				fprintf(stderr, "Could not get object for %s. Skipping.\n",
> +					scratch_buf.buf);
> +				continue;
> +			}
> +
> +			peeled = deref_tag(the_repository, object, scratch_buf.buf, 0);
> +			if (peeled && peeled->type == OBJ_COMMIT)
> +				commit = (struct commit *)peeled;
> +			if (!commit) {
> +				fprintf(stderr, "Could not get commit for %s. Skipping.\n",
> +					*argv);
> +				continue;
> +			}
> +
> +			get_format_rev(commit, &format_pp, &scratch_buf);
> +			printf("%s\n", scratch_buf.buf);
> +			strbuf_release(&scratch_buf);
> +		}
> +		break;
> +	default:
> +		BUG("uncovered case: %d", stdin_mode);
> +	}
> +
> +	strbuf_release(&scratch_buf);
> +	string_list_clear(&notes, 0);
> +	release_display_notes(&format_notes_opt);
> +	return 0;
> +}
> diff --git a/command-list.txt b/command-list.txt
> index f9005cf4597..df729872dca 100644
> --- a/command-list.txt
> +++ b/command-list.txt
> @@ -108,6 +108,7 @@ git-fmt-merge-msg                       purehelpers
>   git-for-each-ref                        plumbinginterrogators
>   git-for-each-repo                       plumbinginterrogators
>   git-format-patch                        mainporcelain
> +git-format-rev                          plumbinginterrogators
>   git-fsck                                ancillaryinterrogators          complete
>   git-gc                                  mainporcelain
>   git-get-tar-commit-id                   plumbinginterrogators
> diff --git a/git.c b/git.c
> index 2b212e6675d..af5b0422b00 100644
> --- a/git.c
> +++ b/git.c
> @@ -578,6 +578,7 @@ static struct cmd_struct commands[] = {
>   	{ "for-each-ref", cmd_for_each_ref, RUN_SETUP },
>   	{ "for-each-repo", cmd_for_each_repo, RUN_SETUP_GENTLY },
>   	{ "format-patch", cmd_format_patch, RUN_SETUP },
> +	{ "format-rev", cmd_format_rev, RUN_SETUP },
>   	{ "fsck", cmd_fsck, RUN_SETUP },
>   	{ "fsck-objects", cmd_fsck, RUN_SETUP },
>   	{ "fsmonitor--daemon", cmd_fsmonitor__daemon, RUN_SETUP },
> diff --git a/t/t1517-outside-repo.sh b/t/t1517-outside-repo.sh
> index c824c1a25cf..360a9323343 100755
> --- a/t/t1517-outside-repo.sh
> +++ b/t/t1517-outside-repo.sh
> @@ -114,7 +114,8 @@ do
>   	archimport | citool | credential-netrc | credential-libsecret | \
>   	credential-osxkeychain | cvsexportcommit | cvsimport | cvsserver | \
>   	daemon | \
> -	difftool--helper | filter-branch | fsck-objects | get-tar-commit-id | \
> +	difftool--helper | filter-branch | format-rev | fsck-objects | \
> +	get-tar-commit-id | \
>   	gui | gui--askpass | \
>   	http-backend | http-fetch | http-push | init-db | \
>   	merge-octopus | merge-one-file | merge-resolve | mergetool | \
> diff --git a/t/t6120-describe.sh b/t/t6120-describe.sh
> index 62789f76381..725f7d81b6b 100755
> --- a/t/t6120-describe.sh
> +++ b/t/t6120-describe.sh
> @@ -801,4 +801,108 @@ test_expect_success 'do not be fooled by invalid describe format ' '
>   	test_must_fail git cat-file -t "refs/tags/super-invalid/./../...../ ~^:/?*[////\\\\\\&}/busted.lock-42-g"$(cat out)
>   '
>   
> +test_expect_success 'name-rev --format setup' '
> +	mkdir repo-format &&
> +	git -C repo-format init &&
> +	test_commit -C repo-format first &&
> +	test_commit -C repo-format second &&
> +	test_commit -C repo-format third &&
> +	test_commit -C repo-format fourth &&
> +	test_commit -C repo-format fifth &&
> +	test_commit -C repo-format sixth &&
> +	test_commit -C repo-format seventh &&
> +	test_commit -C repo-format eighth
> +'
> +
> +test_expect_success 'format-rev --stdin-mode=revs' '
> +	cat >expect <<-\EOF &&
> +	eighth
> +	seventh
> +	fifth
> +	EOF
> +	git -C repo-format format-rev --stdin-mode=revs \
> +		--format=%s >actual <<-\EOF &&
> +	HEAD
> +	HEAD~
> +	HEAD~3
> +	EOF
> +	test_cmp expect actual
> +'
> +
> +test_expect_success 'format-rev --stdin-mode=text from rev-list same as log' '
> +	git -C repo-format log --format=reference >expect &&
> +	test_file_not_empty expect &&
> +	git -C repo-format rev-list HEAD >list &&
> +	git -C repo-format format-rev --stdin-mode=text \
> +		--format=reference <list >actual &&
> +	test_cmp expect actual
> +'
> +
> +test_expect_success 'format-rev --stdin-mode=text with running text and tree oid' '
> +	cmit_oid=$(git -C repo-format rev-parse :/fifth) &&
> +	reference=$(git -C repo-format log -n1 --format=reference :/fifth) &&
> +	tree=$(git -C repo-format rev-parse HEAD^{tree}) &&
> +	cat >expect <<-EOF &&
> +	We thought we fixed this in ${reference}.
> +	But look at this tree: ${tree}.
> +	EOF
> +	git -C repo-format format-rev --stdin-mode=text --format=reference \
> +		>actual <<-EOF &&
> +	We thought we fixed this in ${cmit_oid}.
> +	But look at this tree: ${tree}.
> +	EOF
> +	test_cmp expect actual
> +'
> +
> +test_expect_success 'format-rev with %N (note)' '
> +	test_when_finished "git -C repo-format notes remove" &&
> +	git -C repo-format notes add -m"Make a note" &&
> +	printf "Make a note\n\n\n" >expect &&
> +	git -C repo-format format-rev --stdin-mode=revs \
> +		--format="tformat:%N" \
> +		>actual <<-\EOF &&
> +	HEAD
> +	HEAD~
> +	EOF
> +	test_cmp expect actual
> +'
> +
> +test_expect_success 'format-rev --notes<ref> (custom notes ref)' '
> +	# One custom notes ref
> +	test_when_finished "git -C repo-format notes remove" &&
> +	test_when_finished "git -C repo-format notes --ref=word remove" &&
> +	git -C repo-format notes add -m"default" &&
> +	git -C repo-format notes --ref=word add -m"custom" &&
> +	printf "custom\n\n" >expect &&
> +	git -C repo-format format-rev --stdin-mode=revs \
> +		--format="tformat:%N" \
> +		--notes=word \
> +		>actual <<-\EOF &&
> +	HEAD
> +	EOF
> +	test_cmp expect actual &&
> +	# Glob all
> +	printf "default\ncustom\n\n" >expect &&
> +	git -C repo-format format-rev --stdin-mode=revs \
> +		--format="tformat:%N" \
> +		--notes=* >actual <<-\EOF &&
> +	HEAD
> +	EOF
> +	test_cmp expect actual
> +'
> +
> +test_expect_success 'format-rev --stdin-mode=revs on annotated tag peels to commit' '
> +	test_when_finished "git -C repo-format tag -d version" &&
> +	git -C repo-format tag -a -m"new version" version &&
> +	cat >expect <<-\EOF &&
> +	eighth
> +	EOF
> +	git -C repo-format format-rev --stdin-mode=revs \
> +		--format=%s \
> +		--notes=* >actual <<-\EOF &&
> +	version
> +	EOF
> +	test_cmp expect actual
> +'
> +
>   test_done
kristofferhaugsbakk@fastmail.comMay 1, 2026, 17:24 UTC in reply to Phillip Wood on lore

Re: [PATCH v3 3/5] name-rev: factor code for sharing with a new command

Hi Phillip. Thanks for taking a look.
On Thu, Apr 30, 2026, at 15:54, Phillip Wood wrote:
Show 18 quoted lines
>>[snip]
>> @@ -524,25 +543,32 @@ static void name_rev_line(char *p, struct name_ref_data *data)
>>   			const char *name = NULL;
>>   			char c = *(p + 1);
>>   			int p_len = p - p_start + 1;
>> +			struct object *o = NULL;
>> +			int oid_ret = 1;
>>
>>   			counter = 0;
>>
>>   			*(p + 1) = 0;
>> -			if (!repo_get_oid(the_repository, p - (hexsz - 1), &oid)) {
>> -				struct object *o =
>> -					lookup_object(the_repository, &oid);
>> +			oid_ret = repo_get_oid(the_repository, p - (hexsz - 1), &oid);
>
> It would be safer to restore *(p + 1) here rather that relying on each
> case block to do it.

Yeah, I didn’t want to repeat that bookkeeping but in some iteration it looked necessary. But it’s good that it isn’t.

Show 15 quoted lines
>
> 			*(p + 1) = c;
>> +
>> +			switch (cmd->type) {
>> +			case NAME_REV:
>> +				if (!oid_ret)
>> +					o = lookup_object(the_repository, &oid);
>>   				if (o)
>>   					name = get_rev_name(o, &buf);
>> +				*(p + 1) = c;
>> +				if (!name)
>> +					goto start;
>
> The pre-image uses "continue" which will increment p - why the change in
> behavior?

They looked the same to me. So I will need to think about this some more. Just a lack of C experience on my part.

Replacing the `continue` with a goto at the start of the loop was also unnecessary. Of course the `continue` breaks out of the loop and not the switch-block (unlike `break`).

But I didn’t break `t6120-describe.sh`. So I’ll also take a look to see if there are any holes.

Thanks again.
>[snip]
-- 
Happy May Day
kristofferhaugsbakk@fastmail.comMay 1, 2026, 18:27 UTC in reply to Phillip Wood on lore

Re: [PATCH v3 5/5] format-rev: introduce builtin for on-demand pretty formatting

On Fri, May 1, 2026, at 12:16, Phillip Wood wrote:
Show 8 quoted lines
>>[snip]
>> • You can’t feed commits piecemeal to these commands, one input
>>    for one output; they block until standard in is closed
>
> So you can feed them piecemeal but you don't get any output until you
> close stdin. That can be helpful as it means the calling process can
> write to "git log --stdin" and then read the output without worrying
> about getting deadlocked.
Okay. I don’t have much experience with concurrent programming.
Show 6 quoted lines
> The Implementation below works fine if there
> are separate processes or threads writing to and reading from "git
> format-rev", but if we want a single process to be able to read from and
> write to "git format-rev --stdin-mode=text" there will need to be a way
> to delimit message boundaries so that git knows where the input message
> ends and the caller knows where the response ends.
Okay, so I guess a null-terminator mode for output.
> We'll also need to be
> careful about flushing the output at the end of a processed message.
I don’t get why this takes special care. I’ll think about it.
Show 11 quoted lines
> For "--stdin-mode=revs" the caller cannot know how many lines the output
> will span because formats like %(trailers) will produce a variable
> number of lines depending on which trailers are present. It is also
> possible for a rev name to span more than one line. The following
> example finds the most recent commit that mentions 'cherry-pick' in the
> subject line
>
> :/^[^
> ]cherry-pick
>
> so we need a way to delimit the input and output records there as well.
Okay, so a null-terminator mode for input as well?
Show 9 quoted lines
> I think the functionality implemented here is useful (transforming the
> output of 'git blame' or 'git-last-modified' are convicing examples) and
> it is probably better to do it as a command rather than adding a
> "--format" option to name-rev.
>
>> • You can’t feed a list of possibly duplicate commits, like the output
>>    of git-last-modified(1); they effectively deduplicate the output
>
> That is definitely a problem
Great, thanks.
>> Beyond these two points there’s also the input massage problem: you
>
> s/massagge/message/?

No. I meant massaging the input so that it can be processed by whatever tool you have. :) In this case splitting the object name column and file column because tools like git-log(1) can only deal with revision input.

Thanks for reviewing the usability design.
-- 
Happy May Day
Phillip WoodMay 2, 2026, 10:00 UTC in reply to kristofferhaugsbakk@fastmail.com on lore

Re: [PATCH v3 5/5] format-rev: introduce builtin for on-demand pretty formatting

Hi Kristoffer
On 01/05/2026 19:27, kristofferhaugsbakk@fastmail.com wrote:
Show 20 quoted lines
> On Fri, May 1, 2026, at 12:16, Phillip Wood wrote:
>>> [snip]
>>> • You can’t feed commits piecemeal to these commands, one input
>>>     for one output; they block until standard in is closed
>>
>> So you can feed them piecemeal but you don't get any output until you
>> close stdin. That can be helpful as it means the calling process can
>> write to "git log --stdin" and then read the output without worrying
>> about getting deadlocked.
> 
> Okay. I don’t have much experience with concurrent programming.
> 
>> The Implementation below works fine if there
>> are separate processes or threads writing to and reading from "git
>> format-rev", but if we want a single process to be able to read from and
>> write to "git format-rev --stdin-mode=text" there will need to be a way
>> to delimit message boundaries so that git knows where the input message
>> ends and the caller knows where the response ends.
> 
> Okay, so I guess a null-terminator mode for output.
I think that's a good idea
>> We'll also need to be
>> careful about flushing the output at the end of a processed message.
> 
> I don’t get why this takes special care. I’ll think about it.

Because the output from printf() is buffered, unless you explicitly flush it you can get into a state where git thinks it has printed the output and is waiting for the caller to write more input, but the caller is still waiting to read git's output and so they are deadlocked. Calling maybe_flush_or_die() is the usual way to handle this I think - see 344a107b55 (merge-tree --stdin: flush stdout to avoid deadlock, 2025-02-18)

Show 13 quoted lines
>> For "--stdin-mode=revs" the caller cannot know how many lines the output
>> will span because formats like %(trailers) will produce a variable
>> number of lines depending on which trailers are present. It is also
>> possible for a rev name to span more than one line. The following
>> example finds the most recent commit that mentions 'cherry-pick' in the
>> subject line
>>
>> :/^[^
>> ]cherry-pick
>>
>> so we need a way to delimit the input and output records there as well.
> 
> Okay, so a null-terminator mode for input as well?
Yes I think "-z" should mean NUL terminated input and output.
Show 19 quoted lines
>> I think the functionality implemented here is useful (transforming the
>> output of 'git blame' or 'git-last-modified' are convicing examples) and
>> it is probably better to do it as a command rather than adding a
>> "--format" option to name-rev.
>>
>>> • You can’t feed a list of possibly duplicate commits, like the output
>>>     of git-last-modified(1); they effectively deduplicate the output
>>
>> That is definitely a problem
> 
> Great, thanks.
> 
>>> Beyond these two points there’s also the input massage problem: you
>>
>> s/massagge/message/?
> 
> No. I meant massaging the input so that it can be processed by whatever
> tool you have. :) In this case splitting the object name column and file
> column because tools like git-log(1) can only deal with revision input.
Oh I see
Thanks
Phillip
> Thanks for reviewing the usability design.
> 
Phillip WoodMay 2, 2026, 10:00 UTC in reply to kristofferhaugsbakk@fastmail.com on lore

Re: [PATCH v3 3/5] name-rev: factor code for sharing with a new command

On 01/05/2026 18:24, kristofferhaugsbakk@fastmail.com wrote:
Show 24 quoted lines
> Hi Phillip. Thanks for taking a look.
> 
> On Thu, Apr 30, 2026, at 15:54, Phillip Wood wrote:
>>> [snip]
>>> @@ -524,25 +543,32 @@ static void name_rev_line(char *p, struct name_ref_data *data)
>>>    			const char *name = NULL;
>>>    			char c = *(p + 1);
>>>    			int p_len = p - p_start + 1;
>>> +			struct object *o = NULL;
>>> +			int oid_ret = 1;
>>>
>>>    			counter = 0;
>>>
>>>    			*(p + 1) = 0;
>>> -			if (!repo_get_oid(the_repository, p - (hexsz - 1), &oid)) {
>>> -				struct object *o =
>>> -					lookup_object(the_repository, &oid);
>>> +			oid_ret = repo_get_oid(the_repository, p - (hexsz - 1), &oid);
>>
>> It would be safer to restore *(p + 1) here rather that relying on each
>> case block to do it.
> 
> Yeah, I didn’t want to repeat that bookkeeping but in some iteration it
> looked necessary. But it’s good that it isn’t.

It looks like the printing code is shared between the two case blocks in patch 5 as well so we should move that outside them as well and just set "name" inside the switch statement.

Show 24 quoted lines
>> 			*(p + 1) = c;
>>> +
>>> +			switch (cmd->type) {
>>> +			case NAME_REV:
>>> +				if (!oid_ret)
>>> +					o = lookup_object(the_repository, &oid);
>>>    				if (o)
>>>    					name = get_rev_name(o, &buf);
>>> +				*(p + 1) = c;
>>> +				if (!name)
>>> +					goto start;
>>
>> The pre-image uses "continue" which will increment p - why the change in
>> behavior?
> 
> They looked the same to me. So I will need to think about this some
> more. Just a lack of C experience on my part.
> 
> Replacing the `continue` with a goto at the start of the loop was also
> unnecessary. Of course the `continue` breaks out of the loop and not the
> switch-block (unlike `break`).
> 
> But I didn’t break `t6120-describe.sh`. So I’ll also take a look to see
> if there are any holes.

I think the difference only matters in pathological cases as "goto start" means we end up looking at the same character twice but the loop carries on as normal after that. We should just keep using "continue", I'm not sure we need a new test case.

Thanks
Phillip
Show 5 quoted lines
> 
> Thanks again.
> 
>> [snip]
> 
Junio C HamanoMay 3, 2026, 19:19 UTC in reply to kristofferhaugsbakk@fastmail.com on lore

Re: [PATCH v3 5/5] format-rev: introduce builtin for on-demand pretty formatting

builtin/name-rev.c:883:8: error: variable 'commit' is used uninitialized whenever 'if' condition is false [-Werror,-Wsometimes-uninitialized]
  883 |                         if (peeled && peeled->type == OBJ_COMMIT)
      |                             ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
builtin/name-rev.c:885:9: note: uninitialized use occurs here
  885 |                         if (!commit) {
      |                              ^~~~~~
builtin/name-rev.c:883:4: note: remove the 'if' if its condition is always true
  883 |                         if (peeled && peeled->type == OBJ_COMMIT)
      |                         ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  884 |                                 commit = (struct commit *)peeled;
builtin/name-rev.c:883:8: error: variable 'commit' is used uninitialized whenever '&&' condition is false [-Werror,-Wsometimes-uninitialized]
  883 |                         if (peeled && peeled->type == OBJ_COMMIT)
      |                             ^~~~~~
builtin/name-rev.c:885:9: note: uninitialized use occurs here
  885 |                         if (!commit) {
      |                              ^~~~~~
builtin/name-rev.c:883:8: note: remove the '&&' if its condition is always true
  883 |                         if (peeled && peeled->type == OBJ_COMMIT)
      |                             ^~~~~~~~~
builtin/name-rev.c:867:25: note: initialize the variable 'commit' to silence this warning
  867 |                         struct commit *commit;
      |                                              ^
      |                                               = NULL
2 errors generated.
 
Kristoffer HaugsbakkMay 5, 2026, 19:21 UTC in reply to Phillip Wood on lore

Re: [PATCH v3 3/5] name-rev: factor code for sharing with a new command

On Sat, May 2, 2026, at 12:00, Phillip Wood wrote:
Show 8 quoted lines
>>>[snip]
>>
>> Yeah, I didn’t want to repeat that bookkeeping but in some iteration it
>> looked necessary. But it’s good that it isn’t.
>
> It looks like the printing code is shared between the two case blocks in
> patch 5 as well so we should move that outside them as well and just set
> "name" inside the switch statement.

They’re not shared. Both commands work the same: either the object name is consumed and replaced or it is written out as-is, in other words when commit lookup fails. But somehow the name-rev path prints that *failure to look up* case before continuing here (I don’t know how):

    if (!name)
        continue;

Because the printf(3) only prints when a symbolic name was found. Either name-only:

    <symbolic name>
Or not name-only:
    <object name> (<symbolic name>)

On the other hand format-rev uses those two print statements to output either the name lookup case or the lookup failure case.

Show 16 quoted lines
>>>[snip]
>>
>> They looked the same to me. So I will need to think about this some
>> more. Just a lack of C experience on my part.
>>
>> Replacing the `continue` with a goto at the start of the loop was also
>> unnecessary. Of course the `continue` breaks out of the loop and not the
>> switch-block (unlike `break`).
>>
>> But I didn’t break `t6120-describe.sh`. So I’ll also take a look to see
>> if there are any holes.
>
> I think the difference only matters in pathological cases as "goto
> start" means we end up looking at the same character twice but the loop
> carries on as normal after that. We should just keep using "continue",
> I'm not sure we need a new test case.
Yeah, I have gone back to the sensible `continue`. Thanks.

(To be honest I tried to provoke a parsing bug here but I was unable to. Somewhat annoying.)

Kristoffer HaugsbakkMay 5, 2026, 19:27 UTC in reply to Phillip Wood on lore

Re: [PATCH v3 5/5] format-rev: introduce builtin for on-demand pretty formatting

On Sat, May 2, 2026, at 12:00, Phillip Wood wrote:
Show 13 quoted lines
>>>[snip]
>>> We'll also need to be
>>> careful about flushing the output at the end of a processed message.
>>
>> I don’t get why this takes special care. I’ll think about it.
>
> Because the output from printf() is buffered, unless you explicitly
> flush it you can get into a state where git thinks it has printed the
> output and is waiting for the caller to write more input, but the caller
> is still waiting to read git's output and so they are deadlocked.
> Calling maybe_flush_or_die() is the usual way to handle this I think -
> see 344a107b55 (merge-tree --stdin: flush stdout to avoid deadlock,
> 2025-02-18)
Ah, I understand now. Very well explained. Thanks :)
Show 15 quoted lines
>>> For "--stdin-mode=revs" the caller cannot know how many lines the output
>>> will span because formats like %(trailers) will produce a variable
>>> number of lines depending on which trailers are present. It is also
>>> possible for a rev name to span more than one line. The following
>>> example finds the most recent commit that mentions 'cherry-pick' in the
>>> subject line
>>>
>>> :/^[^
>>> ]cherry-pick
>>>
>>> so we need a way to delimit the input and output records there as well.
>>
>> Okay, so a null-terminator mode for input as well?
>
> Yes I think "-z" should mean NUL terminated input and output.

I will use `-z` (and `--null`) to mean NUL terminated input and output based on your recommendation and because I see that it is the approach used in other commands that I have found that have NUL termination for both stdin and stdout.

I also want to supply `--null-input` and `--null-output` since I think `--null-output` will be more generally useful.

***

That these long options ended up being called `--null` instead of `--nul` by convention is maybe just a historical accident? Considering one writes NUL byte/character.

>[snip]
kristofferhaugsbakk@fastmail.comMay 7, 2026, 19:34 UTC in reply to kristofferhaugsbakk@fastmail.com on lore

[PATCH v4 0/5] format-rev: introduce builtin for on-demand pretty formatting

From: Kristoffer Haugsbakk <code@khaugsbakk.name>
(Subject from v2: name-rev: learn --format=<pretty>)
Topic name (applied): kh/name-rev-custom-format

Topic summary: Introduce a new builtin for pretty formatting either (1) one revision expression per line or (2) commit object names found in running text.

See the last patch for the motivation. In short there isn’t anything that I have found that lets you format however many commits you want through one process (so looping over `git show` is excepted).

The other patches prepare for this change.
§ Changes in v4
Main changes are all in the final patch, with details on the Note there:

• Fix `-Werror=maybe-uninitialized` error • Add NUL terminator options • Use `maybe_flush_or_die(...)` to avoid potential deadlocks.

Also changes to patch 3/5 (see details on the Note).

There is also a lot of discussion about the input and output format and how it works. I don’t know if it is valuable or just noise. Like this part:

    Regarding input in this mode: using `-z` or `--null-input` makes sure
    that _NUL_ characters in the input are passed through correctly.

This is just saying that NUL in the input will cut the processing short unless `--null-input` is given (because we loop over a NUL-terminated C string in `name_rev_line(...)`).

§ CI/CB

This time I ran GitHub actions (in my private fork) and let it finish *before* sending this out.[1]

A lot of failures there but I did not see any errors relating to my changes. I could just see `git-compat-util.h` errors and C11 `Generic`.

† 1: https://lore.kernel.org/git/374661c1-4676-4538-af24-0564f38469ca@app.fastmail.com/
§ Why experimental command? (unchanged in v4)

This command is marked Experimental three times (like git-replay(1)).[2] Not so much because the UI design seems difficult. It’s more so that it can be thrown out if it doesn’t end up being worth having around.

(*Experimental* also implies wholesale trashing. Right? That’s one possible change in behavior.)

Or the behavior here could be moved somewhere else.
† 2: I vaguely recall reading a good argument for emphasizing this on the
     mailing list. I wasn’t able to find back to it right now.
§ Outstanding work

I could look more into how to do the translation strings for “X is require” (arg), I’m probably missing something that already exists.

§ Link to v3
https://lore.kernel.org/git/V3_format-rev_new_builtin.66f@msgid.xyz/

[1/5] name-rev: wrap both blocks in braces [2/5] name-rev: run clang-format before factoring code [3/5] name-rev: factor code for sharing with a new command [4/5] name-rev: make dedicated --annotate-stdin --name-only test [5/5] format-rev: introduce builtin for on-demand pretty formatting

 .gitignore                        |   1 +
 Documentation/git-format-rev.adoc | 215 +++++++++++++++++++++
 Documentation/meson.build         |   1 +
 Makefile                          |   1 +
 builtin.h                         |   1 +
 builtin/name-rev.c                | 300 +++++++++++++++++++++++++++---
 command-list.txt                  |   1 +
 git.c                             |   1 +
 t/t1517-outside-repo.sh           |   3 +-
 t/t6120-describe.sh               | 208 +++++++++++++++++++++
 10 files changed, 706 insertions(+), 26 deletions(-)
 create mode 100644 Documentation/git-format-rev.adoc
Interdiff against v3:
Show changes to 5 files +220 −25

.gitignore, Documentation/git-format-rev.adoc, Documentation/meson.build, builtin/name-rev.c, t/t6120-describe.sh

diff --git a/.gitignore b/.gitignore
index 24635cf2d6f..e406d3741cd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -71,6 +71,7 @@
 /git-for-each-ref
 /git-for-each-repo
 /git-format-patch
+/git-format-rev
 /git-fsck
 /git-fsck-objects
 /git-fsmonitor--daemon
diff --git a/Documentation/git-format-rev.adoc b/Documentation/git-format-rev.adoc
index d960001d750..436980012bc 100644
--- a/Documentation/git-format-rev.adoc
+++ b/Documentation/git-format-rev.adoc
@@ -9,7 +9,7 @@ git-format-rev - EXPERIMENTAL: Pretty format revisions on demand
 SYNOPSIS
 --------
 [synopsis]
-(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> --format=<pretty> [--notes=<ref>]
+(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> --format=<pretty> [--[no-]notes=<ref>] [-z] [--[no-]null-output] [--[no-]null-input]
 
 DESCRIPTION
 -----------
@@ -25,7 +25,8 @@ OPTIONS
 	How to interpret standard input data:
 +
 --
-`revs`:: Each line is interpreted as a commit. Any kind of revision
+`revs`:: Each line or record (see the <<io,INPUT AND OUTPUT FORMATS>>
+	section) is interpreted as a commit. Any kind of revision
 	expression can be used (see linkgit:gitrevisions[7]). Annotated
 	tags are peeled (see linkgit:gitglossary[7]).
 +
@@ -33,6 +34,9 @@ The argument `rev` is also accepted.
 `text`:: Formats all commit object names found in freeform text. These
 	must the full object names, i.e. abbreviated hexidecimal object
 	names will not be interpreted.
++
+Anything that is parsed as an object name but that is not found to be a
+commit object name is left alone (echoed).
 --
 
 `--format=<pretty>`::
@@ -43,6 +47,63 @@ The argument `rev` is also accepted.
 	Custom notes ref. Notes are displayed when using the `%N`
 	atom. See linkgit:git-notes[1].
 
+`-z`::
+`--null`::
+	Use _NUL_ character to terminate both input and output instead
+	of newline. This option cannot be negated.
++
+This is useful if both the input and output could contain newlines or if
+the input could contain _NUL_ characters; see the <<io,INPUT AND OUTPUT
+FORMATS>> section.
+
+`--null-output`::
+`--no-null-output`::
+	Use _NUL_ character to terminate output instead of newline. The
+	default is `--no-null-output`.
++
+This is useful if the output could contain newlines, for example if the
+`%n` (newline) atom is used.
+
+`--null-input`::
+`--no-null-input`::
+	Use _NUL_ character to terminate input instead of newline. The
+	default is `--no-null-input`.
++
+This is useful if the input revision expressions could contain newlines.
+It is also useful if the input could contain _NUL_ characters; see the
+<<io,INPUT AND OUTPUT FORMATS>> section.
+
+[[io]]
+INPUT AND OUTPUT FORMAT
+-----------------------
+
+The command uses newlines for both input and output termination by
+default. See the `-z`, `--null-output`, and `--null-input` options for
+using _NUL_ character as the terminator.
+
+The mode `--stdin-mode=revs` outputs one formatted commit followed by
+the terminator. This could either be called a _line_ or a _record_ in
+case "line" is too suggestive of newline termination.
+
+Note that this means that the terminator character (newline or _NUL_)
+acts as a _terminator_, not a _separator_. In other words, the final
+line or record is also terminated by the terminator character.
+
+The mode `--stdin-mode=text` replaces each object name with the
+formatted commit, i.e. the format `%s` would transform the object name
+`abcdef012...` to `<subject>` without any termination. Like this:
+
+----
+Did we not fix this in "<subject>"?
+----
+
+Regarding input in this mode: using `-z` or `--null-input` makes sure
+that _NUL_ characters in the input are passed through correctly.
+
+It is safe to interactively read and write from this command since each
+record is immediately flushed.
+
+[[examples]]
 EXAMPLES
 --------
 
@@ -110,7 +171,8 @@ DISCUSSION
 
 This command lets you format any number of revisions in any order
 through one command invocation. Consider the
-linkgit:git-last-modified[1] case from the "EXAMPLES" section above:
+linkgit:git-last-modified[1] case from the <<examples,EXAMPLES>> section
+above:
 
 1. There might be hundreds of files
 2. Commits can be repeated, i.e. two or more files were last modified in
@@ -123,7 +185,7 @@ not a good fit for the above use case.
 - The output of linkgit:git-last-modified[1] would have to be processed
   in stages since you need to transform the first column separately and
   then link the author to the filename. But this is surmountable.
-- You can feed each commit to `git show` or `git show --no-walk -1`. But
+- You can feed each commit to `git show` or `git log --no-walk -1`. But
   that means that you need to create a process for each line.
 - Let’s say that you want to use one process, not one per line. So you
   want to feed all the commits to the command. Now you face the problem
@@ -143,6 +205,11 @@ process per line. It is much more work if you just want to use one
 process, but still doable. In contrast, this problem is just another
 shell pipeline with this command.
 
+SEE ALSO
+--------
+linkgit:git-name-rev[1],
+linkgit:git-log[1].
+
 GIT
 ---
 Part of the linkgit:git[1] suite
diff --git a/Documentation/meson.build b/Documentation/meson.build
index d6365b888bb..58e7c6a0b8a 100644
--- a/Documentation/meson.build
+++ b/Documentation/meson.build
@@ -55,6 +55,7 @@ manpages = {
   'git-for-each-ref.adoc' : 1,
   'git-for-each-repo.adoc' : 1,
   'git-format-patch.adoc' : 1,
+  'git-format-rev.adoc' : 1,
   'git-fsck-objects.adoc' : 1,
   'git-fsck.adoc' : 1,
   'git-fsmonitor--daemon.adoc' : 1,
diff --git a/builtin/name-rev.c b/builtin/name-rev.c
index b60cc766279..5494b0424b3 100644
--- a/builtin/name-rev.c
+++ b/builtin/name-rev.c
@@ -21,6 +21,7 @@
 #include "pretty.h"
 #include "revision.h"
 #include "notes.h"
+#include "write-or-die.h"
 
 /*
  * One day.  See the 'name a rev shortly after epoch' test in t6120 when
@@ -577,7 +578,6 @@ static void name_rev_line(char *p, struct command *cmd)
 
 	for (p_start = p; *p; p++) {
 #define ishex(x) (isdigit((x)) || ((x) >= 'a' && (x) <= 'f'))
-	start:
 		if (!ishex(*p)) {
 			counter = 0;
 		} else if (++counter == hexsz &&
@@ -593,6 +593,7 @@ static void name_rev_line(char *p, struct command *cmd)
 
 			*(p + 1) = 0;
 			oid_ret = repo_get_oid(the_repository, p - (hexsz - 1), &oid);
+			*(p + 1) = c;
 
 			switch (cmd->type) {
 			case NAME_REV:
@@ -600,9 +601,8 @@ static void name_rev_line(char *p, struct command *cmd)
 					o = lookup_object(the_repository, &oid);
 				if (o)
 					name = get_rev_name(o, &buf);
-				*(p + 1) = c;
 				if (!name)
-					goto start;
+					continue;
 				if (cmd->u.name_only)
 					printf("%.*s%s", p_len - hexsz, p_start, name);
 				else
@@ -615,7 +615,6 @@ static void name_rev_line(char *p, struct command *cmd)
 					name = get_format_rev((const struct commit *)o,
 							      cmd->u.pretty_format,
 							      &buf);
-				*(p + 1) = c;
 				if (name)
 					printf("%.*s%s", p_len - hexsz, p_start, name);
 				else
@@ -775,6 +774,23 @@ int cmd_name_rev(int argc,
 	return 0;
 }
 
+struct format_nul_data {
+	bool nul_input;
+	bool nul_output;
+};
+
+static int format_nul_cb(const struct option *option,
+			 const char *arg,
+			 int unset)
+{
+	struct format_nul_data *data = option->value;
+	data->nul_input = 1;
+	data->nul_output = 1;
+	BUG_ON_OPT_NEG(unset);
+	BUG_ON_OPT_ARG(arg);
+	return 0;
+}
+
 static enum stdin_mode parse_stdin_mode(const char *stdin_mode)
 {
 	if (!strcmp(stdin_mode, "text"))
@@ -788,7 +804,9 @@ static enum stdin_mode parse_stdin_mode(const char *stdin_mode)
 }
 
 static char const *const format_rev_usage[] = {
-	N_("(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> --format=<pretty> [--notes=<ref>]"),
+	N_("(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> "
+	   "--format=<pretty> [--[no-]notes=<ref>] "
+	   "[-z] [--[no-]null-output] [--[no-]null-input]"),
 	NULL
 };
 
@@ -800,6 +818,9 @@ int cmd_format_rev(int argc,
 	const char *format = NULL;
 	enum stdin_mode stdin_mode;
 	const char *stdin_mode_arg = NULL;
+	struct format_nul_data nul_data = { 0, 0 };
+	char output_terminator;
+	strbuf_getline_fn getline_fn;
 	struct display_notes_opt format_notes_opt;
 	struct rev_info format_rev = REV_INFO_INIT;
 	struct pretty_format format_pp = { 0 };
@@ -813,6 +834,13 @@ int cmd_format_rev(int argc,
 			   N_("how revs are processed")),
 		OPT_STRING_LIST(0, "notes", &notes, N_("notes"),
 				N_("display notes for pretty format")),
+		OPT_CALLBACK_F('z', "null", &nul_data, N_("z"),
+			       N_("Use NUL for input and output termination"),
+			       PARSE_OPT_NOARG | PARSE_OPT_NONEG, format_nul_cb),
+		OPT_BOOL(0, "null-input", &nul_data.nul_input,
+			 N_("Use NUL for input termination")),
+		OPT_BOOL(0, "null-output", &nul_data.nul_output,
+			 N_("Use NUL for output termination")),
 		OPT_END(),
 	};
 
@@ -828,6 +856,9 @@ int cmd_format_rev(int argc,
 	if (!stdin_mode_arg)
 		die(_("'%s' is required"), "--stdin-mode");
 
+	getline_fn = nul_data.nul_input ? strbuf_getline_nul : strbuf_getline_lf;
+	output_terminator = nul_data.nul_output ? '\0' : '\n';
+
 	init_display_notes(&format_notes_opt);
 	stdin_mode = parse_stdin_mode(stdin_mode_arg);
 
@@ -856,20 +887,24 @@ int cmd_format_rev(int argc,
 
 	switch (stdin_mode) {
 	case TEXT:
-		while (strbuf_getline(&scratch_buf, stdin) != EOF) {
-			strbuf_addch(&scratch_buf, '\n');
+		while (getline_fn(&scratch_buf, stdin) != EOF) {
 			name_rev_line(scratch_buf.buf, &cmd);
+			/*
+			 * We do not pass on the terminator to name_rev_line,
+			 * unlike name-rev.
+			 */
+			printf("%c", output_terminator);
+			maybe_flush_or_die(stdout, "stdout");
 		}
 		break;
 	case REVS:
-		while (strbuf_getline(&scratch_buf, stdin) != EOF) {
+		while (getline_fn(&scratch_buf, stdin) != EOF) {
 			struct object_id oid;
 			struct object *object;
 			struct object *peeled;
-			struct commit *commit;
 
 			if (repo_get_oid(the_repository, scratch_buf.buf, &oid)) {
-				fprintf(stderr, "Could not get sha1 for %s. Skipping.\n",
+				fprintf(stderr, "Could not get object name for %s. Skipping.\n",
 					scratch_buf.buf);
 				continue;
 			}
@@ -882,16 +917,17 @@ int cmd_format_rev(int argc,
 			}
 
 			peeled = deref_tag(the_repository, object, scratch_buf.buf, 0);
-			if (peeled && peeled->type == OBJ_COMMIT)
-				commit = (struct commit *)peeled;
-			if (!commit) {
-				fprintf(stderr, "Could not get commit for %s. Skipping.\n",
-					*argv);
+			if (!peeled || peeled->type != OBJ_COMMIT) {
+				fprintf(stderr,
+					"Could not get commit for %s. Skipping.\n",
+					scratch_buf.buf);
 				continue;
 			}
 
-			get_format_rev(commit, &format_pp, &scratch_buf);
-			printf("%s\n", scratch_buf.buf);
+			get_format_rev((struct commit *)peeled,
+				       &format_pp, &scratch_buf);
+			printf("%s%c", scratch_buf.buf, output_terminator);
+			maybe_flush_or_die(stdout, "stdout");
 			strbuf_release(&scratch_buf);
 		}
 		break;
diff --git a/t/t6120-describe.sh b/t/t6120-describe.sh
index 725f7d81b6b..8ee3d2c37d0 100755
--- a/t/t6120-describe.sh
+++ b/t/t6120-describe.sh
@@ -801,7 +801,7 @@ test_expect_success 'do not be fooled by invalid describe format ' '
 	test_must_fail git cat-file -t "refs/tags/super-invalid/./../...../ ~^:/?*[////\\\\\\&}/busted.lock-42-g"$(cat out)
 '
 
-test_expect_success 'name-rev --format setup' '
+test_expect_success 'setup: format-rev' '
 	mkdir repo-format &&
 	git -C repo-format init &&
 	test_commit -C repo-format first &&
@@ -839,8 +839,8 @@ test_expect_success 'format-rev --stdin-mode=text from rev-list same as log' '
 '
 
 test_expect_success 'format-rev --stdin-mode=text with running text and tree oid' '
-	cmit_oid=$(git -C repo-format rev-parse :/fifth) &&
-	reference=$(git -C repo-format log -n1 --format=reference :/fifth) &&
+	cmit_oid=$(git -C repo-format rev-parse fifth) &&
+	reference=$(git -C repo-format log -n1 --format=reference fifth) &&
 	tree=$(git -C repo-format rev-parse HEAD^{tree}) &&
 	cat >expect <<-EOF &&
 	We thought we fixed this in ${reference}.
@@ -899,10 +899,100 @@ test_expect_success 'format-rev --stdin-mode=revs on annotated tag peels to comm
 	EOF
 	git -C repo-format format-rev --stdin-mode=revs \
 		--format=%s \
-		--notes=* >actual <<-\EOF &&
+		>actual <<-\EOF &&
 	version
 	EOF
 	test_cmp expect actual
 '
 
+test_expect_success 'format-rev --stdin-mode=revs lookup failures' '
+	test_when_finished "git -C repo-format tag -d tag-to-tree" &&
+	invalid_syntax=not-valid &&
+	non_existing_oid=${EMPTY_BLOB} &&
+	tree=$(git -C repo-format rev-parse eighth^{tree}) &&
+	git -C repo-format tag -a -mmessage tag-to-tree "$tree" &&
+	tag_to_tree=$(git -C repo-format rev-parse tag-to-tree) &&
+	cat >expect <<-EOF &&
+	Could not get object name for ${invalid_syntax}. Skipping.
+	Could not get object for ${non_existing_oid}. Skipping.
+	Could not get commit for ${tree}. Skipping.
+	Could not get commit for ${tag_to_tree}. Skipping.
+	EOF
+	git -C repo-format format-rev --stdin-mode=revs \
+		--format=%s \
+		2>actual >out <<-EOF &&
+	${invalid_syntax}
+	${non_existing_oid}
+	${tree}
+	${tag_to_tree}
+	EOF
+	test_line_count = 0 out &&
+	test_cmp expect actual
+'
+
+
+test_expect_success 'format-rev -z --stdin-mode=text with object name lookup failures' '
+	printf "%s\0" "$(git -C repo-format rev-parse HEAD)" >input &&
+	printf "%s\0" "$(git -C repo-format rev-parse HEAD^{tree})" >>input &&
+	printf "%s\0" "$EMPTY_BLOB" >>input &&
+	printf "%s\0" "$(git -C repo-format log --format=%s -1)" >expect &&
+	printf "%s\0" "$(git -C repo-format rev-parse HEAD^{tree})" >>expect &&
+	printf "%s\0" "$EMPTY_BLOB" >>expect &&
+	git -C repo-format format-rev --stdin-mode=text \
+		--format=%s -z <input >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'setup: format-rev input and output separators' '
+	git -C repo-format rev-list HEAD >input-lf &&
+	git -C repo-format rev-list -z HEAD >input-nul &&
+	git -C repo-format log --format=%s >output-lf &&
+	git -C repo-format log -z --format=%s >output-nul &&
+	echo revs >stdin-modes &&
+	echo text >>stdin-modes
+'
+
+while read mode
+do
+	test_expect_success "format-rev -z --stdin-mode=$mode" '
+		cat output-nul >expect &&
+		git -C repo-format format-rev --stdin-mode="$mode" \
+			--format=%s -z <input-nul >actual &&
+		test_cmp expect actual
+	'
+
+	test_expect_success "format-rev -z --no-null-input --no-null-output --stdin-mode=$mode" '
+		cat output-lf >expect &&
+		git -C repo-format format-rev --stdin-mode="$mode" \
+			--format=%s -z --no-null-input --no-null-output \
+			<input-lf >actual &&
+		test_cmp expect actual
+	'
+
+	test_expect_success "format-rev ---null-input --stdin-mode=$mode" '
+		cat output-lf >expect &&
+		git -C repo-format format-rev --stdin-mode="$mode" \
+			--format=%s --null-input \
+			<input-nul >actual &&
+		test_cmp expect actual
+	'
+
+	test_expect_success "format-rev --null-output --stdin-mode=$mode" '
+		cat output-nul >expect &&
+		git -C repo-format format-rev --stdin-mode="$mode" \
+			--format=%s --null-output \
+			<input-lf >actual &&
+		test_cmp expect actual
+	'
+
+	test_expect_success "format-rev -z --stdin-mode=$mode with multi-line output" '
+		format="%s%n%aI" &&
+		git -C repo-format log -z --format="$format" \
+			>expect &&
+		git -C repo-format format-rev --stdin-mode="$mode" \
+			--format="$format" -z <input-nul >actual &&
+		test_cmp expect actual
+	'
+done <stdin-modes
+
 test_done
Range-diff against v3:
1:  9cb5cfd1ec3 = 1:  9cb5cfd1ec3 name-rev: wrap both blocks in braces
2:  14900271321 = 2:  14900271321 name-rev: run clang-format before factoring code
3:  1f17f0b0090 ! 3:  724ec022894 name-rev: factor code for sharing with a new command
    @@ builtin/name-rev.c: static char const * const name_rev_usage[] = {
      	struct strbuf buf = STRBUF_INIT;
      	int counter = 0;
     @@ builtin/name-rev.c: static void name_rev_line(char *p, struct name_ref_data *data)
    - 
    - 	for (p_start = p; *p; p++) {
    - #define ishex(x) (isdigit((x)) || ((x) >= 'a' && (x) <= 'f'))
    -+	start:
    - 		if (!ishex(*p)) {
    - 			counter = 0;
    - 		} else if (++counter == hexsz &&
    -@@ builtin/name-rev.c: static void name_rev_line(char *p, struct name_ref_data *data)
      			const char *name = NULL;
      			char c = *(p + 1);
      			int p_len = p - p_start + 1;
    @@ builtin/name-rev.c: static void name_rev_line(char *p, struct name_ref_data *dat
     -				struct object *o =
     -					lookup_object(the_repository, &oid);
     +			oid_ret = repo_get_oid(the_repository, p - (hexsz - 1), &oid);
    ++			*(p + 1) = c;
     +
     +			switch (cmd->type) {
     +			case NAME_REV:
    @@ builtin/name-rev.c: static void name_rev_line(char *p, struct name_ref_data *dat
     +					o = lookup_object(the_repository, &oid);
      				if (o)
      					name = get_rev_name(o, &buf);
    -+				*(p + 1) = c;
     +				if (!name)
    -+					goto start;
    ++					continue;
     +				if (cmd->u.name_only)
     +					printf("%.*s%s", p_len - hexsz, p_start, name);
     +				else
4:  40989b3672a = 4:  382efc3ddb8 name-rev: make dedicated --annotate-stdin --name-only test
5:  bb54e8f753e ! 5:  049a45e32bc format-rev: introduce builtin for on-demand pretty formatting
    @@ Commit message
         `t/t1517-outside-repo.sh` because it uses “EXPERIMENTAL!”
         in the usage line.
     
    +    Helped-by: Phillip Wood <phillip.wood@dunelm.org.uk>
    +    Helped-by: Ramsay Jones <ramsay@ramsayjones.plus.com>
    +    Helped-by: Junio C Hamano <gitster@pobox.com>
         Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
     
    + ## .gitignore ##
    +@@
    + /git-for-each-ref
    + /git-for-each-repo
    + /git-format-patch
    ++/git-format-rev
    + /git-fsck
    + /git-fsck-objects
    + /git-fsmonitor--daemon
    +
      ## Documentation/git-format-rev.adoc (new) ##
     @@
     +git-format-rev(1)
    @@ Documentation/git-format-rev.adoc (new)
     +SYNOPSIS
     +--------
     +[synopsis]
    -+(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> --format=<pretty> [--notes=<ref>]
    ++(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> --format=<pretty> [--[no-]notes=<ref>] [-z] [--[no-]null-output] [--[no-]null-input]
     +
     +DESCRIPTION
     +-----------
    @@ Documentation/git-format-rev.adoc (new)
     +	How to interpret standard input data:
     ++
     +--
    -+`revs`:: Each line is interpreted as a commit. Any kind of revision
    ++`revs`:: Each line or record (see the <<io,INPUT AND OUTPUT FORMATS>>
    ++	section) is interpreted as a commit. Any kind of revision
     +	expression can be used (see linkgit:gitrevisions[7]). Annotated
     +	tags are peeled (see linkgit:gitglossary[7]).
     ++
    @@ Documentation/git-format-rev.adoc (new)
     +`text`:: Formats all commit object names found in freeform text. These
     +	must the full object names, i.e. abbreviated hexidecimal object
     +	names will not be interpreted.
    +++
    ++Anything that is parsed as an object name but that is not found to be a
    ++commit object name is left alone (echoed).
     +--
     +
     +`--format=<pretty>`::
    @@ Documentation/git-format-rev.adoc (new)
     +	Custom notes ref. Notes are displayed when using the `%N`
     +	atom. See linkgit:git-notes[1].
     +
    ++`-z`::
    ++`--null`::
    ++	Use _NUL_ character to terminate both input and output instead
    ++	of newline. This option cannot be negated.
    +++
    ++This is useful if both the input and output could contain newlines or if
    ++the input could contain _NUL_ characters; see the <<io,INPUT AND OUTPUT
    ++FORMATS>> section.
    ++
    ++`--null-output`::
    ++`--no-null-output`::
    ++	Use _NUL_ character to terminate output instead of newline. The
    ++	default is `--no-null-output`.
    +++
    ++This is useful if the output could contain newlines, for example if the
    ++`%n` (newline) atom is used.
    ++
    ++`--null-input`::
    ++`--no-null-input`::
    ++	Use _NUL_ character to terminate input instead of newline. The
    ++	default is `--no-null-input`.
    +++
    ++This is useful if the input revision expressions could contain newlines.
    ++It is also useful if the input could contain _NUL_ characters; see the
    ++<<io,INPUT AND OUTPUT FORMATS>> section.
    ++
    ++[[io]]
    ++INPUT AND OUTPUT FORMAT
    ++-----------------------
    ++
    ++The command uses newlines for both input and output termination by
    ++default. See the `-z`, `--null-output`, and `--null-input` options for
    ++using _NUL_ character as the terminator.
    ++
    ++The mode `--stdin-mode=revs` outputs one formatted commit followed by
    ++the terminator. This could either be called a _line_ or a _record_ in
    ++case "line" is too suggestive of newline termination.
    ++
    ++Note that this means that the terminator character (newline or _NUL_)
    ++acts as a _terminator_, not a _separator_. In other words, the final
    ++line or record is also terminated by the terminator character.
    ++
    ++The mode `--stdin-mode=text` replaces each object name with the
    ++formatted commit, i.e. the format `%s` would transform the object name
    ++`abcdef012...` to `<subject>` without any termination. Like this:
    ++
    ++----
    ++Did we not fix this in "<subject>"?
    ++----
    ++
    ++Regarding input in this mode: using `-z` or `--null-input` makes sure
    ++that _NUL_ characters in the input are passed through correctly.
    ++
    ++It is safe to interactively read and write from this command since each
    ++record is immediately flushed.
    ++
    ++[[examples]]
     +EXAMPLES
     +--------
     +
    @@ Documentation/git-format-rev.adoc (new)
     +
     +This command lets you format any number of revisions in any order
     +through one command invocation. Consider the
    -+linkgit:git-last-modified[1] case from the "EXAMPLES" section above:
    ++linkgit:git-last-modified[1] case from the <<examples,EXAMPLES>> section
    ++above:
     +
     +1. There might be hundreds of files
     +2. Commits can be repeated, i.e. two or more files were last modified in
    @@ Documentation/git-format-rev.adoc (new)
     +- The output of linkgit:git-last-modified[1] would have to be processed
     +  in stages since you need to transform the first column separately and
     +  then link the author to the filename. But this is surmountable.
    -+- You can feed each commit to `git show` or `git show --no-walk -1`. But
    ++- You can feed each commit to `git show` or `git log --no-walk -1`. But
     +  that means that you need to create a process for each line.
     +- Let’s say that you want to use one process, not one per line. So you
     +  want to feed all the commits to the command. Now you face the problem
    @@ Documentation/git-format-rev.adoc (new)
     +process, but still doable. In contrast, this problem is just another
     +shell pipeline with this command.
     +
    ++SEE ALSO
    ++--------
    ++linkgit:git-name-rev[1],
    ++linkgit:git-log[1].
    ++
     +GIT
     +---
     +Part of the linkgit:git[1] suite
     
    + ## Documentation/meson.build ##
    +@@ Documentation/meson.build: manpages = {
    +   'git-for-each-ref.adoc' : 1,
    +   'git-for-each-repo.adoc' : 1,
    +   'git-format-patch.adoc' : 1,
    ++  'git-format-rev.adoc' : 1,
    +   'git-fsck-objects.adoc' : 1,
    +   'git-fsck.adoc' : 1,
    +   'git-fsmonitor--daemon.adoc' : 1,
    +
      ## Makefile ##
     @@ Makefile: BUILT_INS += $(patsubst builtin/%.o,git-%$X,$(BUILTIN_OBJS))
      BUILT_INS += git-cherry$X
    @@ builtin/name-rev.c
     +#include "pretty.h"
     +#include "revision.h"
     +#include "notes.h"
    ++#include "write-or-die.h"
      
      /*
       * One day.  See the 'name a rev shortly after epoch' test in t6120 when
    @@ builtin/name-rev.c: static void name_rev_line(char *p, struct command *cmd)
     +					name = get_format_rev((const struct commit *)o,
     +							      cmd->u.pretty_format,
     +							      &buf);
    -+				*(p + 1) = c;
     +				if (name)
     +					printf("%.*s%s", p_len - hexsz, p_start, name);
     +				else
    @@ builtin/name-rev.c: int cmd_name_rev(int argc,
      	return 0;
      }
     +
    ++struct format_nul_data {
    ++	bool nul_input;
    ++	bool nul_output;
    ++};
    ++
    ++static int format_nul_cb(const struct option *option,
    ++			 const char *arg,
    ++			 int unset)
    ++{
    ++	struct format_nul_data *data = option->value;
    ++	data->nul_input = 1;
    ++	data->nul_output = 1;
    ++	BUG_ON_OPT_NEG(unset);
    ++	BUG_ON_OPT_ARG(arg);
    ++	return 0;
    ++}
    ++
     +static enum stdin_mode parse_stdin_mode(const char *stdin_mode)
     +{
     +	if (!strcmp(stdin_mode, "text"))
    @@ builtin/name-rev.c: int cmd_name_rev(int argc,
     +}
     +
     +static char const *const format_rev_usage[] = {
    -+	N_("(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> --format=<pretty> [--notes=<ref>]"),
    ++	N_("(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> "
    ++	   "--format=<pretty> [--[no-]notes=<ref>] "
    ++	   "[-z] [--[no-]null-output] [--[no-]null-input]"),
     +	NULL
     +};
     +
    @@ builtin/name-rev.c: int cmd_name_rev(int argc,
     +	const char *format = NULL;
     +	enum stdin_mode stdin_mode;
     +	const char *stdin_mode_arg = NULL;
    ++	struct format_nul_data nul_data = { 0, 0 };
    ++	char output_terminator;
    ++	strbuf_getline_fn getline_fn;
     +	struct display_notes_opt format_notes_opt;
     +	struct rev_info format_rev = REV_INFO_INIT;
     +	struct pretty_format format_pp = { 0 };
    @@ builtin/name-rev.c: int cmd_name_rev(int argc,
     +			   N_("how revs are processed")),
     +		OPT_STRING_LIST(0, "notes", &notes, N_("notes"),
     +				N_("display notes for pretty format")),
    ++		OPT_CALLBACK_F('z', "null", &nul_data, N_("z"),
    ++			       N_("Use NUL for input and output termination"),
    ++			       PARSE_OPT_NOARG | PARSE_OPT_NONEG, format_nul_cb),
    ++		OPT_BOOL(0, "null-input", &nul_data.nul_input,
    ++			 N_("Use NUL for input termination")),
    ++		OPT_BOOL(0, "null-output", &nul_data.nul_output,
    ++			 N_("Use NUL for output termination")),
     +		OPT_END(),
     +	};
     +
    @@ builtin/name-rev.c: int cmd_name_rev(int argc,
     +	if (!stdin_mode_arg)
     +		die(_("'%s' is required"), "--stdin-mode");
     +
    ++	getline_fn = nul_data.nul_input ? strbuf_getline_nul : strbuf_getline_lf;
    ++	output_terminator = nul_data.nul_output ? '\0' : '\n';
    ++
     +	init_display_notes(&format_notes_opt);
     +	stdin_mode = parse_stdin_mode(stdin_mode_arg);
     +
    @@ builtin/name-rev.c: int cmd_name_rev(int argc,
     +
     +	switch (stdin_mode) {
     +	case TEXT:
    -+		while (strbuf_getline(&scratch_buf, stdin) != EOF) {
    -+			strbuf_addch(&scratch_buf, '\n');
    ++		while (getline_fn(&scratch_buf, stdin) != EOF) {
     +			name_rev_line(scratch_buf.buf, &cmd);
    ++			/*
    ++			 * We do not pass on the terminator to name_rev_line,
    ++			 * unlike name-rev.
    ++			 */
    ++			printf("%c", output_terminator);
    ++			maybe_flush_or_die(stdout, "stdout");
     +		}
     +		break;
     +	case REVS:
    -+		while (strbuf_getline(&scratch_buf, stdin) != EOF) {
    ++		while (getline_fn(&scratch_buf, stdin) != EOF) {
     +			struct object_id oid;
     +			struct object *object;
     +			struct object *peeled;
    -+			struct commit *commit;
     +
     +			if (repo_get_oid(the_repository, scratch_buf.buf, &oid)) {
    -+				fprintf(stderr, "Could not get sha1 for %s. Skipping.\n",
    ++				fprintf(stderr, "Could not get object name for %s. Skipping.\n",
     +					scratch_buf.buf);
     +				continue;
     +			}
    @@ builtin/name-rev.c: int cmd_name_rev(int argc,
     +			}
     +
     +			peeled = deref_tag(the_repository, object, scratch_buf.buf, 0);
    -+			if (peeled && peeled->type == OBJ_COMMIT)
    -+				commit = (struct commit *)peeled;
    -+			if (!commit) {
    -+				fprintf(stderr, "Could not get commit for %s. Skipping.\n",
    -+					*argv);
    ++			if (!peeled || peeled->type != OBJ_COMMIT) {
    ++				fprintf(stderr,
    ++					"Could not get commit for %s. Skipping.\n",
    ++					scratch_buf.buf);
     +				continue;
     +			}
     +
    -+			get_format_rev(commit, &format_pp, &scratch_buf);
    -+			printf("%s\n", scratch_buf.buf);
    ++			get_format_rev((struct commit *)peeled,
    ++				       &format_pp, &scratch_buf);
    ++			printf("%s%c", scratch_buf.buf, output_terminator);
    ++			maybe_flush_or_die(stdout, "stdout");
     +			strbuf_release(&scratch_buf);
     +		}
     +		break;
    @@ t/t6120-describe.sh: test_expect_success 'do not be fooled by invalid describe f
      	test_must_fail git cat-file -t "refs/tags/super-invalid/./../...../ ~^:/?*[////\\\\\\&}/busted.lock-42-g"$(cat out)
      '
      
    -+test_expect_success 'name-rev --format setup' '
    ++test_expect_success 'setup: format-rev' '
     +	mkdir repo-format &&
     +	git -C repo-format init &&
     +	test_commit -C repo-format first &&
    @@ t/t6120-describe.sh: test_expect_success 'do not be fooled by invalid describe f
     +'
     +
     +test_expect_success 'format-rev --stdin-mode=text with running text and tree oid' '
    -+	cmit_oid=$(git -C repo-format rev-parse :/fifth) &&
    -+	reference=$(git -C repo-format log -n1 --format=reference :/fifth) &&
    ++	cmit_oid=$(git -C repo-format rev-parse fifth) &&
    ++	reference=$(git -C repo-format log -n1 --format=reference fifth) &&
     +	tree=$(git -C repo-format rev-parse HEAD^{tree}) &&
     +	cat >expect <<-EOF &&
     +	We thought we fixed this in ${reference}.
    @@ t/t6120-describe.sh: test_expect_success 'do not be fooled by invalid describe f
     +	EOF
     +	git -C repo-format format-rev --stdin-mode=revs \
     +		--format=%s \
    -+		--notes=* >actual <<-\EOF &&
    ++		>actual <<-\EOF &&
     +	version
     +	EOF
     +	test_cmp expect actual
     +'
    ++
    ++test_expect_success 'format-rev --stdin-mode=revs lookup failures' '
    ++	test_when_finished "git -C repo-format tag -d tag-to-tree" &&
    ++	invalid_syntax=not-valid &&
    ++	non_existing_oid=${EMPTY_BLOB} &&
    ++	tree=$(git -C repo-format rev-parse eighth^{tree}) &&
    ++	git -C repo-format tag -a -mmessage tag-to-tree "$tree" &&
    ++	tag_to_tree=$(git -C repo-format rev-parse tag-to-tree) &&
    ++	cat >expect <<-EOF &&
    ++	Could not get object name for ${invalid_syntax}. Skipping.
    ++	Could not get object for ${non_existing_oid}. Skipping.
    ++	Could not get commit for ${tree}. Skipping.
    ++	Could not get commit for ${tag_to_tree}. Skipping.
    ++	EOF
    ++	git -C repo-format format-rev --stdin-mode=revs \
    ++		--format=%s \
    ++		2>actual >out <<-EOF &&
    ++	${invalid_syntax}
    ++	${non_existing_oid}
    ++	${tree}
    ++	${tag_to_tree}
    ++	EOF
    ++	test_line_count = 0 out &&
    ++	test_cmp expect actual
    ++'
    ++
    ++
    ++test_expect_success 'format-rev -z --stdin-mode=text with object name lookup failures' '
    ++	printf "%s\0" "$(git -C repo-format rev-parse HEAD)" >input &&
    ++	printf "%s\0" "$(git -C repo-format rev-parse HEAD^{tree})" >>input &&
    ++	printf "%s\0" "$EMPTY_BLOB" >>input &&
    ++	printf "%s\0" "$(git -C repo-format log --format=%s -1)" >expect &&
    ++	printf "%s\0" "$(git -C repo-format rev-parse HEAD^{tree})" >>expect &&
    ++	printf "%s\0" "$EMPTY_BLOB" >>expect &&
    ++	git -C repo-format format-rev --stdin-mode=text \
    ++		--format=%s -z <input >actual &&
    ++	test_cmp expect actual
    ++'
    ++
    ++test_expect_success 'setup: format-rev input and output separators' '
    ++	git -C repo-format rev-list HEAD >input-lf &&
    ++	git -C repo-format rev-list -z HEAD >input-nul &&
    ++	git -C repo-format log --format=%s >output-lf &&
    ++	git -C repo-format log -z --format=%s >output-nul &&
    ++	echo revs >stdin-modes &&
    ++	echo text >>stdin-modes
    ++'
    ++
    ++while read mode
    ++do
    ++	test_expect_success "format-rev -z --stdin-mode=$mode" '
    ++		cat output-nul >expect &&
    ++		git -C repo-format format-rev --stdin-mode="$mode" \
    ++			--format=%s -z <input-nul >actual &&
    ++		test_cmp expect actual
    ++	'
    ++
    ++	test_expect_success "format-rev -z --no-null-input --no-null-output --stdin-mode=$mode" '
    ++		cat output-lf >expect &&
    ++		git -C repo-format format-rev --stdin-mode="$mode" \
    ++			--format=%s -z --no-null-input --no-null-output \
    ++			<input-lf >actual &&
    ++		test_cmp expect actual
    ++	'
    ++
    ++	test_expect_success "format-rev ---null-input --stdin-mode=$mode" '
    ++		cat output-lf >expect &&
    ++		git -C repo-format format-rev --stdin-mode="$mode" \
    ++			--format=%s --null-input \
    ++			<input-nul >actual &&
    ++		test_cmp expect actual
    ++	'
    ++
    ++	test_expect_success "format-rev --null-output --stdin-mode=$mode" '
    ++		cat output-nul >expect &&
    ++		git -C repo-format format-rev --stdin-mode="$mode" \
    ++			--format=%s --null-output \
    ++			<input-lf >actual &&
    ++		test_cmp expect actual
    ++	'
    ++
    ++	test_expect_success "format-rev -z --stdin-mode=$mode with multi-line output" '
    ++		format="%s%n%aI" &&
    ++		git -C repo-format log -z --format="$format" \
    ++			>expect &&
    ++		git -C repo-format format-rev --stdin-mode="$mode" \
    ++			--format="$format" -z <input-nul >actual &&
    ++		test_cmp expect actual
    ++	'
    ++done <stdin-modes
     +
      test_done

base-commit: 67006b9db8b772423ad0706029286096307d2567
-- 
2.54.0.13.g9c7419e39f8
kristofferhaugsbakk@fastmail.comMay 7, 2026, 19:34 UTC in reply to kristofferhaugsbakk@fastmail.com on lore

[PATCH v4 1/5] name-rev: wrap both blocks in braces

From: Kristoffer Haugsbakk <code@khaugsbakk.name>
See `CodingGuidelines`:
    - When there are multiple arms to a conditional and some of them
      require braces, enclose even a single line block in braces for
      consistency. [...]
Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
---
Notes (series):
    v2:
    Fix stray formatting of `(p+1)`
 builtin/name-rev.c | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)
Show changes to builtin/name-rev.c +4 −4
diff --git a/builtin/name-rev.c b/builtin/name-rev.c
index 6188cf98ce0..171e7bd0e98 100644
--- a/builtin/name-rev.c
+++ b/builtin/name-rev.c
@@ -466,9 +466,9 @@ static const char *get_rev_name(const struct object *o, struct strbuf *buf)
 	if (!n)
 		return NULL;
 
-	if (!n->generation)
+	if (!n->generation) {
 		return n->tip_name;
-	else {
+	} else {
 		strbuf_reset(buf);
 		strbuf_addstr(buf, n->tip_name);
 		strbuf_strip_suffix(buf, "^0");
@@ -516,9 +516,9 @@ static void name_rev_line(char *p, struct name_ref_data *data)
 
 	for (p_start = p; *p; p++) {
 #define ishex(x) (isdigit((x)) || ((x) >= 'a' && (x) <= 'f'))
-		if (!ishex(*p))
+		if (!ishex(*p)) {
 			counter = 0;
-		else if (++counter == hexsz &&
+		} else if (++counter == hexsz &&
 			 !ishex(*(p+1))) {
 			struct object_id oid;
 			const char *name = NULL;
-- 
2.54.0.13.g9c7419e39f8
kristofferhaugsbakk@fastmail.comMay 7, 2026, 19:34 UTC in reply to kristofferhaugsbakk@fastmail.com on lore

[PATCH v4 2/5] name-rev: run clang-format before factoring code

From: Kristoffer Haugsbakk <code@khaugsbakk.name>

We are about to move code around to prepare for adding a new command. Let’s deal with clang-format changes first in the affected areas.

Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
---
 builtin/name-rev.c | 18 +++++++++---------
 1 file changed, 9 insertions(+), 9 deletions(-)
Show changes to builtin/name-rev.c +9 −9
diff --git a/builtin/name-rev.c b/builtin/name-rev.c
index 171e7bd0e98..6357eaa76d0 100644
--- a/builtin/name-rev.c
+++ b/builtin/name-rev.c
@@ -519,22 +519,22 @@ static void name_rev_line(char *p, struct name_ref_data *data)
 		if (!ishex(*p)) {
 			counter = 0;
 		} else if (++counter == hexsz &&
-			 !ishex(*(p+1))) {
+			   !ishex(*(p + 1))) {
 			struct object_id oid;
 			const char *name = NULL;
-			char c = *(p+1);
+			char c = *(p + 1);
 			int p_len = p - p_start + 1;
 
 			counter = 0;
 
-			*(p+1) = 0;
+			*(p + 1) = 0;
 			if (!repo_get_oid(the_repository, p - (hexsz - 1), &oid)) {
 				struct object *o =
 					lookup_object(the_repository, &oid);
 				if (o)
 					name = get_rev_name(o, &buf);
 			}
-			*(p+1) = c;
+			*(p + 1) = c;
 
 			if (!name)
 				continue;
@@ -571,9 +571,9 @@ int cmd_name_rev(int argc,
 		OPT_BOOL(0, "name-only", &data.name_only, N_("print only ref-based names (no object names)")),
 		OPT_BOOL(0, "tags", &data.tags_only, N_("only use tags to name the commits")),
 		OPT_STRING_LIST(0, "refs", &data.ref_filters, N_("pattern"),
-				   N_("only use refs matching <pattern>")),
+				N_("only use refs matching <pattern>")),
 		OPT_STRING_LIST(0, "exclude", &data.exclude_filters, N_("pattern"),
-				   N_("ignore refs matching <pattern>")),
+				N_("ignore refs matching <pattern>")),
 		OPT_GROUP(""),
 		OPT_BOOL(0, "all", &all, N_("list all commits reachable from all refs")),
 #ifndef WITH_BREAKING_CHANGES
@@ -585,10 +585,10 @@ int cmd_name_rev(int argc,
 #endif /* WITH_BREAKING_CHANGES */
 		OPT_BOOL(0, "annotate-stdin", &annotate_stdin, N_("annotate text from stdin")),
 		OPT_BOOL(0, "undefined", &allow_undefined, N_("allow to print `undefined` names (default)")),
-		OPT_BOOL(0, "always",     &always,
-			   N_("show abbreviated commit object as fallback")),
+		OPT_BOOL(0, "always", &always,
+			 N_("show abbreviated commit object as fallback")),
 		OPT_HIDDEN_BOOL(0, "peel-tag", &peel_tag,
-			   N_("dereference tags in the input (internal use)")),
+				N_("dereference tags in the input (internal use)")),
 		OPT_END(),
 	};
 
-- 
2.54.0.13.g9c7419e39f8
kristofferhaugsbakk@fastmail.comMay 7, 2026, 19:34 UTC in reply to kristofferhaugsbakk@fastmail.com on lore

[PATCH v4 3/5] name-rev: factor code for sharing with a new command

From: Kristoffer Haugsbakk <code@khaugsbakk.name>

We are about to introduce a new command git-format-rev(1) to this file. Let’s factor some code so that we can share it with the new command.

We want to be able to format commits found in freeform text, and git-name-rev(1) already has a function for that but for symbolic names. Let’s use a tagged union for the command-specific payload.

No functional changes.
Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
---
Notes (series):
    v4:
    • Pull out `*(p + 1)` instead of doing it in every `case` (Phillip)
    • Go back to using `continue` instead of `goto` (Phillip)
 builtin/name-rev.c | 53 ++++++++++++++++++++++++++++++++++------------
 1 file changed, 40 insertions(+), 13 deletions(-)
Show changes to builtin/name-rev.c +40 −13
diff --git a/builtin/name-rev.c b/builtin/name-rev.c
index 6357eaa76d0..475efb0b82b 100644
--- a/builtin/name-rev.c
+++ b/builtin/name-rev.c
@@ -272,6 +272,24 @@ struct name_ref_data {
 	struct string_list exclude_filters;
 };
 
+enum command_type {
+	NAME_REV = 1,
+};
+
+struct command {
+	enum command_type type;
+	union {
+		int name_only;
+	} u;
+};
+
+static void init_name_rev_command(struct command *cmd,
+				  int name_only)
+{
+	cmd->type = NAME_REV;
+	cmd->u.name_only = name_only;
+}
+
 static struct tip_table {
 	struct tip_table_entry {
 		struct object_id oid;
@@ -507,7 +525,7 @@ static char const * const name_rev_usage[] = {
 	NULL
 };
 
-static void name_rev_line(char *p, struct name_ref_data *data)
+static void name_rev_line(char *p, struct command *cmd)
 {
 	struct strbuf buf = STRBUF_INIT;
 	int counter = 0;
@@ -524,25 +542,32 @@ static void name_rev_line(char *p, struct name_ref_data *data)
 			const char *name = NULL;
 			char c = *(p + 1);
 			int p_len = p - p_start + 1;
+			struct object *o = NULL;
+			int oid_ret = 1;
 
 			counter = 0;
 
 			*(p + 1) = 0;
-			if (!repo_get_oid(the_repository, p - (hexsz - 1), &oid)) {
-				struct object *o =
-					lookup_object(the_repository, &oid);
+			oid_ret = repo_get_oid(the_repository, p - (hexsz - 1), &oid);
+			*(p + 1) = c;
+
+			switch (cmd->type) {
+			case NAME_REV:
+				if (!oid_ret)
+					o = lookup_object(the_repository, &oid);
 				if (o)
 					name = get_rev_name(o, &buf);
+				if (!name)
+					continue;
+				if (cmd->u.name_only)
+					printf("%.*s%s", p_len - hexsz, p_start, name);
+				else
+					printf("%.*s (%s)", p_len, p_start, name);
+				break;
+			default:
+				BUG("uncovered case: %d", cmd->type);
 			}
-			*(p + 1) = c;
-
-			if (!name)
-				continue;
 
-			if (data->name_only)
-				printf("%.*s%s", p_len - hexsz, p_start, name);
-			else
-				printf("%.*s (%s)", p_len, p_start, name);
 			p_start = p + 1;
 		}
 	}
@@ -567,6 +592,7 @@ int cmd_name_rev(int argc,
 #endif
 	int all = 0, annotate_stdin = 0, allow_undefined = 1, always = 0, peel_tag = 0;
 	struct name_ref_data data = { 0, 0, STRING_LIST_INIT_NODUP, STRING_LIST_INIT_NODUP };
+	struct command cmd;
 	struct option opts[] = {
 		OPT_BOOL(0, "name-only", &data.name_only, N_("print only ref-based names (no object names)")),
 		OPT_BOOL(0, "tags", &data.tags_only, N_("only use tags to name the commits")),
@@ -596,6 +622,7 @@ int cmd_name_rev(int argc,
 	init_commit_rev_name(&rev_names);
 	repo_config(the_repository, git_default_config, NULL);
 	argc = parse_options(argc, argv, prefix, opts, name_rev_usage, 0);
+	init_name_rev_command(&cmd, data.name_only);
 
 #ifndef WITH_BREAKING_CHANGES
 	if (transform_stdin) {
@@ -663,7 +690,7 @@ int cmd_name_rev(int argc,
 
 		while (strbuf_getline(&sb, stdin) != EOF) {
 			strbuf_addch(&sb, '\n');
-			name_rev_line(sb.buf, &data);
+			name_rev_line(sb.buf, &cmd);
 		}
 		strbuf_release(&sb);
 	} else if (all) {
-- 
2.54.0.13.g9c7419e39f8
kristofferhaugsbakk@fastmail.comMay 7, 2026, 19:34 UTC in reply to kristofferhaugsbakk@fastmail.com on lore

[PATCH v4 4/5] name-rev: make dedicated --annotate-stdin --name-only test

From: Kristoffer Haugsbakk <code@khaugsbakk.name>
The previous commit split the `--name-only` handling:
1. `--annotate-stdin`: uses the new `struct command`
2. The rest: uses `struct name_ref_data`

But there is no dedicated test for the option combination in (1). That means that the following tests will fail if you neglect to set `command.u.name_only` properly:

    name-rev --annotate-stdin works with commitGraph
    name-rev --annotate-stdin works with non-monotonic timestamps

even though it has nothing to do with what these tests are supposed to test.

Let’s add another regression test now that it is relevant.
Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
---
 t/t6120-describe.sh | 14 ++++++++++++++
 1 file changed, 14 insertions(+)
Show changes to t/t6120-describe.sh +14 −0
diff --git a/t/t6120-describe.sh b/t/t6120-describe.sh
index 2c70cc561ad..62789f76381 100755
--- a/t/t6120-describe.sh
+++ b/t/t6120-describe.sh
@@ -298,6 +298,20 @@ test_expect_success 'name-rev --annotate-stdin' '
 	test_cmp expect actual
 '
 
+test_expect_success 'name-rev --annotate-stdin --name-only' '
+	>expect.unsorted &&
+	for rev in $(git rev-list --all)
+	do
+		name=$(git name-rev --name-only $rev) &&
+		echo "$name" >>expect.unsorted || return 1
+	done &&
+	sort <expect.unsorted >expect &&
+	git name-rev --annotate-stdin --name-only \
+		<list >actual.unsorted &&
+	sort <actual.unsorted >actual &&
+	test_cmp expect actual
+'
+
 test_expect_success 'name-rev --stdin deprecated' '
 	git rev-list --all >list &&
 	if ! test_have_prereq WITH_BREAKING_CHANGES
-- 
2.54.0.13.g9c7419e39f8
kristofferhaugsbakk@fastmail.comMay 7, 2026, 19:34 UTC in reply to kristofferhaugsbakk@fastmail.com on lore

[PATCH v4 5/5] format-rev: introduce builtin for on-demand pretty formatting

From: Kristoffer Haugsbakk <code@khaugsbakk.name>

Introduce a new builtin for pretty formatting one revision expression per line or commit object names found in running text.

Sometimes you want to format commits. Most of the time you’re walking the graph, e.g. getting a range of commits like `master..topic`. That’s a job for git-log(1).

But there are times when you want to format commits that you encounter on demand:

• Full hashes in running text that you might want to pretty-print
• git-last-modified(1) outputs full hashes that you can do the same
  with
• git-cherry(1) has `-v` for commit subject, but maybe you want
  something else?
But now you can’t use git-log(1), git-show(1), or git-rev-list(1):
• You can’t feed commits piecemeal to these commands, one input
  for one output; they block until standard in is closed
• You can’t feed a list of possibly duplicate commits, like the output
  of git-last-modified(1); they effectively deduplicate the output

Beyond these two points there’s also the input massage problem: you cannot feed mixed input (revisions mixed with arbitrary text).

One might hope that git-cat-file(1) can save us. But it doesn’t support pretty formats.

But there is one command that already both handles revisions as arguments, revisions on standard input, and even revisions mixed in with arbitrary text. Namely git-name-rev(1): the command for outputting symbolic names for commits.

We made some room in `builtin/name-rev.c` two commits ago. Let’s now add this new git-format-rev(1) command. Taking inspiration from git-name-rev(1), there are two modes:

• revs: like git-name-rev(1) in argv mode, but one revision per line
  on standard in
• text: like git-name-rev(1) with `--annotate-stdin`
***

We need to add this command to the exception list in `t/t1517-outside-repo.sh` because it uses “EXPERIMENTAL!” in the usage line.

Helped-by: Phillip Wood <phillip.wood@dunelm.org.uk>
Helped-by: Ramsay Jones <ramsay@ramsayjones.plus.com>
Helped-by: Junio C Hamano <gitster@pobox.com>
Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
---
Notes (series):
    v4:
    • Squash in “SQUASH???” commits from Junio’s branch:
      • Fix -Werror=maybe-uninitialized at builtin/name-rev.c:893:25
      • Add this new builtin to Meson build file as well
    • Flush every output line/record with `maybe_flush_or_die(...)` (Phillip)
    • Introduce `-z` and other NUL output options (Phillip)
    • Tests:
      • More tests: “name lookup failures” (This would have caught my
        uninitialized mistake from v3)
      • More tests: for NUL/LF termination
      • Fix stale setup command which uses “name-rev” in the name
      • Use `setup:` prefix for both (now two) setup tests
      • Drop some `:/fifth` expressions in favor just `fifth` (the tag from
      `test_commit`)
    • Flesh out documentation
    • --stdin-mode=revs: Replace “Could not get [sha1][object name]” since the
      user could be using SHA256
      • This error string was originally stolen from name-rev
    • Remember to add this command to `.gitignore` this time
    • Correct doc to `git [show][log] --no-walk -1` (next to `git show`)
    • Plus some minor things!
    v3:
    • And don’t forget to document --notes this time
    
          https://lore.kernel.org/git/CALnO6CB5WOTp_e7Kv3CrEbQ+3XE-gDxNVHf7qATBEbyKWfxpLg@mail.gmail.com/
 .gitignore                        |   1 +
 Documentation/git-format-rev.adoc | 215 ++++++++++++++++++++++++++++
 Documentation/meson.build         |   1 +
 Makefile                          |   1 +
 builtin.h                         |   1 +
 builtin/name-rev.c                | 223 ++++++++++++++++++++++++++++++
 command-list.txt                  |   1 +
 git.c                             |   1 +
 t/t1517-outside-repo.sh           |   3 +-
 t/t6120-describe.sh               | 194 ++++++++++++++++++++++++++
 10 files changed, 640 insertions(+), 1 deletion(-)
 create mode 100644 Documentation/git-format-rev.adoc
Show changes to 10 files +640 −1

.gitignore, Documentation/git-format-rev.adoc, Documentation/meson.build, Makefile, builtin.h, builtin/name-rev.c, command-list.txt, git.c, t/t1517-outside-repo.sh, t/t6120-describe.sh

diff --git a/.gitignore b/.gitignore
index 24635cf2d6f..e406d3741cd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -71,6 +71,7 @@
 /git-for-each-ref
 /git-for-each-repo
 /git-format-patch
+/git-format-rev
 /git-fsck
 /git-fsck-objects
 /git-fsmonitor--daemon
diff --git a/Documentation/git-format-rev.adoc b/Documentation/git-format-rev.adoc
new file mode 100644
index 00000000000..436980012bc
--- /dev/null
+++ b/Documentation/git-format-rev.adoc
@@ -0,0 +1,215 @@
+git-format-rev(1)
+=================
+
+NAME
+----
+git-format-rev - EXPERIMENTAL: Pretty format revisions on demand
+
+
+SYNOPSIS
+--------
+[synopsis]
+(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> --format=<pretty> [--[no-]notes=<ref>] [-z] [--[no-]null-output] [--[no-]null-input]
+
+DESCRIPTION
+-----------
+
+Pretty format revisions from standard input.
+
+THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
+
+OPTIONS
+-------
+
+`--stdin-mode=<mode>`::
+	How to interpret standard input data:
++
+--
+`revs`:: Each line or record (see the <<io,INPUT AND OUTPUT FORMATS>>
+	section) is interpreted as a commit. Any kind of revision
+	expression can be used (see linkgit:gitrevisions[7]). Annotated
+	tags are peeled (see linkgit:gitglossary[7]).
++
+The argument `rev` is also accepted.
+`text`:: Formats all commit object names found in freeform text. These
+	must the full object names, i.e. abbreviated hexidecimal object
+	names will not be interpreted.
++
+Anything that is parsed as an object name but that is not found to be a
+commit object name is left alone (echoed).
+--
+
+`--format=<pretty>`::
+	Pretty format string.
+
+`--notes=<ref>`::
+`--no-notes`::
+	Custom notes ref. Notes are displayed when using the `%N`
+	atom. See linkgit:git-notes[1].
+
+`-z`::
+`--null`::
+	Use _NUL_ character to terminate both input and output instead
+	of newline. This option cannot be negated.
++
+This is useful if both the input and output could contain newlines or if
+the input could contain _NUL_ characters; see the <<io,INPUT AND OUTPUT
+FORMATS>> section.
+
+`--null-output`::
+`--no-null-output`::
+	Use _NUL_ character to terminate output instead of newline. The
+	default is `--no-null-output`.
++
+This is useful if the output could contain newlines, for example if the
+`%n` (newline) atom is used.
+
+`--null-input`::
+`--no-null-input`::
+	Use _NUL_ character to terminate input instead of newline. The
+	default is `--no-null-input`.
++
+This is useful if the input revision expressions could contain newlines.
+It is also useful if the input could contain _NUL_ characters; see the
+<<io,INPUT AND OUTPUT FORMATS>> section.
+
+[[io]]
+INPUT AND OUTPUT FORMAT
+-----------------------
+
+The command uses newlines for both input and output termination by
+default. See the `-z`, `--null-output`, and `--null-input` options for
+using _NUL_ character as the terminator.
+
+The mode `--stdin-mode=revs` outputs one formatted commit followed by
+the terminator. This could either be called a _line_ or a _record_ in
+case "line" is too suggestive of newline termination.
+
+Note that this means that the terminator character (newline or _NUL_)
+acts as a _terminator_, not a _separator_. In other words, the final
+line or record is also terminated by the terminator character.
+
+The mode `--stdin-mode=text` replaces each object name with the
+formatted commit, i.e. the format `%s` would transform the object name
+`abcdef012...` to `<subject>` without any termination. Like this:
+
+----
+Did we not fix this in "<subject>"?
+----
+
+Regarding input in this mode: using `-z` or `--null-input` makes sure
+that _NUL_ characters in the input are passed through correctly.
+
+It is safe to interactively read and write from this command since each
+record is immediately flushed.
+
+[[examples]]
+EXAMPLES
+--------
+
+The command linkgit:git-last-modified[1] shows the commit that each file
+was last modified in.
+
+----
+$ git last-modified -- README.md Makefile
+7798034171030be0909c56377a4e0e10e6d2df93	Makefile
+c50fbb2dd225e7e82abba4380423ae105089f4d7	README.md
+----
+
+We can pipe the result to this command in order to replace the object
+name with the commit author.
+
+----
+$ git last-modified -- README.md Makefile |
+    git format-rev --stdin-mode=text --format=%an
+Junio C Hamano	Makefile
+Todd Zullinger	README.md
+----
+
+Another example is _formatting commits in commit messages_. Given this commit message:
+
+----
+Fix off-by-one error
+
+Fix off-by-one error introduced in
+e83c5163316f89bfbde7d9ab23ca2e25604af290.
+
+We thought we fixed this in 5569bf9bbedd63a00780fc5c110e0cfab3aa97b9 but
+that only covered 1/3 of the faulty cases.
+----
+
+We can format the commits and use par(1) to reflow the text, say in a
+`commit-msg` hook:
+
+----
+$ git config set hook.reference-commits.event commit-msg
+$ git config set hook.reference-commits.command reference-commits
+$ cat $(which reference-commits)
+#/bin/sh
+
+msg="$1"
+rewritten=$(mktemp)
+git format-rev --stdin-mode=text --format=reference <"$msg" |
+    par >"$rewritten"
+mv "$rewritten" "$msg"
+----
+
+Which will produce something like this:
+
+----
+Fix off-by-one error
+
+Fix off-by-one error introduced in e83c5163316 (Implement better memory
+allocator, 2005-04-07).
+
+We thought we fixed this in 5569bf9bbed (Fix memory allocator,
+2005-06-22) but that only covered 1/3 of the faulty cases.
+----
+
+DISCUSSION
+----------
+
+This command lets you format any number of revisions in any order
+through one command invocation. Consider the
+linkgit:git-last-modified[1] case from the <<examples,EXAMPLES>> section
+above:
+
+1. There might be hundreds of files
+2. Commits can be repeated, i.e. two or more files were last modified in
+   the same commit
+
+Two widely-used commands which pretty formats commits are
+linkgit:git-log[1] and linkgit:git-show[1]. It turns out that they are
+not a good fit for the above use case.
+
+- The output of linkgit:git-last-modified[1] would have to be processed
+  in stages since you need to transform the first column separately and
+  then link the author to the filename. But this is surmountable.
+- You can feed each commit to `git show` or `git log --no-walk -1`. But
+  that means that you need to create a process for each line.
+- Let’s say that you want to use one process, not one per line. So you
+  want to feed all the commits to the command. Now you face the problem
+  that you have to feed all the commits to the commands before you get
+  any output (this is also the case for the `--stdin` modes). In other
+  words, you cannot loop through each line, get the author for the
+  commit, and output the author and the filename. You need to feed all
+  the commits, get back all the output, and match the output with the
+  filename.
+- But the next problem is that commands will deduplicate the input and
+  only output one commit one single time only. Thus you cannot make the
+  output order match the input order, since a commit could have been
+  repeated in the original input.
+
+In short, it is straightforward to use these two commands if you use one
+process per line. It is much more work if you just want to use one
+process, but still doable. In contrast, this problem is just another
+shell pipeline with this command.
+
+SEE ALSO
+--------
+linkgit:git-name-rev[1],
+linkgit:git-log[1].
+
+GIT
+---
+Part of the linkgit:git[1] suite
diff --git a/Documentation/meson.build b/Documentation/meson.build
index d6365b888bb..58e7c6a0b8a 100644
--- a/Documentation/meson.build
+++ b/Documentation/meson.build
@@ -55,6 +55,7 @@ manpages = {
   'git-for-each-ref.adoc' : 1,
   'git-for-each-repo.adoc' : 1,
   'git-format-patch.adoc' : 1,
+  'git-format-rev.adoc' : 1,
   'git-fsck-objects.adoc' : 1,
   'git-fsck.adoc' : 1,
   'git-fsmonitor--daemon.adoc' : 1,
diff --git a/Makefile b/Makefile
index 15b1ded1a0b..cbaf91fd846 100644
--- a/Makefile
+++ b/Makefile
@@ -895,6 +895,7 @@ BUILT_INS += $(patsubst builtin/%.o,git-%$X,$(BUILTIN_OBJS))
 BUILT_INS += git-cherry$X
 BUILT_INS += git-cherry-pick$X
 BUILT_INS += git-format-patch$X
+BUILT_INS += git-format-rev$X
 BUILT_INS += git-fsck-objects$X
 BUILT_INS += git-init$X
 BUILT_INS += git-maintenance$X
diff --git a/builtin.h b/builtin.h
index 235c51f30e5..63813c90125 100644
--- a/builtin.h
+++ b/builtin.h
@@ -189,6 +189,7 @@ int cmd_fmt_merge_msg(int argc, const char **argv, const char *prefix, struct re
 int cmd_for_each_ref(int argc, const char **argv, const char *prefix, struct repository *repo);
 int cmd_for_each_repo(int argc, const char **argv, const char *prefix, struct repository *repo);
 int cmd_format_patch(int argc, const char **argv, const char *prefix, struct repository *repo);
+int cmd_format_rev(int argc, const char **argv, const char *prefix, struct repository *repo);
 int cmd_fsck(int argc, const char **argv, const char *prefix, struct repository *repo);
 int cmd_fsmonitor__daemon(int argc, const char **argv, const char *prefix, struct repository *repo);
 int cmd_gc(int argc, const char **argv, const char *prefix, struct repository *repo);
diff --git a/builtin/name-rev.c b/builtin/name-rev.c
index 475efb0b82b..5494b0424b3 100644
--- a/builtin/name-rev.c
+++ b/builtin/name-rev.c
@@ -18,6 +18,10 @@
 #include "commit-graph.h"
 #include "wildmatch.h"
 #include "mem-pool.h"
+#include "pretty.h"
+#include "revision.h"
+#include "notes.h"
+#include "write-or-die.h"
 
 /*
  * One day.  See the 'name a rev shortly after epoch' test in t6120 when
@@ -272,14 +276,26 @@ struct name_ref_data {
 	struct string_list exclude_filters;
 };
 
+struct pretty_format {
+	struct pretty_print_context ctx;
+	struct userformat_want want;
+};
+
 enum command_type {
 	NAME_REV = 1,
+	FORMAT_REV = 2,
+};
+
+enum stdin_mode {
+    TEXT = 1,
+    REVS = 2,
 };
 
 struct command {
 	enum command_type type;
 	union {
 		int name_only;
+		struct pretty_format *pretty_format;
 	} u;
 };
 
@@ -290,6 +306,13 @@ static void init_name_rev_command(struct command *cmd,
 	cmd->u.name_only = name_only;
 }
 
+static void init_format_rev_command(struct command *cmd,
+				    struct pretty_format *pretty_format)
+{
+	cmd->type = FORMAT_REV;
+	cmd->u.pretty_format = pretty_format;
+}
+
 static struct tip_table {
 	struct tip_table_entry {
 		struct object_id oid;
@@ -495,6 +518,27 @@ static const char *get_rev_name(const struct object *o, struct strbuf *buf)
 	}
 }
 
+static const char *get_format_rev(const struct commit *c,
+				  struct pretty_format *format_ctx,
+				  struct strbuf *buf)
+{
+	strbuf_reset(buf);
+
+	if (format_ctx->want.notes) {
+		struct strbuf notebuf = STRBUF_INIT;
+
+		format_display_notes(&c->object.oid, &notebuf,
+				     get_log_output_encoding(),
+				     format_ctx->ctx.fmt == CMIT_FMT_USERFORMAT);
+		format_ctx->ctx.notes_message = strbuf_detach(&notebuf, NULL);
+	}
+
+	pretty_print_commit(&format_ctx->ctx, c, buf);
+	FREE_AND_NULL(format_ctx->ctx.notes_message);
+
+	return buf->buf;
+}
+
 static void show_name(const struct object *obj,
 		      const char *caller_name,
 		      int always, int allow_undefined, int name_only)
@@ -564,6 +608,18 @@ static void name_rev_line(char *p, struct command *cmd)
 				else
 					printf("%.*s (%s)", p_len, p_start, name);
 				break;
+			case FORMAT_REV:
+				if (!oid_ret)
+					o = parse_object(the_repository, &oid);
+				if (o && o->type == OBJ_COMMIT)
+					name = get_format_rev((const struct commit *)o,
+							      cmd->u.pretty_format,
+							      &buf);
+				if (name)
+					printf("%.*s%s", p_len - hexsz, p_start, name);
+				else
+					printf("%.*s", p_len, p_start);
+				break;
 			default:
 				BUG("uncovered case: %d", cmd->type);
 			}
@@ -717,3 +773,170 @@ int cmd_name_rev(int argc,
 	object_array_clear(&revs);
 	return 0;
 }
+
+struct format_nul_data {
+	bool nul_input;
+	bool nul_output;
+};
+
+static int format_nul_cb(const struct option *option,
+			 const char *arg,
+			 int unset)
+{
+	struct format_nul_data *data = option->value;
+	data->nul_input = 1;
+	data->nul_output = 1;
+	BUG_ON_OPT_NEG(unset);
+	BUG_ON_OPT_ARG(arg);
+	return 0;
+}
+
+static enum stdin_mode parse_stdin_mode(const char *stdin_mode)
+{
+	if (!strcmp(stdin_mode, "text"))
+		return TEXT;
+	else if (!strcmp(stdin_mode, "revs") ||
+		 !strcmp(stdin_mode, "rev"))
+		return REVS;
+	else
+		die(_("'%s' needs to be either text, revs, or rev"),
+		    "--stdin-mode");
+}
+
+static char const *const format_rev_usage[] = {
+	N_("(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> "
+	   "--format=<pretty> [--[no-]notes=<ref>] "
+	   "[-z] [--[no-]null-output] [--[no-]null-input]"),
+	NULL
+};
+
+int cmd_format_rev(int argc,
+		   const char **argv,
+		   const char *prefix,
+		   struct repository *repo UNUSED)
+{
+	const char *format = NULL;
+	enum stdin_mode stdin_mode;
+	const char *stdin_mode_arg = NULL;
+	struct format_nul_data nul_data = { 0, 0 };
+	char output_terminator;
+	strbuf_getline_fn getline_fn;
+	struct display_notes_opt format_notes_opt;
+	struct rev_info format_rev = REV_INFO_INIT;
+	struct pretty_format format_pp = { 0 };
+	struct string_list notes = STRING_LIST_INIT_NODUP;
+	struct strbuf scratch_buf = STRBUF_INIT;
+	struct command cmd;
+	struct option opts[] = {
+		OPT_STRING(0, "format", &format, N_("format"),
+			   N_("pretty format to use")),
+		OPT_STRING(0, "stdin-mode", &stdin_mode_arg, N_("stdin-mode"),
+			   N_("how revs are processed")),
+		OPT_STRING_LIST(0, "notes", &notes, N_("notes"),
+				N_("display notes for pretty format")),
+		OPT_CALLBACK_F('z', "null", &nul_data, N_("z"),
+			       N_("Use NUL for input and output termination"),
+			       PARSE_OPT_NOARG | PARSE_OPT_NONEG, format_nul_cb),
+		OPT_BOOL(0, "null-input", &nul_data.nul_input,
+			 N_("Use NUL for input termination")),
+		OPT_BOOL(0, "null-output", &nul_data.nul_output,
+			 N_("Use NUL for output termination")),
+		OPT_END(),
+	};
+
+	argc = parse_options(argc, argv, prefix, opts, format_rev_usage, 0);
+
+	if (argc > 0) {
+		error(_("too many arguments"));
+		usage_with_options(format_rev_usage, opts);
+	}
+
+	if (!format)
+		die(_("'%s' is required"), "--format");
+	if (!stdin_mode_arg)
+		die(_("'%s' is required"), "--stdin-mode");
+
+	getline_fn = nul_data.nul_input ? strbuf_getline_nul : strbuf_getline_lf;
+	output_terminator = nul_data.nul_output ? '\0' : '\n';
+
+	init_display_notes(&format_notes_opt);
+	stdin_mode = parse_stdin_mode(stdin_mode_arg);
+
+	get_commit_format(format, &format_rev);
+	format_pp.ctx.rev = &format_rev;
+	format_pp.ctx.fmt = format_rev.commit_format;
+	format_pp.ctx.abbrev = format_rev.abbrev;
+	format_pp.ctx.date_mode_explicit = format_rev.date_mode_explicit;
+	format_pp.ctx.date_mode = format_rev.date_mode;
+	format_pp.ctx.color = GIT_COLOR_AUTO;
+
+	userformat_find_requirements(format,
+				     &format_pp.want);
+	if (format_pp.want.notes) {
+		int ignore_show_notes = 0;
+		struct string_list_item *n;
+
+		for_each_string_list_item(n, &notes)
+			enable_ref_display_notes(&format_notes_opt,
+						 &ignore_show_notes,
+						 n->string);
+		load_display_notes(&format_notes_opt);
+	}
+
+	init_format_rev_command(&cmd, &format_pp);
+
+	switch (stdin_mode) {
+	case TEXT:
+		while (getline_fn(&scratch_buf, stdin) != EOF) {
+			name_rev_line(scratch_buf.buf, &cmd);
+			/*
+			 * We do not pass on the terminator to name_rev_line,
+			 * unlike name-rev.
+			 */
+			printf("%c", output_terminator);
+			maybe_flush_or_die(stdout, "stdout");
+		}
+		break;
+	case REVS:
+		while (getline_fn(&scratch_buf, stdin) != EOF) {
+			struct object_id oid;
+			struct object *object;
+			struct object *peeled;
+
+			if (repo_get_oid(the_repository, scratch_buf.buf, &oid)) {
+				fprintf(stderr, "Could not get object name for %s. Skipping.\n",
+					scratch_buf.buf);
+				continue;
+			}
+
+			object = parse_object(the_repository, &oid);
+			if (!object) {
+				fprintf(stderr, "Could not get object for %s. Skipping.\n",
+					scratch_buf.buf);
+				continue;
+			}
+
+			peeled = deref_tag(the_repository, object, scratch_buf.buf, 0);
+			if (!peeled || peeled->type != OBJ_COMMIT) {
+				fprintf(stderr,
+					"Could not get commit for %s. Skipping.\n",
+					scratch_buf.buf);
+				continue;
+			}
+
+			get_format_rev((struct commit *)peeled,
+				       &format_pp, &scratch_buf);
+			printf("%s%c", scratch_buf.buf, output_terminator);
+			maybe_flush_or_die(stdout, "stdout");
+			strbuf_release(&scratch_buf);
+		}
+		break;
+	default:
+		BUG("uncovered case: %d", stdin_mode);
+	}
+
+	strbuf_release(&scratch_buf);
+	string_list_clear(&notes, 0);
+	release_display_notes(&format_notes_opt);
+	return 0;
+}
diff --git a/command-list.txt b/command-list.txt
index f9005cf4597..df729872dca 100644
--- a/command-list.txt
+++ b/command-list.txt
@@ -108,6 +108,7 @@ git-fmt-merge-msg                       purehelpers
 git-for-each-ref                        plumbinginterrogators
 git-for-each-repo                       plumbinginterrogators
 git-format-patch                        mainporcelain
+git-format-rev                          plumbinginterrogators
 git-fsck                                ancillaryinterrogators          complete
 git-gc                                  mainporcelain
 git-get-tar-commit-id                   plumbinginterrogators
diff --git a/git.c b/git.c
index 2b212e6675d..af5b0422b00 100644
--- a/git.c
+++ b/git.c
@@ -578,6 +578,7 @@ static struct cmd_struct commands[] = {
 	{ "for-each-ref", cmd_for_each_ref, RUN_SETUP },
 	{ "for-each-repo", cmd_for_each_repo, RUN_SETUP_GENTLY },
 	{ "format-patch", cmd_format_patch, RUN_SETUP },
+	{ "format-rev", cmd_format_rev, RUN_SETUP },
 	{ "fsck", cmd_fsck, RUN_SETUP },
 	{ "fsck-objects", cmd_fsck, RUN_SETUP },
 	{ "fsmonitor--daemon", cmd_fsmonitor__daemon, RUN_SETUP },
diff --git a/t/t1517-outside-repo.sh b/t/t1517-outside-repo.sh
index c824c1a25cf..360a9323343 100755
--- a/t/t1517-outside-repo.sh
+++ b/t/t1517-outside-repo.sh
@@ -114,7 +114,8 @@ do
 	archimport | citool | credential-netrc | credential-libsecret | \
 	credential-osxkeychain | cvsexportcommit | cvsimport | cvsserver | \
 	daemon | \
-	difftool--helper | filter-branch | fsck-objects | get-tar-commit-id | \
+	difftool--helper | filter-branch | format-rev | fsck-objects | \
+	get-tar-commit-id | \
 	gui | gui--askpass | \
 	http-backend | http-fetch | http-push | init-db | \
 	merge-octopus | merge-one-file | merge-resolve | mergetool | \
diff --git a/t/t6120-describe.sh b/t/t6120-describe.sh
index 62789f76381..8ee3d2c37d0 100755
--- a/t/t6120-describe.sh
+++ b/t/t6120-describe.sh
@@ -801,4 +801,198 @@ test_expect_success 'do not be fooled by invalid describe format ' '
 	test_must_fail git cat-file -t "refs/tags/super-invalid/./../...../ ~^:/?*[////\\\\\\&}/busted.lock-42-g"$(cat out)
 '
 
+test_expect_success 'setup: format-rev' '
+	mkdir repo-format &&
+	git -C repo-format init &&
+	test_commit -C repo-format first &&
+	test_commit -C repo-format second &&
+	test_commit -C repo-format third &&
+	test_commit -C repo-format fourth &&
+	test_commit -C repo-format fifth &&
+	test_commit -C repo-format sixth &&
+	test_commit -C repo-format seventh &&
+	test_commit -C repo-format eighth
+'
+
+test_expect_success 'format-rev --stdin-mode=revs' '
+	cat >expect <<-\EOF &&
+	eighth
+	seventh
+	fifth
+	EOF
+	git -C repo-format format-rev --stdin-mode=revs \
+		--format=%s >actual <<-\EOF &&
+	HEAD
+	HEAD~
+	HEAD~3
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success 'format-rev --stdin-mode=text from rev-list same as log' '
+	git -C repo-format log --format=reference >expect &&
+	test_file_not_empty expect &&
+	git -C repo-format rev-list HEAD >list &&
+	git -C repo-format format-rev --stdin-mode=text \
+		--format=reference <list >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'format-rev --stdin-mode=text with running text and tree oid' '
+	cmit_oid=$(git -C repo-format rev-parse fifth) &&
+	reference=$(git -C repo-format log -n1 --format=reference fifth) &&
+	tree=$(git -C repo-format rev-parse HEAD^{tree}) &&
+	cat >expect <<-EOF &&
+	We thought we fixed this in ${reference}.
+	But look at this tree: ${tree}.
+	EOF
+	git -C repo-format format-rev --stdin-mode=text --format=reference \
+		>actual <<-EOF &&
+	We thought we fixed this in ${cmit_oid}.
+	But look at this tree: ${tree}.
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success 'format-rev with %N (note)' '
+	test_when_finished "git -C repo-format notes remove" &&
+	git -C repo-format notes add -m"Make a note" &&
+	printf "Make a note\n\n\n" >expect &&
+	git -C repo-format format-rev --stdin-mode=revs \
+		--format="tformat:%N" \
+		>actual <<-\EOF &&
+	HEAD
+	HEAD~
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success 'format-rev --notes<ref> (custom notes ref)' '
+	# One custom notes ref
+	test_when_finished "git -C repo-format notes remove" &&
+	test_when_finished "git -C repo-format notes --ref=word remove" &&
+	git -C repo-format notes add -m"default" &&
+	git -C repo-format notes --ref=word add -m"custom" &&
+	printf "custom\n\n" >expect &&
+	git -C repo-format format-rev --stdin-mode=revs \
+		--format="tformat:%N" \
+		--notes=word \
+		>actual <<-\EOF &&
+	HEAD
+	EOF
+	test_cmp expect actual &&
+	# Glob all
+	printf "default\ncustom\n\n" >expect &&
+	git -C repo-format format-rev --stdin-mode=revs \
+		--format="tformat:%N" \
+		--notes=* >actual <<-\EOF &&
+	HEAD
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success 'format-rev --stdin-mode=revs on annotated tag peels to commit' '
+	test_when_finished "git -C repo-format tag -d version" &&
+	git -C repo-format tag -a -m"new version" version &&
+	cat >expect <<-\EOF &&
+	eighth
+	EOF
+	git -C repo-format format-rev --stdin-mode=revs \
+		--format=%s \
+		>actual <<-\EOF &&
+	version
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success 'format-rev --stdin-mode=revs lookup failures' '
+	test_when_finished "git -C repo-format tag -d tag-to-tree" &&
+	invalid_syntax=not-valid &&
+	non_existing_oid=${EMPTY_BLOB} &&
+	tree=$(git -C repo-format rev-parse eighth^{tree}) &&
+	git -C repo-format tag -a -mmessage tag-to-tree "$tree" &&
+	tag_to_tree=$(git -C repo-format rev-parse tag-to-tree) &&
+	cat >expect <<-EOF &&
+	Could not get object name for ${invalid_syntax}. Skipping.
+	Could not get object for ${non_existing_oid}. Skipping.
+	Could not get commit for ${tree}. Skipping.
+	Could not get commit for ${tag_to_tree}. Skipping.
+	EOF
+	git -C repo-format format-rev --stdin-mode=revs \
+		--format=%s \
+		2>actual >out <<-EOF &&
+	${invalid_syntax}
+	${non_existing_oid}
+	${tree}
+	${tag_to_tree}
+	EOF
+	test_line_count = 0 out &&
+	test_cmp expect actual
+'
+
+
+test_expect_success 'format-rev -z --stdin-mode=text with object name lookup failures' '
+	printf "%s\0" "$(git -C repo-format rev-parse HEAD)" >input &&
+	printf "%s\0" "$(git -C repo-format rev-parse HEAD^{tree})" >>input &&
+	printf "%s\0" "$EMPTY_BLOB" >>input &&
+	printf "%s\0" "$(git -C repo-format log --format=%s -1)" >expect &&
+	printf "%s\0" "$(git -C repo-format rev-parse HEAD^{tree})" >>expect &&
+	printf "%s\0" "$EMPTY_BLOB" >>expect &&
+	git -C repo-format format-rev --stdin-mode=text \
+		--format=%s -z <input >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'setup: format-rev input and output separators' '
+	git -C repo-format rev-list HEAD >input-lf &&
+	git -C repo-format rev-list -z HEAD >input-nul &&
+	git -C repo-format log --format=%s >output-lf &&
+	git -C repo-format log -z --format=%s >output-nul &&
+	echo revs >stdin-modes &&
+	echo text >>stdin-modes
+'
+
+while read mode
+do
+	test_expect_success "format-rev -z --stdin-mode=$mode" '
+		cat output-nul >expect &&
+		git -C repo-format format-rev --stdin-mode="$mode" \
+			--format=%s -z <input-nul >actual &&
+		test_cmp expect actual
+	'
+
+	test_expect_success "format-rev -z --no-null-input --no-null-output --stdin-mode=$mode" '
+		cat output-lf >expect &&
+		git -C repo-format format-rev --stdin-mode="$mode" \
+			--format=%s -z --no-null-input --no-null-output \
+			<input-lf >actual &&
+		test_cmp expect actual
+	'
+
+	test_expect_success "format-rev ---null-input --stdin-mode=$mode" '
+		cat output-lf >expect &&
+		git -C repo-format format-rev --stdin-mode="$mode" \
+			--format=%s --null-input \
+			<input-nul >actual &&
+		test_cmp expect actual
+	'
+
+	test_expect_success "format-rev --null-output --stdin-mode=$mode" '
+		cat output-nul >expect &&
+		git -C repo-format format-rev --stdin-mode="$mode" \
+			--format=%s --null-output \
+			<input-lf >actual &&
+		test_cmp expect actual
+	'
+
+	test_expect_success "format-rev -z --stdin-mode=$mode with multi-line output" '
+		format="%s%n%aI" &&
+		git -C repo-format log -z --format="$format" \
+			>expect &&
+		git -C repo-format format-rev --stdin-mode="$mode" \
+			--format="$format" -z <input-nul >actual &&
+		test_cmp expect actual
+	'
+done <stdin-modes
+
 test_done
-- 
2.54.0.13.g9c7419e39f8
Kristoffer HaugsbakkMay 8, 2026, 13:25 UTC in reply to kristofferhaugsbakk@fastmail.com on lore

Re: [PATCH v4 5/5] format-rev: introduce builtin for on-demand pretty formatting

On Thu, May 7, 2026, at 21:34, me myself wrote:
Show 28 quoted lines
> From: Kristoffer Haugsbakk <code@khaugsbakk.name>
>[snip]
> +[[io]]
> +INPUT AND OUTPUT FORMAT
> +-----------------------
> +
> +The command uses newlines for both input and output termination by
> +default. See the `-z`, `--null-output`, and `--null-input` options for
> +using _NUL_ character as the terminator.
> +
> +The mode `--stdin-mode=revs` outputs one formatted commit followed by
> +the terminator. This could either be called a _line_ or a _record_ in
> +case "line" is too suggestive of newline termination.
> +
> +Note that this means that the terminator character (newline or _NUL_)
> +acts as a _terminator_, not a _separator_. In other words, the final
> +line or record is also terminated by the terminator character.
> +
> +The mode `--stdin-mode=text` replaces each object name with the
> +formatted commit, i.e. the format `%s` would transform the object name
> +`abcdef012...` to `<subject>` without any termination. Like this:
> +
> +----
> +Did we not fix this in "<subject>"?
> +----
> +
> +Regarding input in this mode: using `-z` or `--null-input` makes sure
> +that _NUL_ characters in the input are passed through correctly.
This was the paragraph that I was unhappy with (see cover letter).

We’re supposed to be dealing with text here. So on second thought I don’t think we need to fuzz about passing through NUL characters.

Instead I could mention that `-z` for this mode allows you to further transform running text from other commands that use NUL termination. Like `git last-modified -z` (see the Examples section).

Show 11 quoted lines
> +
> +It is safe to interactively read and write from this command since each
> +record is immediately flushed.
> +
> +[[examples]]
> +EXAMPLES
> +--------
> +
> +The command linkgit:git-last-modified[1] shows the commit that each file
> +was last modified in.
>[snip]
Kristoffer HaugsbakkMay 11, 2026, 13:25 UTC in reply to kristofferhaugsbakk@fastmail.com on lore

Re: [PATCH v4 5/5] format-rev: introduce builtin for on-demand pretty formatting

On Thu, May 7, 2026, at 21:34, kristofferhaugsbakk@fastmail.com wrote:
Show 29 quoted lines
> From: Kristoffer Haugsbakk <code@khaugsbakk.name>
>
> Introduce a new builtin for pretty formatting one revision expression
> per line or commit object names found in running text.
>
>[snip]
>
> diff --git a/Documentation/git-format-rev.adoc
>[snip]
> +OPTIONS
> +-------
> +
> +`--stdin-mode=<mode>`::
> +	How to interpret standard input data:
> ++
> +--
> +`revs`:: Each line or record (see the <<io,INPUT AND OUTPUT FORMATS>>
> +	section) is interpreted as a commit. Any kind of revision
> +	expression can be used (see linkgit:gitrevisions[7]). Annotated
> +	tags are peeled (see linkgit:gitglossary[7]).
> ++
> +The argument `rev` is also accepted.
> +`text`:: Formats all commit object names found in freeform text. These
> +	must the full object names, i.e. abbreviated hexidecimal object
> +	names will not be interpreted.
> ++
> +Anything that is parsed as an object name but that is not found to be a
> +commit object name is left alone (echoed).
> +--
This nested definition list is malformed. I’ll need to fix that.
> +
> +`--format=<pretty>`::
> +	Pretty format string.
>[snip]
kristofferhaugsbakk@fastmail.comMay 11, 2026, 15:45 UTC in reply to kristofferhaugsbakk@fastmail.com on lore

[PATCH v5 0/5] format-rev: introduce builtin for on-demand pretty formatting

From: Kristoffer Haugsbakk <code@khaugsbakk.name>
(Subject from v2: name-rev: learn --format=<pretty>)
Topic name (applied): kh/name-rev-custom-format

Topic summary: Introduce a new builtin for pretty formatting either (1) one revision expression per line or (2) commit object names found in running text.

See the last patch for the motivation. In short there isn’t anything that I have found that lets you format however many commits you want through one process (so looping over `git show` is excepted).

The other patches prepare for this change.
§ Changes in v5
Notes from patch 5/5, all doc changes:
    • Fix definition list mistake
      • No blank line between the two modes
      • Also replace `::` delimiters with `;;`. This is not strictly
        needed since the definition list is inside an open block. But
        it is consistent with all other definition list in definition
        lists I’ve seen. (The command option section is a long
        definition list.)
    • Rewrite part that I was unhappy about regarding
      `--stdin-mode=text` and `-z`:
        https://lore.kernel.org/git/c04d9cf9-e6a9-4e12-8025-9baededfdafc@app.fastmail.com/
    • Replace `abcdef012...` with just “some object name”. The object
      name is arbitrary and my AsciiDoc output seems to do some weird
      things for `...` inside backticks for HTML.
    • Replace “this problem is just another” with “this problem
      *is solved* with...”
§ Link to v4
https://lore.kernel.org/git/V4_CV_format-rev.6aa@msgid.xyz/

[1/5] name-rev: wrap both blocks in braces [2/5] name-rev: run clang-format before factoring code [3/5] name-rev: factor code for sharing with a new command [4/5] name-rev: make dedicated --annotate-stdin --name-only test [5/5] format-rev: introduce builtin for on-demand pretty formatting

 .gitignore                        |   1 +
 Documentation/git-format-rev.adoc | 215 +++++++++++++++++++++
 Documentation/meson.build         |   1 +
 Makefile                          |   1 +
 builtin.h                         |   1 +
 builtin/name-rev.c                | 300 +++++++++++++++++++++++++++---
 command-list.txt                  |   1 +
 git.c                             |   1 +
 t/t1517-outside-repo.sh           |   3 +-
 t/t6120-describe.sh               | 208 +++++++++++++++++++++
 10 files changed, 706 insertions(+), 26 deletions(-)
 create mode 100644 Documentation/git-format-rev.adoc
Interdiff against v4:
Show changes to Documentation/git-format-rev.adoc +13 −13
diff --git a/Documentation/git-format-rev.adoc b/Documentation/git-format-rev.adoc
index 436980012bc..c40d52e9f6d 100644
--- a/Documentation/git-format-rev.adoc
+++ b/Documentation/git-format-rev.adoc
@@ -25,13 +25,14 @@ OPTIONS
 	How to interpret standard input data:
 +
 --
-`revs`:: Each line or record (see the <<io,INPUT AND OUTPUT FORMATS>>
+`revs`;; Each line or record (see the <<io,INPUT AND OUTPUT FORMATS>>
 	section) is interpreted as a commit. Any kind of revision
 	expression can be used (see linkgit:gitrevisions[7]). Annotated
 	tags are peeled (see linkgit:gitglossary[7]).
 +
 The argument `rev` is also accepted.
-`text`:: Formats all commit object names found in freeform text. These
+
+`text`;; Formats all commit object names found in freeform text. These
 	must the full object names, i.e. abbreviated hexidecimal object
 	names will not be interpreted.
 +
@@ -53,8 +54,12 @@ commit object name is left alone (echoed).
 	of newline. This option cannot be negated.
 +
 This is useful if both the input and output could contain newlines or if
-the input could contain _NUL_ characters; see the <<io,INPUT AND OUTPUT
-FORMATS>> section.
+the input to this command also uses _NUL_ character termination; see the
+<<io,INPUT AND OUTPUT FORMATS>> section below.
++
+The mode `--stdin-mode=text` can have use for this option when it needs
+to process input like for example `git last-modified -z`; see the
+<<examples,EXAMPLES>> section below.
 
 `--null-output`::
 `--no-null-output`::
@@ -70,8 +75,6 @@ This is useful if the output could contain newlines, for example if the
 	default is `--no-null-input`.
 +
 This is useful if the input revision expressions could contain newlines.
-It is also useful if the input could contain _NUL_ characters; see the
-<<io,INPUT AND OUTPUT FORMATS>> section.
 
 [[io]]
 INPUT AND OUTPUT FORMAT
@@ -90,16 +93,13 @@ acts as a _terminator_, not a _separator_. In other words, the final
 line or record is also terminated by the terminator character.
 
 The mode `--stdin-mode=text` replaces each object name with the
-formatted commit, i.e. the format `%s` would transform the object name
-`abcdef012...` to `<subject>` without any termination. Like this:
+formatted commit, i.e. the format `%s` would transform some commit
+object name to `<subject>` without any termination. Like this:
 
 ----
 Did we not fix this in "<subject>"?
 ----
 
-Regarding input in this mode: using `-z` or `--null-input` makes sure
-that _NUL_ characters in the input are passed through correctly.
-
 It is safe to interactively read and write from this command since each
 record is immediately flushed.
 
@@ -202,8 +202,8 @@ not a good fit for the above use case.
 
 In short, it is straightforward to use these two commands if you use one
 process per line. It is much more work if you just want to use one
-process, but still doable. In contrast, this problem is just another
-shell pipeline with this command.
+process, but still doable. In contrast, this problem is solved with just
+another shell pipeline with this command.
 
 SEE ALSO
 --------
Range-diff against v4:
1:  9cb5cfd1ec3 = 1:  9cb5cfd1ec3 name-rev: wrap both blocks in braces
2:  14900271321 = 2:  14900271321 name-rev: run clang-format before factoring code
3:  724ec022894 = 3:  724ec022894 name-rev: factor code for sharing with a new command
4:  382efc3ddb8 = 4:  382efc3ddb8 name-rev: make dedicated --annotate-stdin --name-only test
5:  049a45e32bc ! 5:  425eb16728c format-rev: introduce builtin for on-demand pretty formatting
    @@ Documentation/git-format-rev.adoc (new)
     +	How to interpret standard input data:
     ++
     +--
    -+`revs`:: Each line or record (see the <<io,INPUT AND OUTPUT FORMATS>>
    ++`revs`;; Each line or record (see the <<io,INPUT AND OUTPUT FORMATS>>
     +	section) is interpreted as a commit. Any kind of revision
     +	expression can be used (see linkgit:gitrevisions[7]). Annotated
     +	tags are peeled (see linkgit:gitglossary[7]).
     ++
     +The argument `rev` is also accepted.
    -+`text`:: Formats all commit object names found in freeform text. These
    ++
    ++`text`;; Formats all commit object names found in freeform text. These
     +	must the full object names, i.e. abbreviated hexidecimal object
     +	names will not be interpreted.
     ++
    @@ Documentation/git-format-rev.adoc (new)
     +	of newline. This option cannot be negated.
     ++
     +This is useful if both the input and output could contain newlines or if
    -+the input could contain _NUL_ characters; see the <<io,INPUT AND OUTPUT
    -+FORMATS>> section.
    ++the input to this command also uses _NUL_ character termination; see the
    ++<<io,INPUT AND OUTPUT FORMATS>> section below.
    +++
    ++The mode `--stdin-mode=text` can have use for this option when it needs
    ++to process input like for example `git last-modified -z`; see the
    ++<<examples,EXAMPLES>> section below.
     +
     +`--null-output`::
     +`--no-null-output`::
    @@ Documentation/git-format-rev.adoc (new)
     +	default is `--no-null-input`.
     ++
     +This is useful if the input revision expressions could contain newlines.
    -+It is also useful if the input could contain _NUL_ characters; see the
    -+<<io,INPUT AND OUTPUT FORMATS>> section.
     +
     +[[io]]
     +INPUT AND OUTPUT FORMAT
    @@ Documentation/git-format-rev.adoc (new)
     +line or record is also terminated by the terminator character.
     +
     +The mode `--stdin-mode=text` replaces each object name with the
    -+formatted commit, i.e. the format `%s` would transform the object name
    -+`abcdef012...` to `<subject>` without any termination. Like this:
    ++formatted commit, i.e. the format `%s` would transform some commit
    ++object name to `<subject>` without any termination. Like this:
     +
     +----
     +Did we not fix this in "<subject>"?
     +----
     +
    -+Regarding input in this mode: using `-z` or `--null-input` makes sure
    -+that _NUL_ characters in the input are passed through correctly.
    -+
     +It is safe to interactively read and write from this command since each
     +record is immediately flushed.
     +
    @@ Documentation/git-format-rev.adoc (new)
     +
     +In short, it is straightforward to use these two commands if you use one
     +process per line. It is much more work if you just want to use one
    -+process, but still doable. In contrast, this problem is just another
    -+shell pipeline with this command.
    ++process, but still doable. In contrast, this problem is solved with just
    ++another shell pipeline with this command.
     +
     +SEE ALSO
     +--------

base-commit: 67006b9db8b772423ad0706029286096307d2567
-- 
2.54.0.13.g9c7419e39f8
kristofferhaugsbakk@fastmail.comMay 11, 2026, 15:45 UTC in reply to kristofferhaugsbakk@fastmail.com on lore

[PATCH v5 1/5] name-rev: wrap both blocks in braces

From: Kristoffer Haugsbakk <code@khaugsbakk.name>
See `CodingGuidelines`:
    - When there are multiple arms to a conditional and some of them
      require braces, enclose even a single line block in braces for
      consistency. [...]
Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
---
Notes (series):
    v2:
    Fix stray formatting of `(p+1)`
 builtin/name-rev.c | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)
Show changes to builtin/name-rev.c +4 −4
diff --git a/builtin/name-rev.c b/builtin/name-rev.c
index 6188cf98ce0..171e7bd0e98 100644
--- a/builtin/name-rev.c
+++ b/builtin/name-rev.c
@@ -466,9 +466,9 @@ static const char *get_rev_name(const struct object *o, struct strbuf *buf)
 	if (!n)
 		return NULL;
 
-	if (!n->generation)
+	if (!n->generation) {
 		return n->tip_name;
-	else {
+	} else {
 		strbuf_reset(buf);
 		strbuf_addstr(buf, n->tip_name);
 		strbuf_strip_suffix(buf, "^0");
@@ -516,9 +516,9 @@ static void name_rev_line(char *p, struct name_ref_data *data)
 
 	for (p_start = p; *p; p++) {
 #define ishex(x) (isdigit((x)) || ((x) >= 'a' && (x) <= 'f'))
-		if (!ishex(*p))
+		if (!ishex(*p)) {
 			counter = 0;
-		else if (++counter == hexsz &&
+		} else if (++counter == hexsz &&
 			 !ishex(*(p+1))) {
 			struct object_id oid;
 			const char *name = NULL;
-- 
2.54.0.13.g9c7419e39f8
kristofferhaugsbakk@fastmail.comMay 11, 2026, 15:45 UTC in reply to kristofferhaugsbakk@fastmail.com on lore

[PATCH v5 2/5] name-rev: run clang-format before factoring code

From: Kristoffer Haugsbakk <code@khaugsbakk.name>

We are about to move code around to prepare for adding a new command. Let’s deal with clang-format changes first in the affected areas.

Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
---
 builtin/name-rev.c | 18 +++++++++---------
 1 file changed, 9 insertions(+), 9 deletions(-)
Show changes to builtin/name-rev.c +9 −9
diff --git a/builtin/name-rev.c b/builtin/name-rev.c
index 171e7bd0e98..6357eaa76d0 100644
--- a/builtin/name-rev.c
+++ b/builtin/name-rev.c
@@ -519,22 +519,22 @@ static void name_rev_line(char *p, struct name_ref_data *data)
 		if (!ishex(*p)) {
 			counter = 0;
 		} else if (++counter == hexsz &&
-			 !ishex(*(p+1))) {
+			   !ishex(*(p + 1))) {
 			struct object_id oid;
 			const char *name = NULL;
-			char c = *(p+1);
+			char c = *(p + 1);
 			int p_len = p - p_start + 1;
 
 			counter = 0;
 
-			*(p+1) = 0;
+			*(p + 1) = 0;
 			if (!repo_get_oid(the_repository, p - (hexsz - 1), &oid)) {
 				struct object *o =
 					lookup_object(the_repository, &oid);
 				if (o)
 					name = get_rev_name(o, &buf);
 			}
-			*(p+1) = c;
+			*(p + 1) = c;
 
 			if (!name)
 				continue;
@@ -571,9 +571,9 @@ int cmd_name_rev(int argc,
 		OPT_BOOL(0, "name-only", &data.name_only, N_("print only ref-based names (no object names)")),
 		OPT_BOOL(0, "tags", &data.tags_only, N_("only use tags to name the commits")),
 		OPT_STRING_LIST(0, "refs", &data.ref_filters, N_("pattern"),
-				   N_("only use refs matching <pattern>")),
+				N_("only use refs matching <pattern>")),
 		OPT_STRING_LIST(0, "exclude", &data.exclude_filters, N_("pattern"),
-				   N_("ignore refs matching <pattern>")),
+				N_("ignore refs matching <pattern>")),
 		OPT_GROUP(""),
 		OPT_BOOL(0, "all", &all, N_("list all commits reachable from all refs")),
 #ifndef WITH_BREAKING_CHANGES
@@ -585,10 +585,10 @@ int cmd_name_rev(int argc,
 #endif /* WITH_BREAKING_CHANGES */
 		OPT_BOOL(0, "annotate-stdin", &annotate_stdin, N_("annotate text from stdin")),
 		OPT_BOOL(0, "undefined", &allow_undefined, N_("allow to print `undefined` names (default)")),
-		OPT_BOOL(0, "always",     &always,
-			   N_("show abbreviated commit object as fallback")),
+		OPT_BOOL(0, "always", &always,
+			 N_("show abbreviated commit object as fallback")),
 		OPT_HIDDEN_BOOL(0, "peel-tag", &peel_tag,
-			   N_("dereference tags in the input (internal use)")),
+				N_("dereference tags in the input (internal use)")),
 		OPT_END(),
 	};
 
-- 
2.54.0.13.g9c7419e39f8
kristofferhaugsbakk@fastmail.comMay 11, 2026, 15:45 UTC in reply to kristofferhaugsbakk@fastmail.com on lore

[PATCH v5 3/5] name-rev: factor code for sharing with a new command

From: Kristoffer Haugsbakk <code@khaugsbakk.name>

We are about to introduce a new command git-format-rev(1) to this file. Let’s factor some code so that we can share it with the new command.

We want to be able to format commits found in freeform text, and git-name-rev(1) already has a function for that but for symbolic names. Let’s use a tagged union for the command-specific payload.

No functional changes.
Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
---
Notes (series):
    v4:
    • Pull out `*(p + 1)` instead of doing it in every `case` (Phillip)
    • Go back to using `continue` instead of `goto` (Phillip)
 builtin/name-rev.c | 53 ++++++++++++++++++++++++++++++++++------------
 1 file changed, 40 insertions(+), 13 deletions(-)
Show changes to builtin/name-rev.c +40 −13
diff --git a/builtin/name-rev.c b/builtin/name-rev.c
index 6357eaa76d0..475efb0b82b 100644
--- a/builtin/name-rev.c
+++ b/builtin/name-rev.c
@@ -272,6 +272,24 @@ struct name_ref_data {
 	struct string_list exclude_filters;
 };
 
+enum command_type {
+	NAME_REV = 1,
+};
+
+struct command {
+	enum command_type type;
+	union {
+		int name_only;
+	} u;
+};
+
+static void init_name_rev_command(struct command *cmd,
+				  int name_only)
+{
+	cmd->type = NAME_REV;
+	cmd->u.name_only = name_only;
+}
+
 static struct tip_table {
 	struct tip_table_entry {
 		struct object_id oid;
@@ -507,7 +525,7 @@ static char const * const name_rev_usage[] = {
 	NULL
 };
 
-static void name_rev_line(char *p, struct name_ref_data *data)
+static void name_rev_line(char *p, struct command *cmd)
 {
 	struct strbuf buf = STRBUF_INIT;
 	int counter = 0;
@@ -524,25 +542,32 @@ static void name_rev_line(char *p, struct name_ref_data *data)
 			const char *name = NULL;
 			char c = *(p + 1);
 			int p_len = p - p_start + 1;
+			struct object *o = NULL;
+			int oid_ret = 1;
 
 			counter = 0;
 
 			*(p + 1) = 0;
-			if (!repo_get_oid(the_repository, p - (hexsz - 1), &oid)) {
-				struct object *o =
-					lookup_object(the_repository, &oid);
+			oid_ret = repo_get_oid(the_repository, p - (hexsz - 1), &oid);
+			*(p + 1) = c;
+
+			switch (cmd->type) {
+			case NAME_REV:
+				if (!oid_ret)
+					o = lookup_object(the_repository, &oid);
 				if (o)
 					name = get_rev_name(o, &buf);
+				if (!name)
+					continue;
+				if (cmd->u.name_only)
+					printf("%.*s%s", p_len - hexsz, p_start, name);
+				else
+					printf("%.*s (%s)", p_len, p_start, name);
+				break;
+			default:
+				BUG("uncovered case: %d", cmd->type);
 			}
-			*(p + 1) = c;
-
-			if (!name)
-				continue;
 
-			if (data->name_only)
-				printf("%.*s%s", p_len - hexsz, p_start, name);
-			else
-				printf("%.*s (%s)", p_len, p_start, name);
 			p_start = p + 1;
 		}
 	}
@@ -567,6 +592,7 @@ int cmd_name_rev(int argc,
 #endif
 	int all = 0, annotate_stdin = 0, allow_undefined = 1, always = 0, peel_tag = 0;
 	struct name_ref_data data = { 0, 0, STRING_LIST_INIT_NODUP, STRING_LIST_INIT_NODUP };
+	struct command cmd;
 	struct option opts[] = {
 		OPT_BOOL(0, "name-only", &data.name_only, N_("print only ref-based names (no object names)")),
 		OPT_BOOL(0, "tags", &data.tags_only, N_("only use tags to name the commits")),
@@ -596,6 +622,7 @@ int cmd_name_rev(int argc,
 	init_commit_rev_name(&rev_names);
 	repo_config(the_repository, git_default_config, NULL);
 	argc = parse_options(argc, argv, prefix, opts, name_rev_usage, 0);
+	init_name_rev_command(&cmd, data.name_only);
 
 #ifndef WITH_BREAKING_CHANGES
 	if (transform_stdin) {
@@ -663,7 +690,7 @@ int cmd_name_rev(int argc,
 
 		while (strbuf_getline(&sb, stdin) != EOF) {
 			strbuf_addch(&sb, '\n');
-			name_rev_line(sb.buf, &data);
+			name_rev_line(sb.buf, &cmd);
 		}
 		strbuf_release(&sb);
 	} else if (all) {
-- 
2.54.0.13.g9c7419e39f8
kristofferhaugsbakk@fastmail.comMay 11, 2026, 15:45 UTC in reply to kristofferhaugsbakk@fastmail.com on lore

[PATCH v5 4/5] name-rev: make dedicated --annotate-stdin --name-only test

From: Kristoffer Haugsbakk <code@khaugsbakk.name>
The previous commit split the `--name-only` handling:
1. `--annotate-stdin`: uses the new `struct command`
2. The rest: uses `struct name_ref_data`

But there is no dedicated test for the option combination in (1). That means that the following tests will fail if you neglect to set `command.u.name_only` properly:

    name-rev --annotate-stdin works with commitGraph
    name-rev --annotate-stdin works with non-monotonic timestamps

even though it has nothing to do with what these tests are supposed to test.

Let’s add another regression test now that it is relevant.
Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
---
 t/t6120-describe.sh | 14 ++++++++++++++
 1 file changed, 14 insertions(+)
Show changes to t/t6120-describe.sh +14 −0
diff --git a/t/t6120-describe.sh b/t/t6120-describe.sh
index 2c70cc561ad..62789f76381 100755
--- a/t/t6120-describe.sh
+++ b/t/t6120-describe.sh
@@ -298,6 +298,20 @@ test_expect_success 'name-rev --annotate-stdin' '
 	test_cmp expect actual
 '
 
+test_expect_success 'name-rev --annotate-stdin --name-only' '
+	>expect.unsorted &&
+	for rev in $(git rev-list --all)
+	do
+		name=$(git name-rev --name-only $rev) &&
+		echo "$name" >>expect.unsorted || return 1
+	done &&
+	sort <expect.unsorted >expect &&
+	git name-rev --annotate-stdin --name-only \
+		<list >actual.unsorted &&
+	sort <actual.unsorted >actual &&
+	test_cmp expect actual
+'
+
 test_expect_success 'name-rev --stdin deprecated' '
 	git rev-list --all >list &&
 	if ! test_have_prereq WITH_BREAKING_CHANGES
-- 
2.54.0.13.g9c7419e39f8
kristofferhaugsbakk@fastmail.comMay 11, 2026, 15:45 UTC in reply to kristofferhaugsbakk@fastmail.com on lore

[PATCH v5 5/5] format-rev: introduce builtin for on-demand pretty formatting

From: Kristoffer Haugsbakk <code@khaugsbakk.name>

Introduce a new builtin for pretty formatting one revision expression per line or commit object names found in running text.

Sometimes you want to format commits. Most of the time you’re walking the graph, e.g. getting a range of commits like `master..topic`. That’s a job for git-log(1).

But there are times when you want to format commits that you encounter on demand:

• Full hashes in running text that you might want to pretty-print
• git-last-modified(1) outputs full hashes that you can do the same
  with
• git-cherry(1) has `-v` for commit subject, but maybe you want
  something else?
But now you can’t use git-log(1), git-show(1), or git-rev-list(1):
• You can’t feed commits piecemeal to these commands, one input
  for one output; they block until standard in is closed
• You can’t feed a list of possibly duplicate commits, like the output
  of git-last-modified(1); they effectively deduplicate the output

Beyond these two points there’s also the input massage problem: you cannot feed mixed input (revisions mixed with arbitrary text).

One might hope that git-cat-file(1) can save us. But it doesn’t support pretty formats.

But there is one command that already both handles revisions as arguments, revisions on standard input, and even revisions mixed in with arbitrary text. Namely git-name-rev(1): the command for outputting symbolic names for commits.

We made some room in `builtin/name-rev.c` two commits ago. Let’s now add this new git-format-rev(1) command. Taking inspiration from git-name-rev(1), there are two modes:

• revs: like git-name-rev(1) in argv mode, but one revision per line
  on standard in
• text: like git-name-rev(1) with `--annotate-stdin`
***

We need to add this command to the exception list in `t/t1517-outside-repo.sh` because it uses “EXPERIMENTAL!” in the usage line.

Helped-by: Phillip Wood <phillip.wood@dunelm.org.uk>
Helped-by: Ramsay Jones <ramsay@ramsayjones.plus.com>
Helped-by: Junio C Hamano <gitster@pobox.com>
Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
---
Notes (series):
    v5:
    • (All doc changes)
    • Fix definition list mistake
      • No blank line between the two modes
      • Also replace `::` delimiters with `;;`. This is not strictly
        needed since the definition list is inside an open block. But
        it is consistent with all other definition list in definition
        lists I’ve seen. (The command option section is a long
        definition list.)
    • Rewrite part that I was unhappy about regarding
      `--stdin-mode=text` and `-z`:
    
        https://lore.kernel.org/git/c04d9cf9-e6a9-4e12-8025-9baededfdafc@app.fastmail.com/
    • Replace `abcdef012...` with just “some object name”. The object
      name is arbitrary and my AsciiDoc output seems to do some weird
      things for `...` inside backticks for HTML.
    • Replace “this problem is just another” with “this problem
      *is solved* with...”
    
    ***
    
    v4:
    • Squash in “SQUASH???” commits from Junio’s branch:
      • Fix -Werror=maybe-uninitialized at builtin/name-rev.c:893:25
      • Add this new builtin to Meson build file as well
    • Flush every output line/record with `maybe_flush_or_die(...)` (Phillip)
    • Introduce `-z` and other NUL output options (Phillip)
    • Tests:
      • More tests: “name lookup failures” (This would have caught my
        uninitialized mistake from v3)
      • More tests: for NUL/LF termination
      • Fix stale setup command which uses “name-rev” in the name
      • Use `setup:` prefix for both (now two) setup tests
      • Drop some `:/fifth` expressions in favor just `fifth` (the tag from
      `test_commit`)
    • Flesh out documentation
    • --stdin-mode=revs: Replace “Could not get [sha1][object name]” since the
      user could be using SHA256
      • This error string was originally stolen from name-rev
    • Remember to add this command to `.gitignore` this time
    • Correct doc to `git [show][log] --no-walk -1` (next to `git show`)
    • Plus some minor things!
    v3:
    • And don’t forget to document --notes this time
    
          https://lore.kernel.org/git/CALnO6CB5WOTp_e7Kv3CrEbQ+3XE-gDxNVHf7qATBEbyKWfxpLg@mail.gmail.com/
 .gitignore                        |   1 +
 Documentation/git-format-rev.adoc | 215 ++++++++++++++++++++++++++++
 Documentation/meson.build         |   1 +
 Makefile                          |   1 +
 builtin.h                         |   1 +
 builtin/name-rev.c                | 223 ++++++++++++++++++++++++++++++
 command-list.txt                  |   1 +
 git.c                             |   1 +
 t/t1517-outside-repo.sh           |   3 +-
 t/t6120-describe.sh               | 194 ++++++++++++++++++++++++++
 10 files changed, 640 insertions(+), 1 deletion(-)
 create mode 100644 Documentation/git-format-rev.adoc
Show changes to 10 files +640 −1

.gitignore, Documentation/git-format-rev.adoc, Documentation/meson.build, Makefile, builtin.h, builtin/name-rev.c, command-list.txt, git.c, t/t1517-outside-repo.sh, t/t6120-describe.sh

diff --git a/.gitignore b/.gitignore
index 24635cf2d6f..e406d3741cd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -71,6 +71,7 @@
 /git-for-each-ref
 /git-for-each-repo
 /git-format-patch
+/git-format-rev
 /git-fsck
 /git-fsck-objects
 /git-fsmonitor--daemon
diff --git a/Documentation/git-format-rev.adoc b/Documentation/git-format-rev.adoc
new file mode 100644
index 00000000000..c40d52e9f6d
--- /dev/null
+++ b/Documentation/git-format-rev.adoc
@@ -0,0 +1,215 @@
+git-format-rev(1)
+=================
+
+NAME
+----
+git-format-rev - EXPERIMENTAL: Pretty format revisions on demand
+
+
+SYNOPSIS
+--------
+[synopsis]
+(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> --format=<pretty> [--[no-]notes=<ref>] [-z] [--[no-]null-output] [--[no-]null-input]
+
+DESCRIPTION
+-----------
+
+Pretty format revisions from standard input.
+
+THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
+
+OPTIONS
+-------
+
+`--stdin-mode=<mode>`::
+	How to interpret standard input data:
++
+--
+`revs`;; Each line or record (see the <<io,INPUT AND OUTPUT FORMATS>>
+	section) is interpreted as a commit. Any kind of revision
+	expression can be used (see linkgit:gitrevisions[7]). Annotated
+	tags are peeled (see linkgit:gitglossary[7]).
++
+The argument `rev` is also accepted.
+
+`text`;; Formats all commit object names found in freeform text. These
+	must the full object names, i.e. abbreviated hexidecimal object
+	names will not be interpreted.
++
+Anything that is parsed as an object name but that is not found to be a
+commit object name is left alone (echoed).
+--
+
+`--format=<pretty>`::
+	Pretty format string.
+
+`--notes=<ref>`::
+`--no-notes`::
+	Custom notes ref. Notes are displayed when using the `%N`
+	atom. See linkgit:git-notes[1].
+
+`-z`::
+`--null`::
+	Use _NUL_ character to terminate both input and output instead
+	of newline. This option cannot be negated.
++
+This is useful if both the input and output could contain newlines or if
+the input to this command also uses _NUL_ character termination; see the
+<<io,INPUT AND OUTPUT FORMATS>> section below.
++
+The mode `--stdin-mode=text` can have use for this option when it needs
+to process input like for example `git last-modified -z`; see the
+<<examples,EXAMPLES>> section below.
+
+`--null-output`::
+`--no-null-output`::
+	Use _NUL_ character to terminate output instead of newline. The
+	default is `--no-null-output`.
++
+This is useful if the output could contain newlines, for example if the
+`%n` (newline) atom is used.
+
+`--null-input`::
+`--no-null-input`::
+	Use _NUL_ character to terminate input instead of newline. The
+	default is `--no-null-input`.
++
+This is useful if the input revision expressions could contain newlines.
+
+[[io]]
+INPUT AND OUTPUT FORMAT
+-----------------------
+
+The command uses newlines for both input and output termination by
+default. See the `-z`, `--null-output`, and `--null-input` options for
+using _NUL_ character as the terminator.
+
+The mode `--stdin-mode=revs` outputs one formatted commit followed by
+the terminator. This could either be called a _line_ or a _record_ in
+case "line" is too suggestive of newline termination.
+
+Note that this means that the terminator character (newline or _NUL_)
+acts as a _terminator_, not a _separator_. In other words, the final
+line or record is also terminated by the terminator character.
+
+The mode `--stdin-mode=text` replaces each object name with the
+formatted commit, i.e. the format `%s` would transform some commit
+object name to `<subject>` without any termination. Like this:
+
+----
+Did we not fix this in "<subject>"?
+----
+
+It is safe to interactively read and write from this command since each
+record is immediately flushed.
+
+[[examples]]
+EXAMPLES
+--------
+
+The command linkgit:git-last-modified[1] shows the commit that each file
+was last modified in.
+
+----
+$ git last-modified -- README.md Makefile
+7798034171030be0909c56377a4e0e10e6d2df93	Makefile
+c50fbb2dd225e7e82abba4380423ae105089f4d7	README.md
+----
+
+We can pipe the result to this command in order to replace the object
+name with the commit author.
+
+----
+$ git last-modified -- README.md Makefile |
+    git format-rev --stdin-mode=text --format=%an
+Junio C Hamano	Makefile
+Todd Zullinger	README.md
+----
+
+Another example is _formatting commits in commit messages_. Given this commit message:
+
+----
+Fix off-by-one error
+
+Fix off-by-one error introduced in
+e83c5163316f89bfbde7d9ab23ca2e25604af290.
+
+We thought we fixed this in 5569bf9bbedd63a00780fc5c110e0cfab3aa97b9 but
+that only covered 1/3 of the faulty cases.
+----
+
+We can format the commits and use par(1) to reflow the text, say in a
+`commit-msg` hook:
+
+----
+$ git config set hook.reference-commits.event commit-msg
+$ git config set hook.reference-commits.command reference-commits
+$ cat $(which reference-commits)
+#/bin/sh
+
+msg="$1"
+rewritten=$(mktemp)
+git format-rev --stdin-mode=text --format=reference <"$msg" |
+    par >"$rewritten"
+mv "$rewritten" "$msg"
+----
+
+Which will produce something like this:
+
+----
+Fix off-by-one error
+
+Fix off-by-one error introduced in e83c5163316 (Implement better memory
+allocator, 2005-04-07).
+
+We thought we fixed this in 5569bf9bbed (Fix memory allocator,
+2005-06-22) but that only covered 1/3 of the faulty cases.
+----
+
+DISCUSSION
+----------
+
+This command lets you format any number of revisions in any order
+through one command invocation. Consider the
+linkgit:git-last-modified[1] case from the <<examples,EXAMPLES>> section
+above:
+
+1. There might be hundreds of files
+2. Commits can be repeated, i.e. two or more files were last modified in
+   the same commit
+
+Two widely-used commands which pretty formats commits are
+linkgit:git-log[1] and linkgit:git-show[1]. It turns out that they are
+not a good fit for the above use case.
+
+- The output of linkgit:git-last-modified[1] would have to be processed
+  in stages since you need to transform the first column separately and
+  then link the author to the filename. But this is surmountable.
+- You can feed each commit to `git show` or `git log --no-walk -1`. But
+  that means that you need to create a process for each line.
+- Let’s say that you want to use one process, not one per line. So you
+  want to feed all the commits to the command. Now you face the problem
+  that you have to feed all the commits to the commands before you get
+  any output (this is also the case for the `--stdin` modes). In other
+  words, you cannot loop through each line, get the author for the
+  commit, and output the author and the filename. You need to feed all
+  the commits, get back all the output, and match the output with the
+  filename.
+- But the next problem is that commands will deduplicate the input and
+  only output one commit one single time only. Thus you cannot make the
+  output order match the input order, since a commit could have been
+  repeated in the original input.
+
+In short, it is straightforward to use these two commands if you use one
+process per line. It is much more work if you just want to use one
+process, but still doable. In contrast, this problem is solved with just
+another shell pipeline with this command.
+
+SEE ALSO
+--------
+linkgit:git-name-rev[1],
+linkgit:git-log[1].
+
+GIT
+---
+Part of the linkgit:git[1] suite
diff --git a/Documentation/meson.build b/Documentation/meson.build
index d6365b888bb..58e7c6a0b8a 100644
--- a/Documentation/meson.build
+++ b/Documentation/meson.build
@@ -55,6 +55,7 @@ manpages = {
   'git-for-each-ref.adoc' : 1,
   'git-for-each-repo.adoc' : 1,
   'git-format-patch.adoc' : 1,
+  'git-format-rev.adoc' : 1,
   'git-fsck-objects.adoc' : 1,
   'git-fsck.adoc' : 1,
   'git-fsmonitor--daemon.adoc' : 1,
diff --git a/Makefile b/Makefile
index 15b1ded1a0b..cbaf91fd846 100644
--- a/Makefile
+++ b/Makefile
@@ -895,6 +895,7 @@ BUILT_INS += $(patsubst builtin/%.o,git-%$X,$(BUILTIN_OBJS))
 BUILT_INS += git-cherry$X
 BUILT_INS += git-cherry-pick$X
 BUILT_INS += git-format-patch$X
+BUILT_INS += git-format-rev$X
 BUILT_INS += git-fsck-objects$X
 BUILT_INS += git-init$X
 BUILT_INS += git-maintenance$X
diff --git a/builtin.h b/builtin.h
index 235c51f30e5..63813c90125 100644
--- a/builtin.h
+++ b/builtin.h
@@ -189,6 +189,7 @@ int cmd_fmt_merge_msg(int argc, const char **argv, const char *prefix, struct re
 int cmd_for_each_ref(int argc, const char **argv, const char *prefix, struct repository *repo);
 int cmd_for_each_repo(int argc, const char **argv, const char *prefix, struct repository *repo);
 int cmd_format_patch(int argc, const char **argv, const char *prefix, struct repository *repo);
+int cmd_format_rev(int argc, const char **argv, const char *prefix, struct repository *repo);
 int cmd_fsck(int argc, const char **argv, const char *prefix, struct repository *repo);
 int cmd_fsmonitor__daemon(int argc, const char **argv, const char *prefix, struct repository *repo);
 int cmd_gc(int argc, const char **argv, const char *prefix, struct repository *repo);
diff --git a/builtin/name-rev.c b/builtin/name-rev.c
index 475efb0b82b..5494b0424b3 100644
--- a/builtin/name-rev.c
+++ b/builtin/name-rev.c
@@ -18,6 +18,10 @@
 #include "commit-graph.h"
 #include "wildmatch.h"
 #include "mem-pool.h"
+#include "pretty.h"
+#include "revision.h"
+#include "notes.h"
+#include "write-or-die.h"
 
 /*
  * One day.  See the 'name a rev shortly after epoch' test in t6120 when
@@ -272,14 +276,26 @@ struct name_ref_data {
 	struct string_list exclude_filters;
 };
 
+struct pretty_format {
+	struct pretty_print_context ctx;
+	struct userformat_want want;
+};
+
 enum command_type {
 	NAME_REV = 1,
+	FORMAT_REV = 2,
+};
+
+enum stdin_mode {
+    TEXT = 1,
+    REVS = 2,
 };
 
 struct command {
 	enum command_type type;
 	union {
 		int name_only;
+		struct pretty_format *pretty_format;
 	} u;
 };
 
@@ -290,6 +306,13 @@ static void init_name_rev_command(struct command *cmd,
 	cmd->u.name_only = name_only;
 }
 
+static void init_format_rev_command(struct command *cmd,
+				    struct pretty_format *pretty_format)
+{
+	cmd->type = FORMAT_REV;
+	cmd->u.pretty_format = pretty_format;
+}
+
 static struct tip_table {
 	struct tip_table_entry {
 		struct object_id oid;
@@ -495,6 +518,27 @@ static const char *get_rev_name(const struct object *o, struct strbuf *buf)
 	}
 }
 
+static const char *get_format_rev(const struct commit *c,
+				  struct pretty_format *format_ctx,
+				  struct strbuf *buf)
+{
+	strbuf_reset(buf);
+
+	if (format_ctx->want.notes) {
+		struct strbuf notebuf = STRBUF_INIT;
+
+		format_display_notes(&c->object.oid, &notebuf,
+				     get_log_output_encoding(),
+				     format_ctx->ctx.fmt == CMIT_FMT_USERFORMAT);
+		format_ctx->ctx.notes_message = strbuf_detach(&notebuf, NULL);
+	}
+
+	pretty_print_commit(&format_ctx->ctx, c, buf);
+	FREE_AND_NULL(format_ctx->ctx.notes_message);
+
+	return buf->buf;
+}
+
 static void show_name(const struct object *obj,
 		      const char *caller_name,
 		      int always, int allow_undefined, int name_only)
@@ -564,6 +608,18 @@ static void name_rev_line(char *p, struct command *cmd)
 				else
 					printf("%.*s (%s)", p_len, p_start, name);
 				break;
+			case FORMAT_REV:
+				if (!oid_ret)
+					o = parse_object(the_repository, &oid);
+				if (o && o->type == OBJ_COMMIT)
+					name = get_format_rev((const struct commit *)o,
+							      cmd->u.pretty_format,
+							      &buf);
+				if (name)
+					printf("%.*s%s", p_len - hexsz, p_start, name);
+				else
+					printf("%.*s", p_len, p_start);
+				break;
 			default:
 				BUG("uncovered case: %d", cmd->type);
 			}
@@ -717,3 +773,170 @@ int cmd_name_rev(int argc,
 	object_array_clear(&revs);
 	return 0;
 }
+
+struct format_nul_data {
+	bool nul_input;
+	bool nul_output;
+};
+
+static int format_nul_cb(const struct option *option,
+			 const char *arg,
+			 int unset)
+{
+	struct format_nul_data *data = option->value;
+	data->nul_input = 1;
+	data->nul_output = 1;
+	BUG_ON_OPT_NEG(unset);
+	BUG_ON_OPT_ARG(arg);
+	return 0;
+}
+
+static enum stdin_mode parse_stdin_mode(const char *stdin_mode)
+{
+	if (!strcmp(stdin_mode, "text"))
+		return TEXT;
+	else if (!strcmp(stdin_mode, "revs") ||
+		 !strcmp(stdin_mode, "rev"))
+		return REVS;
+	else
+		die(_("'%s' needs to be either text, revs, or rev"),
+		    "--stdin-mode");
+}
+
+static char const *const format_rev_usage[] = {
+	N_("(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> "
+	   "--format=<pretty> [--[no-]notes=<ref>] "
+	   "[-z] [--[no-]null-output] [--[no-]null-input]"),
+	NULL
+};
+
+int cmd_format_rev(int argc,
+		   const char **argv,
+		   const char *prefix,
+		   struct repository *repo UNUSED)
+{
+	const char *format = NULL;
+	enum stdin_mode stdin_mode;
+	const char *stdin_mode_arg = NULL;
+	struct format_nul_data nul_data = { 0, 0 };
+	char output_terminator;
+	strbuf_getline_fn getline_fn;
+	struct display_notes_opt format_notes_opt;
+	struct rev_info format_rev = REV_INFO_INIT;
+	struct pretty_format format_pp = { 0 };
+	struct string_list notes = STRING_LIST_INIT_NODUP;
+	struct strbuf scratch_buf = STRBUF_INIT;
+	struct command cmd;
+	struct option opts[] = {
+		OPT_STRING(0, "format", &format, N_("format"),
+			   N_("pretty format to use")),
+		OPT_STRING(0, "stdin-mode", &stdin_mode_arg, N_("stdin-mode"),
+			   N_("how revs are processed")),
+		OPT_STRING_LIST(0, "notes", &notes, N_("notes"),
+				N_("display notes for pretty format")),
+		OPT_CALLBACK_F('z', "null", &nul_data, N_("z"),
+			       N_("Use NUL for input and output termination"),
+			       PARSE_OPT_NOARG | PARSE_OPT_NONEG, format_nul_cb),
+		OPT_BOOL(0, "null-input", &nul_data.nul_input,
+			 N_("Use NUL for input termination")),
+		OPT_BOOL(0, "null-output", &nul_data.nul_output,
+			 N_("Use NUL for output termination")),
+		OPT_END(),
+	};
+
+	argc = parse_options(argc, argv, prefix, opts, format_rev_usage, 0);
+
+	if (argc > 0) {
+		error(_("too many arguments"));
+		usage_with_options(format_rev_usage, opts);
+	}
+
+	if (!format)
+		die(_("'%s' is required"), "--format");
+	if (!stdin_mode_arg)
+		die(_("'%s' is required"), "--stdin-mode");
+
+	getline_fn = nul_data.nul_input ? strbuf_getline_nul : strbuf_getline_lf;
+	output_terminator = nul_data.nul_output ? '\0' : '\n';
+
+	init_display_notes(&format_notes_opt);
+	stdin_mode = parse_stdin_mode(stdin_mode_arg);
+
+	get_commit_format(format, &format_rev);
+	format_pp.ctx.rev = &format_rev;
+	format_pp.ctx.fmt = format_rev.commit_format;
+	format_pp.ctx.abbrev = format_rev.abbrev;
+	format_pp.ctx.date_mode_explicit = format_rev.date_mode_explicit;
+	format_pp.ctx.date_mode = format_rev.date_mode;
+	format_pp.ctx.color = GIT_COLOR_AUTO;
+
+	userformat_find_requirements(format,
+				     &format_pp.want);
+	if (format_pp.want.notes) {
+		int ignore_show_notes = 0;
+		struct string_list_item *n;
+
+		for_each_string_list_item(n, &notes)
+			enable_ref_display_notes(&format_notes_opt,
+						 &ignore_show_notes,
+						 n->string);
+		load_display_notes(&format_notes_opt);
+	}
+
+	init_format_rev_command(&cmd, &format_pp);
+
+	switch (stdin_mode) {
+	case TEXT:
+		while (getline_fn(&scratch_buf, stdin) != EOF) {
+			name_rev_line(scratch_buf.buf, &cmd);
+			/*
+			 * We do not pass on the terminator to name_rev_line,
+			 * unlike name-rev.
+			 */
+			printf("%c", output_terminator);
+			maybe_flush_or_die(stdout, "stdout");
+		}
+		break;
+	case REVS:
+		while (getline_fn(&scratch_buf, stdin) != EOF) {
+			struct object_id oid;
+			struct object *object;
+			struct object *peeled;
+
+			if (repo_get_oid(the_repository, scratch_buf.buf, &oid)) {
+				fprintf(stderr, "Could not get object name for %s. Skipping.\n",
+					scratch_buf.buf);
+				continue;
+			}
+
+			object = parse_object(the_repository, &oid);
+			if (!object) {
+				fprintf(stderr, "Could not get object for %s. Skipping.\n",
+					scratch_buf.buf);
+				continue;
+			}
+
+			peeled = deref_tag(the_repository, object, scratch_buf.buf, 0);
+			if (!peeled || peeled->type != OBJ_COMMIT) {
+				fprintf(stderr,
+					"Could not get commit for %s. Skipping.\n",
+					scratch_buf.buf);
+				continue;
+			}
+
+			get_format_rev((struct commit *)peeled,
+				       &format_pp, &scratch_buf);
+			printf("%s%c", scratch_buf.buf, output_terminator);
+			maybe_flush_or_die(stdout, "stdout");
+			strbuf_release(&scratch_buf);
+		}
+		break;
+	default:
+		BUG("uncovered case: %d", stdin_mode);
+	}
+
+	strbuf_release(&scratch_buf);
+	string_list_clear(&notes, 0);
+	release_display_notes(&format_notes_opt);
+	return 0;
+}
diff --git a/command-list.txt b/command-list.txt
index f9005cf4597..df729872dca 100644
--- a/command-list.txt
+++ b/command-list.txt
@@ -108,6 +108,7 @@ git-fmt-merge-msg                       purehelpers
 git-for-each-ref                        plumbinginterrogators
 git-for-each-repo                       plumbinginterrogators
 git-format-patch                        mainporcelain
+git-format-rev                          plumbinginterrogators
 git-fsck                                ancillaryinterrogators          complete
 git-gc                                  mainporcelain
 git-get-tar-commit-id                   plumbinginterrogators
diff --git a/git.c b/git.c
index 2b212e6675d..af5b0422b00 100644
--- a/git.c
+++ b/git.c
@@ -578,6 +578,7 @@ static struct cmd_struct commands[] = {
 	{ "for-each-ref", cmd_for_each_ref, RUN_SETUP },
 	{ "for-each-repo", cmd_for_each_repo, RUN_SETUP_GENTLY },
 	{ "format-patch", cmd_format_patch, RUN_SETUP },
+	{ "format-rev", cmd_format_rev, RUN_SETUP },
 	{ "fsck", cmd_fsck, RUN_SETUP },
 	{ "fsck-objects", cmd_fsck, RUN_SETUP },
 	{ "fsmonitor--daemon", cmd_fsmonitor__daemon, RUN_SETUP },
diff --git a/t/t1517-outside-repo.sh b/t/t1517-outside-repo.sh
index c824c1a25cf..360a9323343 100755
--- a/t/t1517-outside-repo.sh
+++ b/t/t1517-outside-repo.sh
@@ -114,7 +114,8 @@ do
 	archimport | citool | credential-netrc | credential-libsecret | \
 	credential-osxkeychain | cvsexportcommit | cvsimport | cvsserver | \
 	daemon | \
-	difftool--helper | filter-branch | fsck-objects | get-tar-commit-id | \
+	difftool--helper | filter-branch | format-rev | fsck-objects | \
+	get-tar-commit-id | \
 	gui | gui--askpass | \
 	http-backend | http-fetch | http-push | init-db | \
 	merge-octopus | merge-one-file | merge-resolve | mergetool | \
diff --git a/t/t6120-describe.sh b/t/t6120-describe.sh
index 62789f76381..8ee3d2c37d0 100755
--- a/t/t6120-describe.sh
+++ b/t/t6120-describe.sh
@@ -801,4 +801,198 @@ test_expect_success 'do not be fooled by invalid describe format ' '
 	test_must_fail git cat-file -t "refs/tags/super-invalid/./../...../ ~^:/?*[////\\\\\\&}/busted.lock-42-g"$(cat out)
 '
 
+test_expect_success 'setup: format-rev' '
+	mkdir repo-format &&
+	git -C repo-format init &&
+	test_commit -C repo-format first &&
+	test_commit -C repo-format second &&
+	test_commit -C repo-format third &&
+	test_commit -C repo-format fourth &&
+	test_commit -C repo-format fifth &&
+	test_commit -C repo-format sixth &&
+	test_commit -C repo-format seventh &&
+	test_commit -C repo-format eighth
+'
+
+test_expect_success 'format-rev --stdin-mode=revs' '
+	cat >expect <<-\EOF &&
+	eighth
+	seventh
+	fifth
+	EOF
+	git -C repo-format format-rev --stdin-mode=revs \
+		--format=%s >actual <<-\EOF &&
+	HEAD
+	HEAD~
+	HEAD~3
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success 'format-rev --stdin-mode=text from rev-list same as log' '
+	git -C repo-format log --format=reference >expect &&
+	test_file_not_empty expect &&
+	git -C repo-format rev-list HEAD >list &&
+	git -C repo-format format-rev --stdin-mode=text \
+		--format=reference <list >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'format-rev --stdin-mode=text with running text and tree oid' '
+	cmit_oid=$(git -C repo-format rev-parse fifth) &&
+	reference=$(git -C repo-format log -n1 --format=reference fifth) &&
+	tree=$(git -C repo-format rev-parse HEAD^{tree}) &&
+	cat >expect <<-EOF &&
+	We thought we fixed this in ${reference}.
+	But look at this tree: ${tree}.
+	EOF
+	git -C repo-format format-rev --stdin-mode=text --format=reference \
+		>actual <<-EOF &&
+	We thought we fixed this in ${cmit_oid}.
+	But look at this tree: ${tree}.
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success 'format-rev with %N (note)' '
+	test_when_finished "git -C repo-format notes remove" &&
+	git -C repo-format notes add -m"Make a note" &&
+	printf "Make a note\n\n\n" >expect &&
+	git -C repo-format format-rev --stdin-mode=revs \
+		--format="tformat:%N" \
+		>actual <<-\EOF &&
+	HEAD
+	HEAD~
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success 'format-rev --notes<ref> (custom notes ref)' '
+	# One custom notes ref
+	test_when_finished "git -C repo-format notes remove" &&
+	test_when_finished "git -C repo-format notes --ref=word remove" &&
+	git -C repo-format notes add -m"default" &&
+	git -C repo-format notes --ref=word add -m"custom" &&
+	printf "custom\n\n" >expect &&
+	git -C repo-format format-rev --stdin-mode=revs \
+		--format="tformat:%N" \
+		--notes=word \
+		>actual <<-\EOF &&
+	HEAD
+	EOF
+	test_cmp expect actual &&
+	# Glob all
+	printf "default\ncustom\n\n" >expect &&
+	git -C repo-format format-rev --stdin-mode=revs \
+		--format="tformat:%N" \
+		--notes=* >actual <<-\EOF &&
+	HEAD
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success 'format-rev --stdin-mode=revs on annotated tag peels to commit' '
+	test_when_finished "git -C repo-format tag -d version" &&
+	git -C repo-format tag -a -m"new version" version &&
+	cat >expect <<-\EOF &&
+	eighth
+	EOF
+	git -C repo-format format-rev --stdin-mode=revs \
+		--format=%s \
+		>actual <<-\EOF &&
+	version
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success 'format-rev --stdin-mode=revs lookup failures' '
+	test_when_finished "git -C repo-format tag -d tag-to-tree" &&
+	invalid_syntax=not-valid &&
+	non_existing_oid=${EMPTY_BLOB} &&
+	tree=$(git -C repo-format rev-parse eighth^{tree}) &&
+	git -C repo-format tag -a -mmessage tag-to-tree "$tree" &&
+	tag_to_tree=$(git -C repo-format rev-parse tag-to-tree) &&
+	cat >expect <<-EOF &&
+	Could not get object name for ${invalid_syntax}. Skipping.
+	Could not get object for ${non_existing_oid}. Skipping.
+	Could not get commit for ${tree}. Skipping.
+	Could not get commit for ${tag_to_tree}. Skipping.
+	EOF
+	git -C repo-format format-rev --stdin-mode=revs \
+		--format=%s \
+		2>actual >out <<-EOF &&
+	${invalid_syntax}
+	${non_existing_oid}
+	${tree}
+	${tag_to_tree}
+	EOF
+	test_line_count = 0 out &&
+	test_cmp expect actual
+'
+
+
+test_expect_success 'format-rev -z --stdin-mode=text with object name lookup failures' '
+	printf "%s\0" "$(git -C repo-format rev-parse HEAD)" >input &&
+	printf "%s\0" "$(git -C repo-format rev-parse HEAD^{tree})" >>input &&
+	printf "%s\0" "$EMPTY_BLOB" >>input &&
+	printf "%s\0" "$(git -C repo-format log --format=%s -1)" >expect &&
+	printf "%s\0" "$(git -C repo-format rev-parse HEAD^{tree})" >>expect &&
+	printf "%s\0" "$EMPTY_BLOB" >>expect &&
+	git -C repo-format format-rev --stdin-mode=text \
+		--format=%s -z <input >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'setup: format-rev input and output separators' '
+	git -C repo-format rev-list HEAD >input-lf &&
+	git -C repo-format rev-list -z HEAD >input-nul &&
+	git -C repo-format log --format=%s >output-lf &&
+	git -C repo-format log -z --format=%s >output-nul &&
+	echo revs >stdin-modes &&
+	echo text >>stdin-modes
+'
+
+while read mode
+do
+	test_expect_success "format-rev -z --stdin-mode=$mode" '
+		cat output-nul >expect &&
+		git -C repo-format format-rev --stdin-mode="$mode" \
+			--format=%s -z <input-nul >actual &&
+		test_cmp expect actual
+	'
+
+	test_expect_success "format-rev -z --no-null-input --no-null-output --stdin-mode=$mode" '
+		cat output-lf >expect &&
+		git -C repo-format format-rev --stdin-mode="$mode" \
+			--format=%s -z --no-null-input --no-null-output \
+			<input-lf >actual &&
+		test_cmp expect actual
+	'
+
+	test_expect_success "format-rev ---null-input --stdin-mode=$mode" '
+		cat output-lf >expect &&
+		git -C repo-format format-rev --stdin-mode="$mode" \
+			--format=%s --null-input \
+			<input-nul >actual &&
+		test_cmp expect actual
+	'
+
+	test_expect_success "format-rev --null-output --stdin-mode=$mode" '
+		cat output-nul >expect &&
+		git -C repo-format format-rev --stdin-mode="$mode" \
+			--format=%s --null-output \
+			<input-lf >actual &&
+		test_cmp expect actual
+	'
+
+	test_expect_success "format-rev -z --stdin-mode=$mode with multi-line output" '
+		format="%s%n%aI" &&
+		git -C repo-format log -z --format="$format" \
+			>expect &&
+		git -C repo-format format-rev --stdin-mode="$mode" \
+			--format="$format" -z <input-nul >actual &&
+		test_cmp expect actual
+	'
+done <stdin-modes
+
 test_done
-- 
2.54.0.13.g9c7419e39f8

Back to recent threads