{"thread":{"id":"65237","subject":"[PATCH 0/2] name-rev: learn --format=<pretty>","startedAt":"2026-03-13T16:04:26Z","lastAt":"2026-05-11T15:47:43Z","messageCount":45,"participants":["kristofferhaugsbakk@fastmail.com","Junio C Hamano","Kristoffer Haugsbakk","D. Ben Knoble","Phillip Wood"],"isPatch":true,"patchVersion":1,"patchTotal":2},"messages":[{"id":"538908","messageId":"CV_name-rev_--format.4ad@msgid.xyz","threadId":"65237","inReplyTo":null,"subject":"[PATCH 0/2] name-rev: learn --format=<pretty>","fromName":"","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-03-13T16:03:36Z","receivedAt":"2026-03-13T16:04:26Z","isPatch":true,"body":"From: Kristoffer Haugsbakk <code@khaugsbakk.name>\n\nTopic name: kh/name-rev-pretty-format\n\nTopic summary: Teach git-name-rev(1) a mode to pretty format revisions\ninstead of outputting symbolic names.\n\n(See the second patch for details.)\n\nThe first patch is just for `CodingGuidelines`. Unrelated.\n\nI have gone through three approaches: pretty print, `log-tree:\nlog_tree_commit`, and pretty print again. At first I was confused\nwhen using `pretty_print_commit` because I couldn’t seem to get the same\noutput as git-log(1):\n\n    # for git-log(1)\n    git log --format=<pretty>\n    # for git-name-rev(1)\n    git rev-list HEAD |\n        git name-rev --format=<pretty> --annotate-stdin\n\nBecause there were some minor differences for a few things I tried:\n\n• fuller: log has the `commit <commit>` header; pretty does not\n• oneline: log has the oid; pretty does not\n\nThen I tried `log_tree_commit`. But then I got some things that I didn’t\nwant. This function also didn’t fit in with the git-name-rev(1) processing\nsince it just dumps straight to standard out instead of allowing you to\naccumulate things in a scratch buffer (strbuf). So then I went back to the\n`pretty_print_commit` approach.\n\nNotes are handled by reading the display refs. I can imagine that it would\nbe better for this command to use `--[no-]notes` arguments like the ones\nthat git-log(1) has for explicit control (without using env. variables).\n\n[1/2] name-rev: wrap both blocks in braces\n[2/2] name-rev: learn --format=<pretty>\n\n Documentation/git-name-rev.adoc |  9 +++-\n builtin/name-rev.c              | 85 ++++++++++++++++++++++++++++-----\n t/t6120-describe.sh             | 58 ++++++++++++++++++++++\n 3 files changed, 139 insertions(+), 13 deletions(-)\n\n\nbase-commit: 67006b9db8b772423ad0706029286096307d2567\n-- \n2.53.0.32.gf6228eaf9cc\n\n"},{"id":"538909","messageId":"name-rev_braces.4ae@msgid.xyz","threadId":"65237","inReplyTo":"CV_name-rev_--format.4ad@msgid.xyz","subject":"[PATCH 1/2] name-rev: wrap both blocks in braces","fromName":"","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-03-13T16:03:37Z","receivedAt":"2026-03-13T16:04:45Z","isPatch":true,"body":"From: Kristoffer Haugsbakk <code@khaugsbakk.name>\n\nSee `CodingGuidelines`:\n\n    - When there are multiple arms to a conditional and some of them\n      require braces, enclose even a single line block in braces for\n      consistency. [...]\n\nSigned-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>\n---\n builtin/name-rev.c | 10 +++++-----\n 1 file changed, 5 insertions(+), 5 deletions(-)\n\ndiff --git a/builtin/name-rev.c b/builtin/name-rev.c\nindex 6188cf98ce0..9d2774f3723 100644\n--- a/builtin/name-rev.c\n+++ b/builtin/name-rev.c\n@@ -466,9 +466,9 @@ static const char *get_rev_name(const struct object *o, struct strbuf *buf)\n \tif (!n)\n \t\treturn NULL;\n \n-\tif (!n->generation)\n+\tif (!n->generation) {\n \t\treturn n->tip_name;\n-\telse {\n+\t} else {\n \t\tstrbuf_reset(buf);\n \t\tstrbuf_addstr(buf, n->tip_name);\n \t\tstrbuf_strip_suffix(buf, \"^0\");\n@@ -516,10 +516,10 @@ static void name_rev_line(char *p, struct name_ref_data *data)\n \n \tfor (p_start = p; *p; p++) {\n #define ishex(x) (isdigit((x)) || ((x) >= 'a' && (x) <= 'f'))\n-\t\tif (!ishex(*p))\n+\t\tif (!ishex(*p)) {\n \t\t\tcounter = 0;\n-\t\telse if (++counter == hexsz &&\n-\t\t\t !ishex(*(p+1))) {\n+\t\t} else if (++counter == hexsz &&\n+\t\t\t   !ishex(*(p + 1))) {\n \t\t\tstruct object_id oid;\n \t\t\tconst char *name = NULL;\n \t\t\tchar c = *(p+1);\n-- \n2.53.0.32.gf6228eaf9cc\n\n"},{"id":"538910","messageId":"name-rev_--format.4af@msgid.xyz","threadId":"65237","inReplyTo":"CV_name-rev_--format.4ad@msgid.xyz","subject":"[PATCH 2/2] name-rev: learn --format=<pretty>","fromName":"","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-03-13T16:03:38Z","receivedAt":"2026-03-13T16:05:04Z","isPatch":true,"body":"From: Kristoffer Haugsbakk <code@khaugsbakk.name>\n\nTeach git-name-rev(1) to format the given revisions instead of creating\nsymbolic names.\n\nSometimes you want to format commits. Most of the time you’re walking\nthe graph, e.g. getting a range of commits like `master..topic`. That’s\na job for git-log(1).\n\nBut sometimes you might want to format commits that you encounter\non demand:\n\n• Full hashes in running text that you might want to pretty-print\n• git-last-modified(1) outputs full hashes that you can do the same with\n• git-cherry(1) has `-v` for commit subject, but maybe you want\n  something else?\n\nBut now you can’t use git-log(1), git-show(1), or git-rev-list(1):\n\n• You can’t feed commits piecemeal to these commands, one input for one\n  output; they block until standard in is closed\n• You can’t feed a list of possibly duplicate commits, like the output\n  of git-last-modified(1); they effectively deduplicate the output\n\nBeyond these two points there’s also the input massage problem: you\ncannot feed mixed input (revisions mixed with arbitrary text).\n\nOne might hope that git-cat-file(1) can save us. But it doesn’t support\npretty formats.\n\nBut there is one command that already both handles revisions as\narguments, revisions on standard input, and even revisions mixed\nin with arbitrary text. Namely git-name-rev(1).\n\nTeach it to work in a format mode where the output for each revision is\nthe pretty output (implies `--name-only`). This can be used to format\nany revision expression when given as arguments, and all full commit\nhashes in running text on stdin.\n\nJust bring the hashes (to the pipeline). We will pretty print them.\n\nSigned-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>\n---\n Documentation/git-name-rev.adoc |  9 +++-\n builtin/name-rev.c              | 75 ++++++++++++++++++++++++++++++---\n t/t6120-describe.sh             | 58 +++++++++++++++++++++++++\n 3 files changed, 134 insertions(+), 8 deletions(-)\n\ndiff --git a/Documentation/git-name-rev.adoc b/Documentation/git-name-rev.adoc\nindex d4f1c4d5945..8f050cd4763 100644\n--- a/Documentation/git-name-rev.adoc\n+++ b/Documentation/git-name-rev.adoc\n@@ -9,7 +9,7 @@ git-name-rev - Find symbolic names for given revs\n SYNOPSIS\n --------\n [verse]\n-'git name-rev' [--tags] [--refs=<pattern>]\n+'git name-rev' [--tags] [--refs=<pattern>] [--format=<pretty>]\n \t       ( --all | --annotate-stdin | <commit-ish>... )\n \n DESCRIPTION\n@@ -21,6 +21,13 @@ format parsable by 'git rev-parse'.\n OPTIONS\n -------\n \n+--format=<pretty>::\n+--no-format::\n+\tFormat revisions instead of outputting symbolic names. The\n+\tdefault is `--no-format`.\n++\n+Implies `--name-only`.\n+\n --tags::\n \tDo not use branch names, but only tags to name the commits\n \ndiff --git a/builtin/name-rev.c b/builtin/name-rev.c\nindex 9d2774f3723..30d981104c6 100644\n--- a/builtin/name-rev.c\n+++ b/builtin/name-rev.c\n@@ -18,6 +18,9 @@\n #include \"commit-graph.h\"\n #include \"wildmatch.h\"\n #include \"mem-pool.h\"\n+#include \"pretty.h\"\n+#include \"revision.h\"\n+#include \"notes.h\"\n \n /*\n  * One day.  See the 'name a rev shortly after epoch' test in t6120 when\n@@ -33,6 +36,11 @@ struct rev_name {\n \tint from_tag;\n };\n \n+struct pretty_format {\n+\tstruct pretty_print_context ctx;\n+\tstruct userformat_want want;\n+};\n+\n define_commit_slab(commit_rev_name, struct rev_name);\n \n static timestamp_t generation_cutoff = GENERATION_NUMBER_INFINITY;\n@@ -454,7 +462,9 @@ static const char *get_exact_ref_match(const struct object *o)\n }\n \n /* may return a constant string or use \"buf\" as scratch space */\n-static const char *get_rev_name(const struct object *o, struct strbuf *buf)\n+static const char *get_rev_name(const struct object *o,\n+\t\t\t\tstruct pretty_format *format_ctx,\n+\t\t\t\tstruct strbuf *buf)\n {\n \tstruct rev_name *n;\n \tconst struct commit *c;\n@@ -462,6 +472,25 @@ static const char *get_rev_name(const struct object *o, struct strbuf *buf)\n \tif (o->type != OBJ_COMMIT)\n \t\treturn get_exact_ref_match(o);\n \tc = (const struct commit *) o;\n+\n+\tif (format_ctx) {\n+\t\tstrbuf_reset(buf);\n+\n+\t\tif (format_ctx->want.notes) {\n+\t\t\tstruct strbuf notebuf = STRBUF_INIT;\n+\n+\t\t\tformat_display_notes(&c->object.oid, &notebuf,\n+\t\t\t\t\t     get_log_output_encoding(),\n+\t\t\t\t\t     format_ctx->ctx.fmt == CMIT_FMT_USERFORMAT);\n+\t\t\tformat_ctx->ctx.notes_message = strbuf_detach(&notebuf, NULL);\n+\t\t}\n+\n+\t\tpretty_print_commit(&format_ctx->ctx, c, buf);\n+\t\tfree(format_ctx->ctx.notes_message);\n+\n+\t\treturn buf->buf;\n+\t}\n+\n \tn = get_commit_rev_name(c);\n \tif (!n)\n \t\treturn NULL;\n@@ -479,6 +508,7 @@ static const char *get_rev_name(const struct object *o, struct strbuf *buf)\n \n static void show_name(const struct object *obj,\n \t\t      const char *caller_name,\n+\t\t      struct pretty_format *format_ctx,\n \t\t      int always, int allow_undefined, int name_only)\n {\n \tconst char *name;\n@@ -487,7 +517,7 @@ static void show_name(const struct object *obj,\n \n \tif (!name_only)\n \t\tprintf(\"%s \", caller_name ? caller_name : oid_to_hex(oid));\n-\tname = get_rev_name(obj, &buf);\n+\tname = get_rev_name(obj, format_ctx, &buf);\n \tif (name)\n \t\tprintf(\"%s\\n\", name);\n \telse if (allow_undefined)\n@@ -507,7 +537,9 @@ static char const * const name_rev_usage[] = {\n \tNULL\n };\n \n-static void name_rev_line(char *p, struct name_ref_data *data)\n+static void name_rev_line(char *p,\n+\t\t\t  struct name_ref_data *data,\n+\t\t\t  struct pretty_format *format_ctx)\n {\n \tstruct strbuf buf = STRBUF_INIT;\n \tint counter = 0;\n@@ -532,7 +564,7 @@ static void name_rev_line(char *p, struct name_ref_data *data)\n \t\t\t\tstruct object *o =\n \t\t\t\t\tlookup_object(the_repository, &oid);\n \t\t\t\tif (o)\n-\t\t\t\t\tname = get_rev_name(o, &buf);\n+\t\t\t\t\tname = get_rev_name(o, format_ctx, &buf);\n \t\t\t}\n \t\t\t*(p+1) = c;\n \n@@ -567,6 +599,10 @@ int cmd_name_rev(int argc,\n #endif\n \tint all = 0, annotate_stdin = 0, allow_undefined = 1, always = 0, peel_tag = 0;\n \tstruct name_ref_data data = { 0, 0, STRING_LIST_INIT_NODUP, STRING_LIST_INIT_NODUP };\n+\tconst char *format = NULL;\n+\tstruct rev_info format_rev = REV_INFO_INIT;\n+\tstruct pretty_format *format_ctx = NULL;\n+\tstruct pretty_format format_pp = {0};\n \tstruct option opts[] = {\n \t\tOPT_BOOL(0, \"name-only\", &data.name_only, N_(\"print only ref-based names (no object names)\")),\n \t\tOPT_BOOL(0, \"tags\", &data.tags_only, N_(\"only use tags to name the commits\")),\n@@ -584,6 +620,8 @@ int cmd_name_rev(int argc,\n \t\t\t   PARSE_OPT_HIDDEN),\n #endif /* WITH_BREAKING_CHANGES */\n \t\tOPT_BOOL(0, \"annotate-stdin\", &annotate_stdin, N_(\"annotate text from stdin\")),\n+\t\tOPT_STRING(0, \"format\", &format, N_(\"format\"),\n+\t\t\t   \"pretty-print output instead\"),\n \t\tOPT_BOOL(0, \"undefined\", &allow_undefined, N_(\"allow to print `undefined` names (default)\")),\n \t\tOPT_BOOL(0, \"always\",     &always,\n \t\t\t   N_(\"show abbreviated commit object as fallback\")),\n@@ -606,6 +644,29 @@ int cmd_name_rev(int argc,\n \t}\n #endif\n \n+\tif (format) {\n+\t\tstruct pretty_print_context ctx = {0};\n+\t\tstruct userformat_want want = {0};\n+\n+\t\tget_commit_format(format, &format_rev);\n+\t\tctx.rev = &format_rev;\n+\t\tctx.fmt = format_rev.commit_format;\n+\t\tctx.abbrev = format_rev.abbrev;\n+\t\tctx.date_mode_explicit = format_rev.date_mode_explicit;\n+\t\tctx.date_mode = format_rev.date_mode;\n+\t\tctx.color = GIT_COLOR_AUTO;\n+\t\tformat_pp.ctx = ctx;\n+\n+\t\tuserformat_find_requirements(format, &want);\n+\t\tif (want.notes)\n+\t\t\tload_display_notes(NULL);\n+\n+\t\tformat_pp.want = want;\n+\t\tformat_ctx = &format_pp;\n+\n+\t\tdata.name_only = true;\n+\t}\n+\n \tif (all + annotate_stdin + !!argc > 1) {\n \t\terror(\"Specify either a list, or --all, not both!\");\n \t\tusage_with_options(name_rev_usage, opts);\n@@ -663,7 +724,7 @@ int cmd_name_rev(int argc,\n \n \t\twhile (strbuf_getline(&sb, stdin) != EOF) {\n \t\t\tstrbuf_addch(&sb, '\\n');\n-\t\t\tname_rev_line(sb.buf, &data);\n+\t\t\tname_rev_line(sb.buf, &data, format_ctx);\n \t\t}\n \t\tstrbuf_release(&sb);\n \t} else if (all) {\n@@ -674,13 +735,13 @@ int cmd_name_rev(int argc,\n \t\t\tstruct object *obj = get_indexed_object(the_repository, i);\n \t\t\tif (!obj || obj->type != OBJ_COMMIT)\n \t\t\t\tcontinue;\n-\t\t\tshow_name(obj, NULL,\n+\t\t\tshow_name(obj, NULL, format_ctx,\n \t\t\t\t  always, allow_undefined, data.name_only);\n \t\t}\n \t} else {\n \t\tint i;\n \t\tfor (i = 0; i < revs.nr; i++)\n-\t\t\tshow_name(revs.objects[i].item, revs.objects[i].name,\n+\t\t\tshow_name(revs.objects[i].item, revs.objects[i].name, format_ctx,\n \t\t\t\t  always, allow_undefined, data.name_only);\n \t}\n \ndiff --git a/t/t6120-describe.sh b/t/t6120-describe.sh\nindex 2c70cc561ad..6dba392d343 100755\n--- a/t/t6120-describe.sh\n+++ b/t/t6120-describe.sh\n@@ -658,6 +658,64 @@ test_expect_success 'name-rev --annotate-stdin works with commitGraph' '\n \t)\n '\n \n+test_expect_success 'name-rev --format setup' '\n+\tmkdir repo-format &&\n+\tgit -C repo-format init &&\n+\ttest_commit -C repo-format first &&\n+\ttest_commit -C repo-format second &&\n+\ttest_commit -C repo-format third &&\n+\ttest_commit -C repo-format fourth &&\n+\ttest_commit -C repo-format fifth &&\n+\ttest_commit -C repo-format sixth &&\n+\ttest_commit -C repo-format seventh &&\n+\ttest_commit -C repo-format eighth\n+'\n+\n+test_expect_success 'name-rev --format=%s for argument revs' '\n+\tcat >expect <<-\\EOF &&\n+\teighth\n+\tseventh\n+\tfifth\n+\tEOF\n+\tgit -C repo-format name-rev --format=%s \\\n+\t\tHEAD HEAD~ HEAD~3 >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success '--name-rev --format=<pretty> --annotate-stdin from rev-list same as log' '\n+\tgit -C repo-format log --format=reference >expect &&\n+\ttest_file_not_empty expect &&\n+\tgit -C repo-format rev-list HEAD >list &&\n+\tgit -C repo-format name-rev --format=reference \\\n+\t\t--annotate-stdin <list >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success '--name-rev --format=<pretty> --annotate-stdin with running text and tree oid' '\n+\tcmit_oid=$(git -C repo-format rev-parse :/fifth) &&\n+\treference=$(git -C repo-format log -n1 --format=reference :/fifth) &&\n+\ttree=$(git -C repo-format rev-parse HEAD^{tree}) &&\n+\tcat >expect <<-EOF &&\n+\tWe thought we fixed this in ${reference}.\n+\tBut look at this tree: ${tree}.\n+\tEOF\n+\tgit -C repo-format name-rev --format=reference --annotate-stdin \\\n+\t\t>actual <<-EOF &&\n+\tWe thought we fixed this in ${cmit_oid}.\n+\tBut look at this tree: ${tree}.\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success '--name-rev --format=<pretty> with a note' '\n+\ttest_when_finished \"git -C repo-format notes remove\" &&\n+\tgit -C repo-format notes add -m\"Make a note\" &&\n+\tprintf \"Make a note\\n\\n\\n\" >expect &&\n+\tgit -C repo-format name-rev --format=\"tformat:%N\" \\\n+\t\tHEAD HEAD~ >actual &&\n+\ttest_cmp expect actual\n+'\n+\n #               B\n #               o\n #  H             \\\n-- \n2.53.0.32.gf6228eaf9cc\n\n"},{"id":"538945","messageId":"xmqqeclnz2dv.fsf@gitster.g","threadId":"65237","inReplyTo":"name-rev_braces.4ae@msgid.xyz","subject":"Re: [PATCH 1/2] name-rev: wrap both blocks in braces","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-14T00:22:04Z","receivedAt":"2026-03-14T00:22:07Z","isPatch":true,"body":"kristofferhaugsbakk@fastmail.com writes:\n\n> -\t\telse if (++counter == hexsz &&\n> -\t\t\t !ishex(*(p+1))) {\n> +\t\t} else if (++counter == hexsz &&\n> +\t\t\t   !ishex(*(p + 1))) {\n>  \t\t\tstruct object_id oid;\n>  \t\t\tconst char *name = NULL;\n>  \t\t\tchar c = *(p+1);\n\nYou are correcting \"p+1\" to \"p + 1\" to honor our coding style in a\nfew lines above \"while at it\", but there are three others in the\nsame block (we can see one of them in the post-context), which means\nthese are now inconsistent.  Fixing all of them would make it a far\nlarger change than qualifies as a \"while at it\" change.  Either make\nit another step that is an unrelated clean up, or leave it as-is.\n\nThe primary thrust of this patch does make sense and is executed\nwell.\n\nThanks.\n"},{"id":"538946","messageId":"xmqq8qbvz2dm.fsf@gitster.g","threadId":"65237","inReplyTo":"name-rev_--format.4af@msgid.xyz","subject":"Re: [PATCH 2/2] name-rev: learn --format=<pretty>","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-14T00:22:13Z","receivedAt":"2026-03-14T00:22:15Z","isPatch":true,"body":"kristofferhaugsbakk@fastmail.com writes:\n\n> diff --git a/Documentation/git-name-rev.adoc b/Documentation/git-name-rev.adoc\n> index d4f1c4d5945..8f050cd4763 100644\n> --- a/Documentation/git-name-rev.adoc\n> +++ b/Documentation/git-name-rev.adoc\n> @@ -9,7 +9,7 @@ git-name-rev - Find symbolic names for given revs\n>  SYNOPSIS\n>  --------\n>  [verse]\n> -'git name-rev' [--tags] [--refs=<pattern>]\n> +'git name-rev' [--tags] [--refs=<pattern>] [--format=<pretty>]\n>  \t       ( --all | --annotate-stdin | <commit-ish>... )\n\nWe acquired a new option.  Do we need a matching change to\nthe contents of name_rev_usage[] array?\n\n> +--format=<pretty>::\n> +--no-format::\n> +\tFormat revisions instead of outputting symbolic names. The\n> +\tdefault is `--no-format`.\n> ++\n> +Implies `--name-only`.\n\nIf it is implication, would \n\n    git name-rev --format=reference --no-name-only\n\ndo what is naturally expected?\n\n> @@ -462,6 +472,25 @@ static const char *get_rev_name(const struct object *o, struct strbuf *buf)\n>  \tif (o->type != OBJ_COMMIT)\n>  \t\treturn get_exact_ref_match(o);\n>  \tc = (const struct commit *) o;\n> +\n> +\tif (format_ctx) {\n> +\t\tstrbuf_reset(buf);\n> +\n> +\t\tif (format_ctx->want.notes) {\n> +\t\t\tstruct strbuf notebuf = STRBUF_INIT;\n> +\n> +\t\t\tformat_display_notes(&c->object.oid, &notebuf,\n> +\t\t\t\t\t     get_log_output_encoding(),\n> +\t\t\t\t\t     format_ctx->ctx.fmt == CMIT_FMT_USERFORMAT);\n> +\t\t\tformat_ctx->ctx.notes_message = strbuf_detach(&notebuf, NULL);\n> +\t\t}\n> +\n> +\t\tpretty_print_commit(&format_ctx->ctx, c, buf);\n> +\t\tfree(format_ctx->ctx.notes_message);\n\nIs free() the expected thing to do here, or FREE_AND_NULL()?  Unlike\ncallers like log-tree.c:show_log() where a context is prepared, used\nonce, and then discarded, format_pp is initialized in cmd_name_rev()\nonce and then repeatedly used by show_name() potentially multiple\ntimes, so there may be a risk of getting confused by this leftover\nnon-NULL pointer that points at an already free'd piece of memory.\n\nOr there may not be---I did not check, but you as the author must\nhave already checked, hence this question.\n\n> +\t\treturn buf->buf;\n> +\t}\n> +\n>  \tn = get_commit_rev_name(c);\n>  \tif (!n)\n>  \t\treturn NULL;\n> @@ -479,6 +508,7 @@ static const char *get_rev_name(const struct object *o, struct strbuf *buf)\n>  \n>  static void show_name(const struct object *obj,\n>  \t\t      const char *caller_name,\n> +\t\t      struct pretty_format *format_ctx,\n>  \t\t      int always, int allow_undefined, int name_only)\n>  {\n>  \tconst char *name;\n> @@ -487,7 +517,7 @@ static void show_name(const struct object *obj,\n>  \n>  \tif (!name_only)\n>  \t\tprintf(\"%s \", caller_name ? caller_name : oid_to_hex(oid));\n> -\tname = get_rev_name(obj, &buf);\n> +\tname = get_rev_name(obj, format_ctx, &buf);\n>  \tif (name)\n>  \t\tprintf(\"%s\\n\", name);\n>  \telse if (allow_undefined)\n> @@ -507,7 +537,9 @@ static char const * const name_rev_usage[] = {\n>  \tNULL\n>  };\n>  \n> -static void name_rev_line(char *p, struct name_ref_data *data)\n> +static void name_rev_line(char *p,\n> +\t\t\t  struct name_ref_data *data,\n> +\t\t\t  struct pretty_format *format_ctx)\n>  {\n>  \tstruct strbuf buf = STRBUF_INIT;\n>  \tint counter = 0;\n> @@ -532,7 +564,7 @@ static void name_rev_line(char *p, struct name_ref_data *data)\n>  \t\t\t\tstruct object *o =\n>  \t\t\t\t\tlookup_object(the_repository, &oid);\n>  \t\t\t\tif (o)\n> -\t\t\t\t\tname = get_rev_name(o, &buf);\n> +\t\t\t\t\tname = get_rev_name(o, format_ctx, &buf);\n>  \t\t\t}\n>  \t\t\t*(p+1) = c;\n>  \n> @@ -567,6 +599,10 @@ int cmd_name_rev(int argc,\n>  #endif\n>  \tint all = 0, annotate_stdin = 0, allow_undefined = 1, always = 0, peel_tag = 0;\n>  \tstruct name_ref_data data = { 0, 0, STRING_LIST_INIT_NODUP, STRING_LIST_INIT_NODUP };\n> +\tconst char *format = NULL;\n> +\tstruct rev_info format_rev = REV_INFO_INIT;\n> +\tstruct pretty_format *format_ctx = NULL;\n> +\tstruct pretty_format format_pp = {0};\n\nHmph, would we want to use the full init_revisions() instead of\nstatic REV_INFO_INIT that initialises a lot more members of the\nstruct properly, most importantly the \"repo\" member that points at\nthe repostiory to be used?\n\n> +\tif (format) {\n> +\t\tstruct pretty_print_context ctx = {0};\n> +\t\tstruct userformat_want want = {0};\n> +\n> +\t\tget_commit_format(format, &format_rev);\n> +\t\tctx.rev = &format_rev;\n> +\t\tctx.fmt = format_rev.commit_format;\n> +\t\tctx.abbrev = format_rev.abbrev;\n> +\t\tctx.date_mode_explicit = format_rev.date_mode_explicit;\n> +\t\tctx.date_mode = format_rev.date_mode;\n> +\t\tctx.color = GIT_COLOR_AUTO;\n> +\t\tformat_pp.ctx = ctx;\n\nWhy does this code initialize and assign to a on-stack ctx first and\nthen assign it to format_pp.ctx, instead of working on format_pp.ctx\ndirectly?\n\n> +\t\tuserformat_find_requirements(format, &want);\n> +\t\tif (want.notes)\n> +\t\t\tload_display_notes(NULL);\n> +\n> +\t\tformat_pp.want = want;\n> +\t\tformat_ctx = &format_pp;\n> +\n> +\t\tdata.name_only = true;\n> +\t}\n> +\n"},{"id":"539264","messageId":"d3813f1d-174b-4d1b-b0c5-c6a8db260f6c@app.fastmail.com","threadId":"65237","inReplyTo":"xmqq8qbvz2dm.fsf@gitster.g","subject":"Re: [PATCH 2/2] name-rev: learn --format=<pretty>","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-03-17T22:07:59Z","receivedAt":"2026-03-17T22:08:22Z","isPatch":true,"body":"On Sat, Mar 14, 2026, at 01:22, Junio C Hamano wrote:\n> kristofferhaugsbakk@fastmail.com writes:\n>\n>> diff --git a/Documentation/git-name-rev.adoc b/Documentation/git-name-rev.adoc\n>> index d4f1c4d5945..8f050cd4763 100644\n>> --- a/Documentation/git-name-rev.adoc\n>> +++ b/Documentation/git-name-rev.adoc\n>> @@ -9,7 +9,7 @@ git-name-rev - Find symbolic names for given revs\n>>  SYNOPSIS\n>>  --------\n>>  [verse]\n>> -'git name-rev' [--tags] [--refs=<pattern>]\n>> +'git name-rev' [--tags] [--refs=<pattern>] [--format=<pretty>]\n>>  \t       ( --all | --annotate-stdin | <commit-ish>... )\n>\n> We acquired a new option.  Do we need a matching change to\n> the contents of name_rev_usage[] array?\n\nI looked at it and it seemed that `[<options>]` were supposed to stay\ninside that placeholder. But since this is a new mode, maybe:\n\n    diff --git builtin/name-rev.c builtin/name-rev.c\n    index 6188cf98ce0..13e67a7723c 100644\n    --- builtin/name-rev.c\n    +++ builtin/name-rev.c\n    @@ -504,6 +504,7 @@ static char const * const name_rev_usage[] = {\n            N_(\"git name-rev [<options>] <commit>...\"),\n            N_(\"git name-rev [<options>] --all\"),\n            N_(\"git name-rev [<options>] --annotate-stdin\"),\n    +       N_(\"git name-rev --format=<pretty> ...\"),\n            NULL\n     };\n\n>\n>> +--format=<pretty>::\n>> +--no-format::\n>> +\tFormat revisions instead of outputting symbolic names. The\n>> +\tdefault is `--no-format`.\n>> ++\n>> +Implies `--name-only`.\n>\n> If it is implication, would\n>\n>     git name-rev --format=reference --no-name-only\n>\n> do what is naturally expected?\n\nNo it wouldn’t. You’re right, it really locks in that option with no\nescape hatch.\n\nBut in my next version I have switched to a parse-options callback so\nthat `... --format=... --no-name-only` really does turn off `--name-only`.\n\n>\n>> @@ -462,6 +472,25 @@ static const char *get_rev_name(const struct object *o, struct strbuf *buf)\n>>  \tif (o->type != OBJ_COMMIT)\n>>  \t\treturn get_exact_ref_match(o);\n>>  \tc = (const struct commit *) o;\n>> +\n>> +\tif (format_ctx) {\n>> +\t\tstrbuf_reset(buf);\n>> +\n>> +\t\tif (format_ctx->want.notes) {\n>> +\t\t\tstruct strbuf notebuf = STRBUF_INIT;\n>> +\n>> +\t\t\tformat_display_notes(&c->object.oid, &notebuf,\n>> +\t\t\t\t\t     get_log_output_encoding(),\n>> +\t\t\t\t\t     format_ctx->ctx.fmt == CMIT_FMT_USERFORMAT);\n>> +\t\t\tformat_ctx->ctx.notes_message = strbuf_detach(&notebuf, NULL);\n>> +\t\t}\n>> +\n>> +\t\tpretty_print_commit(&format_ctx->ctx, c, buf);\n>> +\t\tfree(format_ctx->ctx.notes_message);\n>\n> Is free() the expected thing to do here, or FREE_AND_NULL()?  Unlike\n> callers like log-tree.c:show_log() where a context is prepared, used\n> once, and then discarded, format_pp is initialized in cmd_name_rev()\n> once and then repeatedly used by show_name() potentially multiple\n> times, so there may be a risk of getting confused by this leftover\n> non-NULL pointer that points at an already free'd piece of memory.\n>\n> Or there may not be---I did not check, but you as the author must\n> have already checked, hence this question.\n\nThis is supposed to be tested by `--name-rev --format=<pretty> with a\nnote`; it has a note on the first revision but not the second.\n\nHere we never use this pointer again and we get a fresh pointer from the\nstrbuf before freeing again (whether it gets populated with a pointer or\nnot).\n\nBut it does sound better to just null it. There’s no need to have it\nlaying around.\n\n>\n>> +\t\treturn buf->buf;\n>> +\t}\n>> +\n>>[snip]\n>> @@ -567,6 +599,10 @@ int cmd_name_rev(int argc,\n>>  #endif\n>>  \tint all = 0, annotate_stdin = 0, allow_undefined = 1, always = 0, peel_tag = 0;\n>>  \tstruct name_ref_data data = { 0, 0, STRING_LIST_INIT_NODUP, STRING_LIST_INIT_NODUP };\n>> +\tconst char *format = NULL;\n>> +\tstruct rev_info format_rev = REV_INFO_INIT;\n>> +\tstruct pretty_format *format_ctx = NULL;\n>> +\tstruct pretty_format format_pp = {0};\n>\n> Hmph, would we want to use the full init_revisions() instead of\n> static REV_INFO_INIT that initialises a lot more members of the\n> struct properly, most importantly the \"repo\" member that points at\n> the repostiory to be used?\n\nHere I looked at the doc for `pretty.h:get_commit_format` in order to\nlearn what I needed to set up. Since it doesn’t say much I did the least\nwork that I could get away with when it comes to struct\ninitializing. Since it does seem like a struct for a lot of different\nsituations while this is just a formatting situation.\n\nI might have done less research than I ought to.\n\n>\n>> +\tif (format) {\n>> +\t\tstruct pretty_print_context ctx = {0};\n>> +\t\tstruct userformat_want want = {0};\n>> +\n>> +\t\tget_commit_format(format, &format_rev);\n>> +\t\tctx.rev = &format_rev;\n>> +\t\tctx.fmt = format_rev.commit_format;\n>> +\t\tctx.abbrev = format_rev.abbrev;\n>> +\t\tctx.date_mode_explicit = format_rev.date_mode_explicit;\n>> +\t\tctx.date_mode = format_rev.date_mode;\n>> +\t\tctx.color = GIT_COLOR_AUTO;\n>> +\t\tformat_pp.ctx = ctx;\n>\n> Why does this code initialize and assign to a on-stack ctx first and\n> then assign it to format_pp.ctx, instead of working on format_pp.ctx\n> directly?\n\nYou’re right. I’ll just assign directly.\n\n>\n>> +\t\tuserformat_find_requirements(format, &want);\n>> +\t\tif (want.notes)\n>> +\t\t\tload_display_notes(NULL);\n>> +\n>> +\t\tformat_pp.want = want;\n>> +\t\tformat_ctx = &format_pp;\n>> +\n>> +\t\tdata.name_only = true;\n>> +\t}\n>> +\n"},{"id":"539267","messageId":"9ffa0e37-3bf4-47e1-bd3f-34962cc69633@app.fastmail.com","threadId":"65237","inReplyTo":"xmqqeclnz2dv.fsf@gitster.g","subject":"Re: [PATCH 1/2] name-rev: wrap both blocks in braces","fromName":"Kristoffer Haugsbakk","fromEmail":"code@khaugsbakk.name","sentAt":"2026-03-17T22:10:11Z","receivedAt":"2026-03-17T22:10:32Z","isPatch":true,"body":"On Sat, Mar 14, 2026, at 01:22, Junio C Hamano wrote:\n> kristofferhaugsbakk@fastmail.com writes:\n>\n>> -\t\telse if (++counter == hexsz &&\n>> -\t\t\t !ishex(*(p+1))) {\n>> +\t\t} else if (++counter == hexsz &&\n>> +\t\t\t   !ishex(*(p + 1))) {\n>>  \t\t\tstruct object_id oid;\n>>  \t\t\tconst char *name = NULL;\n>>  \t\t\tchar c = *(p+1);\n>\n> You are correcting \"p+1\" to \"p + 1\" to honor our coding style in a\n> few lines above \"while at it\", but there are three others in the\n> same block (we can see one of them in the post-context), which means\n> these are now inconsistent.  Fixing all of them would make it a far\n> larger change than qualifies as a \"while at it\" change.  Either make\n> it another step that is an unrelated clean up, or leave it as-is.\n\nCorrect observation. clang-format got ahold of a few stray lines. But I\nthought it only corrected the indentation, not that it also caught this\narithmetic formatting. I’ll remove it in the next version.\n\nThanks\n\n>\n> The primary thrust of this patch does make sense and is executed\n> well.\n"},{"id":"539290","messageId":"6eca2a08-78a9-4186-a199-c876f07415ad@app.fastmail.com","threadId":"65237","inReplyTo":"d3813f1d-174b-4d1b-b0c5-c6a8db260f6c@app.fastmail.com","subject":"Re: [PATCH 2/2] name-rev: learn --format=<pretty>","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-03-18T15:36:50Z","receivedAt":"2026-03-18T15:37:13Z","isPatch":true,"body":"On Tue, Mar 17, 2026, at 23:07, Kristoffer Haugsbakk wrote:\n>>[snip]\n>>\n>> Or there may not be---I did not check, but you as the author must\n>> have already checked, hence this question.\n>\n> This is supposed to be tested by `--name-rev --format=<pretty> with a\n> note`; it has a note on the first revision but not the second.\n>\n> Here we never use this pointer again and we get a fresh pointer from the\n> strbuf before freeing again (whether it gets populated with a pointer or\n> not).\n\nHere I meant to write: “whether it gets populated with any note data\nor not”.\n\n>\n> But it does sound better to just null it. There’s no need to have it\n> laying around.\n>[snip]\n"},{"id":"539543","messageId":"V2_CV_name-rev_--format.51b@msgid.xyz","threadId":"65237","inReplyTo":"CV_name-rev_--format.4ad@msgid.xyz","subject":"[PATCH v2 0/2] name-rev: learn --format=<pretty>","fromName":"","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-03-20T13:09:33Z","receivedAt":"2026-03-20T13:10:04Z","isPatch":true,"body":"From: Kristoffer Haugsbakk <code@khaugsbakk.name>\n\nTopic name (applied): kh/name-rev-custom-format\n\nTopic summary: Teach git-name-rev(1) a mode to pretty format revisions\ninstead of outputting symbolic names.\n\n(See the second patch for details.)\n\nThe first patch is just for `CodingGuidelines`. Unrelated.\n\n(See the previous cover letter for a slightly different introduction.)\n\nThis started because I had a faint wish in the back of my head (that I was\nprobably not going to act on) to implement a third-party git-format-rev(1)\ncommand which does what `git name-rev --format=<pretty>` does with this\nseries applied. I thought I could just use one invocation of some git(1)\ncommand to feed to stdin and read from stdout. Like git-log(1), or\ngit-rev-list(1). But due to apparent limitations (see patch [2/2] message)\nthere didn’t seem to be anything better than invoking a git(1) command per\nrevision/hash that was encountered in the text. I wanted something more\nefficient. (I had no need for something more efficient. I just wanted it.)\n\nI then picked git-name-rev(1) since that was the easiest thing to implement\nfor me. Add an option to an existing command.\n\nRight now this is just version 2 and I don’t know if this is a good\napproach or not. If it ever is accepted (in whatever form) I don’t expect\nit to be soon.\n\ngit(1) already has a lot of commands. This doesn’t expand the footprint. On\nthe other hand, I might be naive about what a proper formatter needs in\nterms of command options. Right now there is the helper `--notes`. But is\nmore needed?\n\n• --abbrev ?\n• --date ?\n\nI don’t know. Maybe at some point this would be too much, too crowded, for\ngit-name-rev(1). Then it might make sense to split it out to a separate\nbuiltin?\n\nThese are some thoughts and context.\n\n§ Changes in v2\n\nImplement `--notes` and respond to reviewer feedback. Details on the second\npatch.\n\nThe first patch now just formats what it intends to (there was stray\nclang-formatting).\n\n[1/2] name-rev: wrap both blocks in braces\n[2/2] name-rev: learn --format=<pretty>\n\n Documentation/git-name-rev.adoc |  10 ++-\n builtin/name-rev.c              | 108 ++++++++++++++++++++++++++++----\n t/t6120-describe.sh             |  96 ++++++++++++++++++++++++++++\n 3 files changed, 202 insertions(+), 12 deletions(-)\n\nInterdiff against v1:\ndiff --git a/Documentation/git-name-rev.adoc b/Documentation/git-name-rev.adoc\nindex 8f050cd4763..65348690c8c 100644\n--- a/Documentation/git-name-rev.adoc\n+++ b/Documentation/git-name-rev.adoc\n@@ -26,7 +26,8 @@ OPTIONS\n \tFormat revisions instead of outputting symbolic names. The\n \tdefault is `--no-format`.\n +\n-Implies `--name-only`.\n+Implies `--name-only`. The negation `--no-format` implies\n+`--no-name-only` (the default for the command).\n \n --tags::\n \tDo not use branch names, but only tags to name the commits\ndiff --git a/builtin/name-rev.c b/builtin/name-rev.c\nindex 30d981104c6..9a008d8b7a8 100644\n--- a/builtin/name-rev.c\n+++ b/builtin/name-rev.c\n@@ -41,6 +41,11 @@ struct pretty_format {\n \tstruct userformat_want want;\n };\n \n+struct format_cb_data {\n+    const char *format;\n+    int *name_only;\n+};\n+\n define_commit_slab(commit_rev_name, struct rev_name);\n \n static timestamp_t generation_cutoff = GENERATION_NUMBER_INFINITY;\n@@ -486,7 +491,7 @@ static const char *get_rev_name(const struct object *o,\n \t\t}\n \n \t\tpretty_print_commit(&format_ctx->ctx, c, buf);\n-\t\tfree(format_ctx->ctx.notes_message);\n+\t\tFREE_AND_NULL(format_ctx->ctx.notes_message);\n \n \t\treturn buf->buf;\n \t}\n@@ -551,7 +556,7 @@ static void name_rev_line(char *p,\n \t\tif (!ishex(*p)) {\n \t\t\tcounter = 0;\n \t\t} else if (++counter == hexsz &&\n-\t\t\t   !ishex(*(p + 1))) {\n+\t\t\t !ishex(*(p+1))) {\n \t\t\tstruct object_id oid;\n \t\t\tconst char *name = NULL;\n \t\t\tchar c = *(p+1);\n@@ -586,6 +591,16 @@ static void name_rev_line(char *p,\n \tstrbuf_release(&buf);\n }\n \n+static int format_cb(const struct option *option,\n+\t\t     const char *arg,\n+\t\t     int unset)\n+{\n+\tstruct format_cb_data *data = option->value;\n+\tdata->format = arg;\n+\t*data->name_only = !unset;\n+\treturn 0;\n+}\n+\n int cmd_name_rev(int argc,\n \t\t const char **argv,\n \t\t const char *prefix,\n@@ -599,10 +614,12 @@ int cmd_name_rev(int argc,\n #endif\n \tint all = 0, annotate_stdin = 0, allow_undefined = 1, always = 0, peel_tag = 0;\n \tstruct name_ref_data data = { 0, 0, STRING_LIST_INIT_NODUP, STRING_LIST_INIT_NODUP };\n-\tconst char *format = NULL;\n+\tstatic struct format_cb_data format_cb_data = { 0 };\n+\tstruct display_notes_opt format_notes_opt;\n \tstruct rev_info format_rev = REV_INFO_INIT;\n \tstruct pretty_format *format_ctx = NULL;\n-\tstruct pretty_format format_pp = {0};\n+\tstruct pretty_format format_pp = { 0 };\n+\tstruct string_list notes = STRING_LIST_INIT_NODUP;\n \tstruct option opts[] = {\n \t\tOPT_BOOL(0, \"name-only\", &data.name_only, N_(\"print only ref-based names (no object names)\")),\n \t\tOPT_BOOL(0, \"tags\", &data.tags_only, N_(\"only use tags to name the commits\")),\n@@ -620,8 +637,10 @@ int cmd_name_rev(int argc,\n \t\t\t   PARSE_OPT_HIDDEN),\n #endif /* WITH_BREAKING_CHANGES */\n \t\tOPT_BOOL(0, \"annotate-stdin\", &annotate_stdin, N_(\"annotate text from stdin\")),\n-\t\tOPT_STRING(0, \"format\", &format, N_(\"format\"),\n-\t\t\t   \"pretty-print output instead\"),\n+\t\tOPT_CALLBACK(0, \"format\", &format_cb_data, N_(\"format\"),\n+\t\t\t     N_(\"pretty-print output instead\"), format_cb),\n+\t\tOPT_STRING_LIST(0, \"notes\", &notes, N_(\"notes\"),\n+\t\t\t\tN_(\"display notes for --format\")),\n \t\tOPT_BOOL(0, \"undefined\", &allow_undefined, N_(\"allow to print `undefined` names (default)\")),\n \t\tOPT_BOOL(0, \"always\",     &always,\n \t\t\t   N_(\"show abbreviated commit object as fallback\")),\n@@ -630,6 +649,8 @@ int cmd_name_rev(int argc,\n \t\tOPT_END(),\n \t};\n \n+\tinit_display_notes(&format_notes_opt);\n+\tformat_cb_data.name_only = &data.name_only;\n \tmem_pool_init(&string_pool, 0);\n \tinit_commit_rev_name(&rev_names);\n \trepo_config(the_repository, git_default_config, NULL);\n@@ -644,27 +665,29 @@ int cmd_name_rev(int argc,\n \t}\n #endif\n \n-\tif (format) {\n-\t\tstruct pretty_print_context ctx = {0};\n-\t\tstruct userformat_want want = {0};\n-\n-\t\tget_commit_format(format, &format_rev);\n-\t\tctx.rev = &format_rev;\n-\t\tctx.fmt = format_rev.commit_format;\n-\t\tctx.abbrev = format_rev.abbrev;\n-\t\tctx.date_mode_explicit = format_rev.date_mode_explicit;\n-\t\tctx.date_mode = format_rev.date_mode;\n-\t\tctx.color = GIT_COLOR_AUTO;\n-\t\tformat_pp.ctx = ctx;\n-\n-\t\tuserformat_find_requirements(format, &want);\n-\t\tif (want.notes)\n-\t\t\tload_display_notes(NULL);\n+\tif (format_cb_data.format) {\n+\t\tget_commit_format(format_cb_data.format, &format_rev);\n+\t\tformat_pp.ctx.rev = &format_rev;\n+\t\tformat_pp.ctx.fmt = format_rev.commit_format;\n+\t\tformat_pp.ctx.abbrev = format_rev.abbrev;\n+\t\tformat_pp.ctx.date_mode_explicit = format_rev.date_mode_explicit;\n+\t\tformat_pp.ctx.date_mode = format_rev.date_mode;\n+\t\tformat_pp.ctx.color = GIT_COLOR_AUTO;\n+\n+\t\tuserformat_find_requirements(format_cb_data.format,\n+\t\t\t\t\t     &format_pp.want);\n+\t\tif (format_pp.want.notes) {\n+\t\t\tint ignore_show_notes = 0;\n+\t\t\tstruct string_list_item *n;\n+\n+\t\t\tfor_each_string_list_item(n, &notes)\n+\t\t\t\tenable_ref_display_notes(&format_notes_opt,\n+\t\t\t\t\t\t\t &ignore_show_notes,\n+\t\t\t\t\t\t\t n->string);\n+\t\t\tload_display_notes(&format_notes_opt);\n+\t\t}\n \n-\t\tformat_pp.want = want;\n \t\tformat_ctx = &format_pp;\n-\n-\t\tdata.name_only = true;\n \t}\n \n \tif (all + annotate_stdin + !!argc > 1) {\n@@ -747,6 +770,8 @@ int cmd_name_rev(int argc,\n \n \tstring_list_clear(&data.ref_filters, 0);\n \tstring_list_clear(&data.exclude_filters, 0);\n+\tstring_list_clear(&notes, 0);\n+\trelease_display_notes(&format_notes_opt);\n \tmem_pool_discard(&string_pool, 0);\n \tobject_array_clear(&revs);\n \treturn 0;\ndiff --git a/t/t6120-describe.sh b/t/t6120-describe.sh\nindex 6dba392d343..0b7e9fe396d 100755\n--- a/t/t6120-describe.sh\n+++ b/t/t6120-describe.sh\n@@ -671,6 +671,25 @@ test_expect_success 'name-rev --format setup' '\n \ttest_commit -C repo-format eighth\n '\n \n+test_expect_success 'name-rev --format --no-name-only' '\n+\tcat >expect <<-\\EOF &&\n+\tHEAD~3 [fifth]\n+\tHEAD [eighth]\n+\tHEAD~5 [third]\n+\tEOF\n+\tgit -C repo-format name-rev --format=\"[%s]\" \\\n+\t\t--no-name-only HEAD~3 HEAD HEAD~5 >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'name-rev --format --no-format is the same as regular name-rev' '\n+\tgit -C repo-format name-rev HEAD~2 HEAD~3 >expect &&\n+\ttest_file_not_empty expect &&\n+\tgit -C repo-format name-rev --format=\"huh?\" \\\n+\t\t--no-format HEAD~2 HEAD~3 >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success 'name-rev --format=%s for argument revs' '\n \tcat >expect <<-\\EOF &&\n \teighth\n@@ -682,7 +701,7 @@ test_expect_success 'name-rev --format=%s for argument revs' '\n \ttest_cmp expect actual\n '\n \n-test_expect_success '--name-rev --format=<pretty> --annotate-stdin from rev-list same as log' '\n+test_expect_success '--name-rev --format=reference --annotate-stdin from rev-list same as log' '\n \tgit -C repo-format log --format=reference >expect &&\n \ttest_file_not_empty expect &&\n \tgit -C repo-format rev-list HEAD >list &&\n@@ -707,7 +726,7 @@ test_expect_success '--name-rev --format=<pretty> --annotate-stdin with running\n \ttest_cmp expect actual\n '\n \n-test_expect_success '--name-rev --format=<pretty> with a note' '\n+test_expect_success 'name-rev --format=<pretty> with %N (note)' '\n \ttest_when_finished \"git -C repo-format notes remove\" &&\n \tgit -C repo-format notes add -m\"Make a note\" &&\n \tprintf \"Make a note\\n\\n\\n\" >expect &&\n@@ -716,6 +735,25 @@ test_expect_success '--name-rev --format=<pretty> with a note' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'name-rev --format=<pretty> --notes<ref>' '\n+\t# One custom notes ref\n+\ttest_when_finished \"git -C repo-format notes remove\" &&\n+\ttest_when_finished \"git -C repo-format notes --ref=word remove\" &&\n+\tgit -C repo-format notes add -m\"default\" &&\n+\tgit -C repo-format notes --ref=word add -m\"custom\" &&\n+\tprintf \"custom\\n\\n\" >expect &&\n+\tgit -C repo-format name-rev --format=\"tformat:%N\" \\\n+\t\t--notes=word \\\n+\t\tHEAD >actual &&\n+\ttest_cmp expect actual &&\n+\t# Glob all\n+\tprintf \"default\\ncustom\\n\\n\" >expect &&\n+\tgit -C repo-format name-rev --format=\"tformat:%N\" \\\n+\t\t--notes=* \\\n+\t\tHEAD >actual &&\n+\ttest_cmp expect actual\n+'\n+\n #               B\n #               o\n #  H             \\\nRange-diff against v1:\n1:  6f88b4c96a9 ! 1:  9cb5cfd1ec3 name-rev: wrap both blocks in braces\n    @@ builtin/name-rev.c: static void name_rev_line(char *p, struct name_ref_data *dat\n     +\t\tif (!ishex(*p)) {\n      \t\t\tcounter = 0;\n     -\t\telse if (++counter == hexsz &&\n    --\t\t\t !ishex(*(p+1))) {\n     +\t\t} else if (++counter == hexsz &&\n    -+\t\t\t   !ishex(*(p + 1))) {\n    + \t\t\t !ishex(*(p+1))) {\n      \t\t\tstruct object_id oid;\n      \t\t\tconst char *name = NULL;\n    - \t\t\tchar c = *(p+1);\n2:  6430627e611 ! 2:  52a52060776 name-rev: learn --format=<pretty>\n    @@ Documentation/git-name-rev.adoc: format parsable by 'git rev-parse'.\n     +\tFormat revisions instead of outputting symbolic names. The\n     +\tdefault is `--no-format`.\n     ++\n    -+Implies `--name-only`.\n    ++Implies `--name-only`. The negation `--no-format` implies\n    ++`--no-name-only` (the default for the command).\n     +\n      --tags::\n      \tDo not use branch names, but only tags to name the commits\n    @@ builtin/name-rev.c: struct rev_name {\n     +\tstruct pretty_print_context ctx;\n     +\tstruct userformat_want want;\n     +};\n    ++\n    ++struct format_cb_data {\n    ++    const char *format;\n    ++    int *name_only;\n    ++};\n     +\n      define_commit_slab(commit_rev_name, struct rev_name);\n      \n    @@ builtin/name-rev.c: static const char *get_rev_name(const struct object *o, stru\n     +\t\t}\n     +\n     +\t\tpretty_print_commit(&format_ctx->ctx, c, buf);\n    -+\t\tfree(format_ctx->ctx.notes_message);\n    ++\t\tFREE_AND_NULL(format_ctx->ctx.notes_message);\n     +\n     +\t\treturn buf->buf;\n     +\t}\n    @@ builtin/name-rev.c: static void name_rev_line(char *p, struct name_ref_data *dat\n      \t\t\t}\n      \t\t\t*(p+1) = c;\n      \n    +@@ builtin/name-rev.c: static void name_rev_line(char *p, struct name_ref_data *data)\n    + \tstrbuf_release(&buf);\n    + }\n    + \n    ++static int format_cb(const struct option *option,\n    ++\t\t     const char *arg,\n    ++\t\t     int unset)\n    ++{\n    ++\tstruct format_cb_data *data = option->value;\n    ++\tdata->format = arg;\n    ++\t*data->name_only = !unset;\n    ++\treturn 0;\n    ++}\n    ++\n    + int cmd_name_rev(int argc,\n    + \t\t const char **argv,\n    + \t\t const char *prefix,\n     @@ builtin/name-rev.c: int cmd_name_rev(int argc,\n      #endif\n      \tint all = 0, annotate_stdin = 0, allow_undefined = 1, always = 0, peel_tag = 0;\n      \tstruct name_ref_data data = { 0, 0, STRING_LIST_INIT_NODUP, STRING_LIST_INIT_NODUP };\n    -+\tconst char *format = NULL;\n    ++\tstatic struct format_cb_data format_cb_data = { 0 };\n    ++\tstruct display_notes_opt format_notes_opt;\n     +\tstruct rev_info format_rev = REV_INFO_INIT;\n     +\tstruct pretty_format *format_ctx = NULL;\n    -+\tstruct pretty_format format_pp = {0};\n    ++\tstruct pretty_format format_pp = { 0 };\n    ++\tstruct string_list notes = STRING_LIST_INIT_NODUP;\n      \tstruct option opts[] = {\n      \t\tOPT_BOOL(0, \"name-only\", &data.name_only, N_(\"print only ref-based names (no object names)\")),\n      \t\tOPT_BOOL(0, \"tags\", &data.tags_only, N_(\"only use tags to name the commits\")),\n    @@ builtin/name-rev.c: int cmd_name_rev(int argc,\n      \t\t\t   PARSE_OPT_HIDDEN),\n      #endif /* WITH_BREAKING_CHANGES */\n      \t\tOPT_BOOL(0, \"annotate-stdin\", &annotate_stdin, N_(\"annotate text from stdin\")),\n    -+\t\tOPT_STRING(0, \"format\", &format, N_(\"format\"),\n    -+\t\t\t   \"pretty-print output instead\"),\n    ++\t\tOPT_CALLBACK(0, \"format\", &format_cb_data, N_(\"format\"),\n    ++\t\t\t     N_(\"pretty-print output instead\"), format_cb),\n    ++\t\tOPT_STRING_LIST(0, \"notes\", &notes, N_(\"notes\"),\n    ++\t\t\t\tN_(\"display notes for --format\")),\n      \t\tOPT_BOOL(0, \"undefined\", &allow_undefined, N_(\"allow to print `undefined` names (default)\")),\n      \t\tOPT_BOOL(0, \"always\",     &always,\n      \t\t\t   N_(\"show abbreviated commit object as fallback\")),\n    +@@ builtin/name-rev.c: int cmd_name_rev(int argc,\n    + \t\tOPT_END(),\n    + \t};\n    + \n    ++\tinit_display_notes(&format_notes_opt);\n    ++\tformat_cb_data.name_only = &data.name_only;\n    + \tmem_pool_init(&string_pool, 0);\n    + \tinit_commit_rev_name(&rev_names);\n    + \trepo_config(the_repository, git_default_config, NULL);\n     @@ builtin/name-rev.c: int cmd_name_rev(int argc,\n      \t}\n      #endif\n      \n    -+\tif (format) {\n    -+\t\tstruct pretty_print_context ctx = {0};\n    -+\t\tstruct userformat_want want = {0};\n    ++\tif (format_cb_data.format) {\n    ++\t\tget_commit_format(format_cb_data.format, &format_rev);\n    ++\t\tformat_pp.ctx.rev = &format_rev;\n    ++\t\tformat_pp.ctx.fmt = format_rev.commit_format;\n    ++\t\tformat_pp.ctx.abbrev = format_rev.abbrev;\n    ++\t\tformat_pp.ctx.date_mode_explicit = format_rev.date_mode_explicit;\n    ++\t\tformat_pp.ctx.date_mode = format_rev.date_mode;\n    ++\t\tformat_pp.ctx.color = GIT_COLOR_AUTO;\n     +\n    -+\t\tget_commit_format(format, &format_rev);\n    -+\t\tctx.rev = &format_rev;\n    -+\t\tctx.fmt = format_rev.commit_format;\n    -+\t\tctx.abbrev = format_rev.abbrev;\n    -+\t\tctx.date_mode_explicit = format_rev.date_mode_explicit;\n    -+\t\tctx.date_mode = format_rev.date_mode;\n    -+\t\tctx.color = GIT_COLOR_AUTO;\n    -+\t\tformat_pp.ctx = ctx;\n    ++\t\tuserformat_find_requirements(format_cb_data.format,\n    ++\t\t\t\t\t     &format_pp.want);\n    ++\t\tif (format_pp.want.notes) {\n    ++\t\t\tint ignore_show_notes = 0;\n    ++\t\t\tstruct string_list_item *n;\n     +\n    -+\t\tuserformat_find_requirements(format, &want);\n    -+\t\tif (want.notes)\n    -+\t\t\tload_display_notes(NULL);\n    ++\t\t\tfor_each_string_list_item(n, &notes)\n    ++\t\t\t\tenable_ref_display_notes(&format_notes_opt,\n    ++\t\t\t\t\t\t\t &ignore_show_notes,\n    ++\t\t\t\t\t\t\t n->string);\n    ++\t\t\tload_display_notes(&format_notes_opt);\n    ++\t\t}\n     +\n    -+\t\tformat_pp.want = want;\n     +\t\tformat_ctx = &format_pp;\n    -+\n    -+\t\tdata.name_only = true;\n     +\t}\n     +\n      \tif (all + annotate_stdin + !!argc > 1) {\n    @@ builtin/name-rev.c: int cmd_name_rev(int argc,\n      \t\t\t\t  always, allow_undefined, data.name_only);\n      \t}\n      \n    + \tstring_list_clear(&data.ref_filters, 0);\n    + \tstring_list_clear(&data.exclude_filters, 0);\n    ++\tstring_list_clear(&notes, 0);\n    ++\trelease_display_notes(&format_notes_opt);\n    + \tmem_pool_discard(&string_pool, 0);\n    + \tobject_array_clear(&revs);\n    + \treturn 0;\n     \n      ## t/t6120-describe.sh ##\n     @@ t/t6120-describe.sh: test_expect_success 'name-rev --annotate-stdin works with commitGraph' '\n    @@ t/t6120-describe.sh: test_expect_success 'name-rev --annotate-stdin works with c\n     +\ttest_commit -C repo-format eighth\n     +'\n     +\n    ++test_expect_success 'name-rev --format --no-name-only' '\n    ++\tcat >expect <<-\\EOF &&\n    ++\tHEAD~3 [fifth]\n    ++\tHEAD [eighth]\n    ++\tHEAD~5 [third]\n    ++\tEOF\n    ++\tgit -C repo-format name-rev --format=\"[%s]\" \\\n    ++\t\t--no-name-only HEAD~3 HEAD HEAD~5 >actual &&\n    ++\ttest_cmp expect actual\n    ++'\n    ++\n    ++test_expect_success 'name-rev --format --no-format is the same as regular name-rev' '\n    ++\tgit -C repo-format name-rev HEAD~2 HEAD~3 >expect &&\n    ++\ttest_file_not_empty expect &&\n    ++\tgit -C repo-format name-rev --format=\"huh?\" \\\n    ++\t\t--no-format HEAD~2 HEAD~3 >actual &&\n    ++\ttest_cmp expect actual\n    ++'\n    ++\n     +test_expect_success 'name-rev --format=%s for argument revs' '\n     +\tcat >expect <<-\\EOF &&\n     +\teighth\n    @@ t/t6120-describe.sh: test_expect_success 'name-rev --annotate-stdin works with c\n     +\ttest_cmp expect actual\n     +'\n     +\n    -+test_expect_success '--name-rev --format=<pretty> --annotate-stdin from rev-list same as log' '\n    ++test_expect_success '--name-rev --format=reference --annotate-stdin from rev-list same as log' '\n     +\tgit -C repo-format log --format=reference >expect &&\n     +\ttest_file_not_empty expect &&\n     +\tgit -C repo-format rev-list HEAD >list &&\n    @@ t/t6120-describe.sh: test_expect_success 'name-rev --annotate-stdin works with c\n     +\ttest_cmp expect actual\n     +'\n     +\n    -+test_expect_success '--name-rev --format=<pretty> with a note' '\n    ++test_expect_success 'name-rev --format=<pretty> with %N (note)' '\n     +\ttest_when_finished \"git -C repo-format notes remove\" &&\n     +\tgit -C repo-format notes add -m\"Make a note\" &&\n     +\tprintf \"Make a note\\n\\n\\n\" >expect &&\n    @@ t/t6120-describe.sh: test_expect_success 'name-rev --annotate-stdin works with c\n     +\t\tHEAD HEAD~ >actual &&\n     +\ttest_cmp expect actual\n     +'\n    ++\n    ++test_expect_success 'name-rev --format=<pretty> --notes<ref>' '\n    ++\t# One custom notes ref\n    ++\ttest_when_finished \"git -C repo-format notes remove\" &&\n    ++\ttest_when_finished \"git -C repo-format notes --ref=word remove\" &&\n    ++\tgit -C repo-format notes add -m\"default\" &&\n    ++\tgit -C repo-format notes --ref=word add -m\"custom\" &&\n    ++\tprintf \"custom\\n\\n\" >expect &&\n    ++\tgit -C repo-format name-rev --format=\"tformat:%N\" \\\n    ++\t\t--notes=word \\\n    ++\t\tHEAD >actual &&\n    ++\ttest_cmp expect actual &&\n    ++\t# Glob all\n    ++\tprintf \"default\\ncustom\\n\\n\" >expect &&\n    ++\tgit -C repo-format name-rev --format=\"tformat:%N\" \\\n    ++\t\t--notes=* \\\n    ++\t\tHEAD >actual &&\n    ++\ttest_cmp expect actual\n    ++'\n     +\n      #               B\n      #               o\n\nbase-commit: 67006b9db8b772423ad0706029286096307d2567\n-- \n2.53.0.32.gf6228eaf9cc\n\n"},{"id":"539544","messageId":"V2_name-rev_braces.51c@msgid.xyz","threadId":"65237","inReplyTo":"V2_CV_name-rev_--format.51b@msgid.xyz","subject":"[PATCH v2 1/2] name-rev: wrap both blocks in braces","fromName":"","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-03-20T13:09:34Z","receivedAt":"2026-03-20T13:10:21Z","isPatch":true,"body":"From: Kristoffer Haugsbakk <code@khaugsbakk.name>\n\nSee `CodingGuidelines`:\n\n    - When there are multiple arms to a conditional and some of them\n      require braces, enclose even a single line block in braces for\n      consistency. [...]\n\nSigned-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>\n---\n\nNotes (series):\n    v2:\n    Fix stray formatting of `(p+1)`\n\n builtin/name-rev.c | 8 ++++----\n 1 file changed, 4 insertions(+), 4 deletions(-)\n\ndiff --git a/builtin/name-rev.c b/builtin/name-rev.c\nindex 6188cf98ce0..171e7bd0e98 100644\n--- a/builtin/name-rev.c\n+++ b/builtin/name-rev.c\n@@ -466,9 +466,9 @@ static const char *get_rev_name(const struct object *o, struct strbuf *buf)\n \tif (!n)\n \t\treturn NULL;\n \n-\tif (!n->generation)\n+\tif (!n->generation) {\n \t\treturn n->tip_name;\n-\telse {\n+\t} else {\n \t\tstrbuf_reset(buf);\n \t\tstrbuf_addstr(buf, n->tip_name);\n \t\tstrbuf_strip_suffix(buf, \"^0\");\n@@ -516,9 +516,9 @@ static void name_rev_line(char *p, struct name_ref_data *data)\n \n \tfor (p_start = p; *p; p++) {\n #define ishex(x) (isdigit((x)) || ((x) >= 'a' && (x) <= 'f'))\n-\t\tif (!ishex(*p))\n+\t\tif (!ishex(*p)) {\n \t\t\tcounter = 0;\n-\t\telse if (++counter == hexsz &&\n+\t\t} else if (++counter == hexsz &&\n \t\t\t !ishex(*(p+1))) {\n \t\t\tstruct object_id oid;\n \t\t\tconst char *name = NULL;\n-- \n2.53.0.32.gf6228eaf9cc\n\n"},{"id":"539545","messageId":"V2_name-rev_--format.51d@msgid.xyz","threadId":"65237","inReplyTo":"V2_CV_name-rev_--format.51b@msgid.xyz","subject":"[PATCH v2 2/2] name-rev: learn --format=<pretty>","fromName":"","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-03-20T13:09:35Z","receivedAt":"2026-03-20T13:10:39Z","isPatch":true,"body":"From: Kristoffer Haugsbakk <code@khaugsbakk.name>\n\nTeach git-name-rev(1) to format the given revisions instead of creating\nsymbolic names.\n\nSometimes you want to format commits. Most of the time you’re walking\nthe graph, e.g. getting a range of commits like `master..topic`. That’s\na job for git-log(1).\n\nBut sometimes you might want to format commits that you encounter\non demand:\n\n• Full hashes in running text that you might want to pretty-print\n• git-last-modified(1) outputs full hashes that you can do the same with\n• git-cherry(1) has `-v` for commit subject, but maybe you want\n  something else?\n\nBut now you can’t use git-log(1), git-show(1), or git-rev-list(1):\n\n• You can’t feed commits piecemeal to these commands, one input for one\n  output; they block until standard in is closed\n• You can’t feed a list of possibly duplicate commits, like the output\n  of git-last-modified(1); they effectively deduplicate the output\n\nBeyond these two points there’s also the input massage problem: you\ncannot feed mixed input (revisions mixed with arbitrary text).\n\nOne might hope that git-cat-file(1) can save us. But it doesn’t support\npretty formats.\n\nBut there is one command that already both handles revisions as\narguments, revisions on standard input, and even revisions mixed\nin with arbitrary text. Namely git-name-rev(1).\n\nTeach it to work in a format mode where the output for each revision is\nthe pretty output (implies `--name-only`). This can be used to format\nany revision expression when given as arguments, and all full commit\nhashes in running text on stdin.\n\nJust bring the hashes (to the pipeline). We will pretty print them.\n\nSigned-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>\n---\n\nNotes (series):\n    v2:\n    • Propely implement “--format implies --name-only”\n    • Don’t use a needless intermediary struct\n    • Add a new member to `name_rev_usage[]`\n    • FREE_AND_NULL notes string. There is no use-after-free but we have no\n      reason to leave a freed pointer just laying there\n    • Implement `--notes` for `%N` atom use (not just restrict to the default\n      notes ref)\n    • Previous review pointed out `init_revisions()` but this still just uses\n      `REV_INFO_INIT` since it seemed enough. But I have no problem with\n      changing it whatsoever. (This series is still very work-in-progress in\n      any case.)\n    • Tweak test name to mention the specific pretty format (reference); it\n      doesn’t generalize to e.g. `oneline` because you get different output in\n      that case\n    \n    Mostly from: https://lore.kernel.org/git/xmqq8qbvz2dm.fsf@gitster.g/\n\n Documentation/git-name-rev.adoc |  10 +++-\n builtin/name-rev.c              | 100 +++++++++++++++++++++++++++++---\n t/t6120-describe.sh             |  96 ++++++++++++++++++++++++++++++\n 3 files changed, 198 insertions(+), 8 deletions(-)\n\ndiff --git a/Documentation/git-name-rev.adoc b/Documentation/git-name-rev.adoc\nindex d4f1c4d5945..65348690c8c 100644\n--- a/Documentation/git-name-rev.adoc\n+++ b/Documentation/git-name-rev.adoc\n@@ -9,7 +9,7 @@ git-name-rev - Find symbolic names for given revs\n SYNOPSIS\n --------\n [verse]\n-'git name-rev' [--tags] [--refs=<pattern>]\n+'git name-rev' [--tags] [--refs=<pattern>] [--format=<pretty>]\n \t       ( --all | --annotate-stdin | <commit-ish>... )\n \n DESCRIPTION\n@@ -21,6 +21,14 @@ format parsable by 'git rev-parse'.\n OPTIONS\n -------\n \n+--format=<pretty>::\n+--no-format::\n+\tFormat revisions instead of outputting symbolic names. The\n+\tdefault is `--no-format`.\n++\n+Implies `--name-only`. The negation `--no-format` implies\n+`--no-name-only` (the default for the command).\n+\n --tags::\n \tDo not use branch names, but only tags to name the commits\n \ndiff --git a/builtin/name-rev.c b/builtin/name-rev.c\nindex 171e7bd0e98..9a008d8b7a8 100644\n--- a/builtin/name-rev.c\n+++ b/builtin/name-rev.c\n@@ -18,6 +18,9 @@\n #include \"commit-graph.h\"\n #include \"wildmatch.h\"\n #include \"mem-pool.h\"\n+#include \"pretty.h\"\n+#include \"revision.h\"\n+#include \"notes.h\"\n \n /*\n  * One day.  See the 'name a rev shortly after epoch' test in t6120 when\n@@ -33,6 +36,16 @@ struct rev_name {\n \tint from_tag;\n };\n \n+struct pretty_format {\n+\tstruct pretty_print_context ctx;\n+\tstruct userformat_want want;\n+};\n+\n+struct format_cb_data {\n+    const char *format;\n+    int *name_only;\n+};\n+\n define_commit_slab(commit_rev_name, struct rev_name);\n \n static timestamp_t generation_cutoff = GENERATION_NUMBER_INFINITY;\n@@ -454,7 +467,9 @@ static const char *get_exact_ref_match(const struct object *o)\n }\n \n /* may return a constant string or use \"buf\" as scratch space */\n-static const char *get_rev_name(const struct object *o, struct strbuf *buf)\n+static const char *get_rev_name(const struct object *o,\n+\t\t\t\tstruct pretty_format *format_ctx,\n+\t\t\t\tstruct strbuf *buf)\n {\n \tstruct rev_name *n;\n \tconst struct commit *c;\n@@ -462,6 +477,25 @@ static const char *get_rev_name(const struct object *o, struct strbuf *buf)\n \tif (o->type != OBJ_COMMIT)\n \t\treturn get_exact_ref_match(o);\n \tc = (const struct commit *) o;\n+\n+\tif (format_ctx) {\n+\t\tstrbuf_reset(buf);\n+\n+\t\tif (format_ctx->want.notes) {\n+\t\t\tstruct strbuf notebuf = STRBUF_INIT;\n+\n+\t\t\tformat_display_notes(&c->object.oid, &notebuf,\n+\t\t\t\t\t     get_log_output_encoding(),\n+\t\t\t\t\t     format_ctx->ctx.fmt == CMIT_FMT_USERFORMAT);\n+\t\t\tformat_ctx->ctx.notes_message = strbuf_detach(&notebuf, NULL);\n+\t\t}\n+\n+\t\tpretty_print_commit(&format_ctx->ctx, c, buf);\n+\t\tFREE_AND_NULL(format_ctx->ctx.notes_message);\n+\n+\t\treturn buf->buf;\n+\t}\n+\n \tn = get_commit_rev_name(c);\n \tif (!n)\n \t\treturn NULL;\n@@ -479,6 +513,7 @@ static const char *get_rev_name(const struct object *o, struct strbuf *buf)\n \n static void show_name(const struct object *obj,\n \t\t      const char *caller_name,\n+\t\t      struct pretty_format *format_ctx,\n \t\t      int always, int allow_undefined, int name_only)\n {\n \tconst char *name;\n@@ -487,7 +522,7 @@ static void show_name(const struct object *obj,\n \n \tif (!name_only)\n \t\tprintf(\"%s \", caller_name ? caller_name : oid_to_hex(oid));\n-\tname = get_rev_name(obj, &buf);\n+\tname = get_rev_name(obj, format_ctx, &buf);\n \tif (name)\n \t\tprintf(\"%s\\n\", name);\n \telse if (allow_undefined)\n@@ -507,7 +542,9 @@ static char const * const name_rev_usage[] = {\n \tNULL\n };\n \n-static void name_rev_line(char *p, struct name_ref_data *data)\n+static void name_rev_line(char *p,\n+\t\t\t  struct name_ref_data *data,\n+\t\t\t  struct pretty_format *format_ctx)\n {\n \tstruct strbuf buf = STRBUF_INIT;\n \tint counter = 0;\n@@ -532,7 +569,7 @@ static void name_rev_line(char *p, struct name_ref_data *data)\n \t\t\t\tstruct object *o =\n \t\t\t\t\tlookup_object(the_repository, &oid);\n \t\t\t\tif (o)\n-\t\t\t\t\tname = get_rev_name(o, &buf);\n+\t\t\t\t\tname = get_rev_name(o, format_ctx, &buf);\n \t\t\t}\n \t\t\t*(p+1) = c;\n \n@@ -554,6 +591,16 @@ static void name_rev_line(char *p, struct name_ref_data *data)\n \tstrbuf_release(&buf);\n }\n \n+static int format_cb(const struct option *option,\n+\t\t     const char *arg,\n+\t\t     int unset)\n+{\n+\tstruct format_cb_data *data = option->value;\n+\tdata->format = arg;\n+\t*data->name_only = !unset;\n+\treturn 0;\n+}\n+\n int cmd_name_rev(int argc,\n \t\t const char **argv,\n \t\t const char *prefix,\n@@ -567,6 +614,12 @@ int cmd_name_rev(int argc,\n #endif\n \tint all = 0, annotate_stdin = 0, allow_undefined = 1, always = 0, peel_tag = 0;\n \tstruct name_ref_data data = { 0, 0, STRING_LIST_INIT_NODUP, STRING_LIST_INIT_NODUP };\n+\tstatic struct format_cb_data format_cb_data = { 0 };\n+\tstruct display_notes_opt format_notes_opt;\n+\tstruct rev_info format_rev = REV_INFO_INIT;\n+\tstruct pretty_format *format_ctx = NULL;\n+\tstruct pretty_format format_pp = { 0 };\n+\tstruct string_list notes = STRING_LIST_INIT_NODUP;\n \tstruct option opts[] = {\n \t\tOPT_BOOL(0, \"name-only\", &data.name_only, N_(\"print only ref-based names (no object names)\")),\n \t\tOPT_BOOL(0, \"tags\", &data.tags_only, N_(\"only use tags to name the commits\")),\n@@ -584,6 +637,10 @@ int cmd_name_rev(int argc,\n \t\t\t   PARSE_OPT_HIDDEN),\n #endif /* WITH_BREAKING_CHANGES */\n \t\tOPT_BOOL(0, \"annotate-stdin\", &annotate_stdin, N_(\"annotate text from stdin\")),\n+\t\tOPT_CALLBACK(0, \"format\", &format_cb_data, N_(\"format\"),\n+\t\t\t     N_(\"pretty-print output instead\"), format_cb),\n+\t\tOPT_STRING_LIST(0, \"notes\", &notes, N_(\"notes\"),\n+\t\t\t\tN_(\"display notes for --format\")),\n \t\tOPT_BOOL(0, \"undefined\", &allow_undefined, N_(\"allow to print `undefined` names (default)\")),\n \t\tOPT_BOOL(0, \"always\",     &always,\n \t\t\t   N_(\"show abbreviated commit object as fallback\")),\n@@ -592,6 +649,8 @@ int cmd_name_rev(int argc,\n \t\tOPT_END(),\n \t};\n \n+\tinit_display_notes(&format_notes_opt);\n+\tformat_cb_data.name_only = &data.name_only;\n \tmem_pool_init(&string_pool, 0);\n \tinit_commit_rev_name(&rev_names);\n \trepo_config(the_repository, git_default_config, NULL);\n@@ -606,6 +665,31 @@ int cmd_name_rev(int argc,\n \t}\n #endif\n \n+\tif (format_cb_data.format) {\n+\t\tget_commit_format(format_cb_data.format, &format_rev);\n+\t\tformat_pp.ctx.rev = &format_rev;\n+\t\tformat_pp.ctx.fmt = format_rev.commit_format;\n+\t\tformat_pp.ctx.abbrev = format_rev.abbrev;\n+\t\tformat_pp.ctx.date_mode_explicit = format_rev.date_mode_explicit;\n+\t\tformat_pp.ctx.date_mode = format_rev.date_mode;\n+\t\tformat_pp.ctx.color = GIT_COLOR_AUTO;\n+\n+\t\tuserformat_find_requirements(format_cb_data.format,\n+\t\t\t\t\t     &format_pp.want);\n+\t\tif (format_pp.want.notes) {\n+\t\t\tint ignore_show_notes = 0;\n+\t\t\tstruct string_list_item *n;\n+\n+\t\t\tfor_each_string_list_item(n, &notes)\n+\t\t\t\tenable_ref_display_notes(&format_notes_opt,\n+\t\t\t\t\t\t\t &ignore_show_notes,\n+\t\t\t\t\t\t\t n->string);\n+\t\t\tload_display_notes(&format_notes_opt);\n+\t\t}\n+\n+\t\tformat_ctx = &format_pp;\n+\t}\n+\n \tif (all + annotate_stdin + !!argc > 1) {\n \t\terror(\"Specify either a list, or --all, not both!\");\n \t\tusage_with_options(name_rev_usage, opts);\n@@ -663,7 +747,7 @@ int cmd_name_rev(int argc,\n \n \t\twhile (strbuf_getline(&sb, stdin) != EOF) {\n \t\t\tstrbuf_addch(&sb, '\\n');\n-\t\t\tname_rev_line(sb.buf, &data);\n+\t\t\tname_rev_line(sb.buf, &data, format_ctx);\n \t\t}\n \t\tstrbuf_release(&sb);\n \t} else if (all) {\n@@ -674,18 +758,20 @@ int cmd_name_rev(int argc,\n \t\t\tstruct object *obj = get_indexed_object(the_repository, i);\n \t\t\tif (!obj || obj->type != OBJ_COMMIT)\n \t\t\t\tcontinue;\n-\t\t\tshow_name(obj, NULL,\n+\t\t\tshow_name(obj, NULL, format_ctx,\n \t\t\t\t  always, allow_undefined, data.name_only);\n \t\t}\n \t} else {\n \t\tint i;\n \t\tfor (i = 0; i < revs.nr; i++)\n-\t\t\tshow_name(revs.objects[i].item, revs.objects[i].name,\n+\t\t\tshow_name(revs.objects[i].item, revs.objects[i].name, format_ctx,\n \t\t\t\t  always, allow_undefined, data.name_only);\n \t}\n \n \tstring_list_clear(&data.ref_filters, 0);\n \tstring_list_clear(&data.exclude_filters, 0);\n+\tstring_list_clear(&notes, 0);\n+\trelease_display_notes(&format_notes_opt);\n \tmem_pool_discard(&string_pool, 0);\n \tobject_array_clear(&revs);\n \treturn 0;\ndiff --git a/t/t6120-describe.sh b/t/t6120-describe.sh\nindex 2c70cc561ad..0b7e9fe396d 100755\n--- a/t/t6120-describe.sh\n+++ b/t/t6120-describe.sh\n@@ -658,6 +658,102 @@ test_expect_success 'name-rev --annotate-stdin works with commitGraph' '\n \t)\n '\n \n+test_expect_success 'name-rev --format setup' '\n+\tmkdir repo-format &&\n+\tgit -C repo-format init &&\n+\ttest_commit -C repo-format first &&\n+\ttest_commit -C repo-format second &&\n+\ttest_commit -C repo-format third &&\n+\ttest_commit -C repo-format fourth &&\n+\ttest_commit -C repo-format fifth &&\n+\ttest_commit -C repo-format sixth &&\n+\ttest_commit -C repo-format seventh &&\n+\ttest_commit -C repo-format eighth\n+'\n+\n+test_expect_success 'name-rev --format --no-name-only' '\n+\tcat >expect <<-\\EOF &&\n+\tHEAD~3 [fifth]\n+\tHEAD [eighth]\n+\tHEAD~5 [third]\n+\tEOF\n+\tgit -C repo-format name-rev --format=\"[%s]\" \\\n+\t\t--no-name-only HEAD~3 HEAD HEAD~5 >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'name-rev --format --no-format is the same as regular name-rev' '\n+\tgit -C repo-format name-rev HEAD~2 HEAD~3 >expect &&\n+\ttest_file_not_empty expect &&\n+\tgit -C repo-format name-rev --format=\"huh?\" \\\n+\t\t--no-format HEAD~2 HEAD~3 >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'name-rev --format=%s for argument revs' '\n+\tcat >expect <<-\\EOF &&\n+\teighth\n+\tseventh\n+\tfifth\n+\tEOF\n+\tgit -C repo-format name-rev --format=%s \\\n+\t\tHEAD HEAD~ HEAD~3 >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success '--name-rev --format=reference --annotate-stdin from rev-list same as log' '\n+\tgit -C repo-format log --format=reference >expect &&\n+\ttest_file_not_empty expect &&\n+\tgit -C repo-format rev-list HEAD >list &&\n+\tgit -C repo-format name-rev --format=reference \\\n+\t\t--annotate-stdin <list >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success '--name-rev --format=<pretty> --annotate-stdin with running text and tree oid' '\n+\tcmit_oid=$(git -C repo-format rev-parse :/fifth) &&\n+\treference=$(git -C repo-format log -n1 --format=reference :/fifth) &&\n+\ttree=$(git -C repo-format rev-parse HEAD^{tree}) &&\n+\tcat >expect <<-EOF &&\n+\tWe thought we fixed this in ${reference}.\n+\tBut look at this tree: ${tree}.\n+\tEOF\n+\tgit -C repo-format name-rev --format=reference --annotate-stdin \\\n+\t\t>actual <<-EOF &&\n+\tWe thought we fixed this in ${cmit_oid}.\n+\tBut look at this tree: ${tree}.\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'name-rev --format=<pretty> with %N (note)' '\n+\ttest_when_finished \"git -C repo-format notes remove\" &&\n+\tgit -C repo-format notes add -m\"Make a note\" &&\n+\tprintf \"Make a note\\n\\n\\n\" >expect &&\n+\tgit -C repo-format name-rev --format=\"tformat:%N\" \\\n+\t\tHEAD HEAD~ >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'name-rev --format=<pretty> --notes<ref>' '\n+\t# One custom notes ref\n+\ttest_when_finished \"git -C repo-format notes remove\" &&\n+\ttest_when_finished \"git -C repo-format notes --ref=word remove\" &&\n+\tgit -C repo-format notes add -m\"default\" &&\n+\tgit -C repo-format notes --ref=word add -m\"custom\" &&\n+\tprintf \"custom\\n\\n\" >expect &&\n+\tgit -C repo-format name-rev --format=\"tformat:%N\" \\\n+\t\t--notes=word \\\n+\t\tHEAD >actual &&\n+\ttest_cmp expect actual &&\n+\t# Glob all\n+\tprintf \"default\\ncustom\\n\\n\" >expect &&\n+\tgit -C repo-format name-rev --format=\"tformat:%N\" \\\n+\t\t--notes=* \\\n+\t\tHEAD >actual &&\n+\ttest_cmp expect actual\n+'\n+\n #               B\n #               o\n #  H             \\\n-- \n2.53.0.32.gf6228eaf9cc\n\n"},{"id":"539559","messageId":"CALnO6CB5WOTp_e7Kv3CrEbQ+3XE-gDxNVHf7qATBEbyKWfxpLg@mail.gmail.com","threadId":"65237","inReplyTo":"V2_name-rev_--format.51d@msgid.xyz","subject":"Re: [PATCH v2 2/2] name-rev: learn --format=<pretty>","fromName":"D. Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2026-03-20T15:25:37Z","receivedAt":"2026-03-20T15:25:51Z","isPatch":true,"body":"On Fri, Mar 20, 2026 at 9:13 AM <kristofferhaugsbakk@fastmail.com> wrote:\n>\n> From: Kristoffer Haugsbakk <code@khaugsbakk.name>\n>\n> Teach git-name-rev(1) to format the given revisions instead of creating\n> symbolic names.\n[snip]\n> ---\n>\n> Notes (series):\n>     v2:\n>     • Propely implement “--format implies --name-only”\n>     • Don’t use a needless intermediary struct\n>     • Add a new member to `name_rev_usage[]`\n>     • FREE_AND_NULL notes string. There is no use-after-free but we have no\n>       reason to leave a freed pointer just laying there\n>     • Implement `--notes` for `%N` atom use (not just restrict to the default\n>       notes ref)\n[snip]\n> diff --git a/Documentation/git-name-rev.adoc b/Documentation/git-name-rev.adoc\n> index d4f1c4d5945..65348690c8c 100644\n> --- a/Documentation/git-name-rev.adoc\n> +++ b/Documentation/git-name-rev.adoc\n> @@ -9,7 +9,7 @@ git-name-rev - Find symbolic names for given revs\n>  SYNOPSIS\n>  --------\n>  [verse]\n> -'git name-rev' [--tags] [--refs=<pattern>]\n> +'git name-rev' [--tags] [--refs=<pattern>] [--format=<pretty>]\n>                ( --all | --annotate-stdin | <commit-ish>... )\n>\n>  DESCRIPTION\n> @@ -21,6 +21,14 @@ format parsable by 'git rev-parse'.\n>  OPTIONS\n>  -------\n>\n> +--format=<pretty>::\n> +--no-format::\n> +       Format revisions instead of outputting symbolic names. The\n> +       default is `--no-format`.\n> ++\n> +Implies `--name-only`. The negation `--no-format` implies\n> +`--no-name-only` (the default for the command).\n> +\n>  --tags::\n>         Do not use branch names, but only tags to name the commits\n\nIIUC that this patch also adds --notes, should it be documented here?\n"},{"id":"539771","messageId":"1873f57e-76b4-48d0-8034-73f72f5fe93d@app.fastmail.com","threadId":"65237","inReplyTo":"CALnO6CB5WOTp_e7Kv3CrEbQ+3XE-gDxNVHf7qATBEbyKWfxpLg@mail.gmail.com","subject":"Re: [PATCH v2 2/2] name-rev: learn --format=<pretty>","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-03-23T17:34:44Z","receivedAt":"2026-03-23T17:35:06Z","isPatch":true,"body":"On Fri, Mar 20, 2026, at 16:25, D. Ben Knoble wrote:\n>>[snip]\n>> +--format=<pretty>::\n>> +--no-format::\n>> +       Format revisions instead of outputting symbolic names. The\n>> +       default is `--no-format`.\n>> ++\n>> +Implies `--name-only`. The negation `--no-format` implies\n>> +`--no-name-only` (the default for the command).\n>> +\n>>  --tags::\n>>         Do not use branch names, but only tags to name the commits\n>\n> IIUC that this patch also adds --notes, should it be documented here?\n\nYes certainly. It dropped from my mind when doing the last session for\nthis version.\n\nThanks for spotting.\n"},{"id":"542452","messageId":"V3_CV_format-rev.66a@msgid.xyz","threadId":"65237","inReplyTo":"V2_CV_name-rev_--format.51b@msgid.xyz","subject":"[PATCH v3 0/5] format-rev: introduce builtin for on-demand pretty formatting","fromName":"","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-04-28T22:25:51Z","receivedAt":"2026-04-28T22:26:30Z","isPatch":true,"body":"From: Kristoffer Haugsbakk <code@khaugsbakk.name>\n\n(Previous subject: name-rev: learn --format=<pretty>)\n\nTopic name (applied): kh/name-rev-custom-format\n\nTopic summary: Introduce a new builtin for pretty formatting one revision\nexpression per line or commit object names found in running text.\n\nSee the last patch for the motivation. In short there isn’t anything that I\nhave found that lets you format however many commits you want through one\nprocess (so looping over `git show` is excepted).\n\nThe other patches prepare for this change.\n\n§ Changes in v3\n\nRewrite from a git-name-rev(1) option add-on to a new builtin. I make room\nin `builtin/name-rev.c` for it.\n\nThis was based on this part from the previous cover letter:\n\n    git(1) already has a lot of commands. This doesn’t expand the footprint. On\n    the other hand, I might be naive about what a proper formatter needs in\n    terms of command options. Right now there is the helper `--notes`. But is\n    more needed?\n\n    • --abbrev ?\n    • --date ?\n\n    I don’t know. Maybe at some point this would be too much, too crowded, for\n    git-name-rev(1). Then it might make sense to split it out to a separate\n    builtin?\n\nI did not want to expand this git-name-rev(1) command with more options\nspecific to pretty formatting.\n\n§ Why experimental command?\n\nThis command is marked Experimental three times (like git-replay(1)).[1]\nNot so much because the UI design seems difficult. It’s more so that it can\nbe thrown out if it doesn’t end up being worth having around.\n\n(*Experimental* also implies wholesale trashing. Right? That’s one possible\nchange in behavior.)\n\nOr the behavior here could be moved somewhere else.\n\n† 1: I vaguely recall reading a good argument for emphasizing this on the\n     mailing list. I wasn’t able to find back to it right now.\n\n§ Outstanding work\n\nI could look more into how to do the translation strings for “X is\nrequire” (arg), I’m probably missing something that already exists. I could\nprobably also link to other commands on the git-format-rev(1) doc. But\nright now I wanted to get this out there. There might not be any appetite\nfor builtin number 148 for this particular niche anyway.\n\n§ Link to v2\n\nhttps://lore.kernel.org/git/V2_CV_name-rev_--format.51b@msgid.xyz/\n\n[1/5] name-rev: wrap both blocks in braces\n[2/5] name-rev: run clang-format before factoring code\n[3/5] name-rev: factor code for sharing with a new command\n[4/5] name-rev: make dedicated --annotate-stdin --name-only test\n[5/5] format-rev: introduce builtin for on-demand pretty formatting\n\n Documentation/git-format-rev.adoc | 148 +++++++++++++++++\n Makefile                          |   1 +\n builtin.h                         |   1 +\n builtin/name-rev.c                | 264 +++++++++++++++++++++++++++---\n command-list.txt                  |   1 +\n git.c                             |   1 +\n t/t1517-outside-repo.sh           |   3 +-\n t/t6120-describe.sh               | 118 +++++++++++++\n 8 files changed, 511 insertions(+), 26 deletions(-)\n create mode 100644 Documentation/git-format-rev.adoc\n\nInterdiff against v2:\ndiff --git a/Documentation/git-format-rev.adoc b/Documentation/git-format-rev.adoc\nnew file mode 100644\nindex 00000000000..d960001d750\n--- /dev/null\n+++ b/Documentation/git-format-rev.adoc\n@@ -0,0 +1,148 @@\n+git-format-rev(1)\n+=================\n+\n+NAME\n+----\n+git-format-rev - EXPERIMENTAL: Pretty format revisions on demand\n+\n+\n+SYNOPSIS\n+--------\n+[synopsis]\n+(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> --format=<pretty> [--notes=<ref>]\n+\n+DESCRIPTION\n+-----------\n+\n+Pretty format revisions from standard input.\n+\n+THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.\n+\n+OPTIONS\n+-------\n+\n+`--stdin-mode=<mode>`::\n+\tHow to interpret standard input data:\n++\n+--\n+`revs`:: Each line is interpreted as a commit. Any kind of revision\n+\texpression can be used (see linkgit:gitrevisions[7]). Annotated\n+\ttags are peeled (see linkgit:gitglossary[7]).\n++\n+The argument `rev` is also accepted.\n+`text`:: Formats all commit object names found in freeform text. These\n+\tmust the full object names, i.e. abbreviated hexidecimal object\n+\tnames will not be interpreted.\n+--\n+\n+`--format=<pretty>`::\n+\tPretty format string.\n+\n+`--notes=<ref>`::\n+`--no-notes`::\n+\tCustom notes ref. Notes are displayed when using the `%N`\n+\tatom. See linkgit:git-notes[1].\n+\n+EXAMPLES\n+--------\n+\n+The command linkgit:git-last-modified[1] shows the commit that each file\n+was last modified in.\n+\n+----\n+$ git last-modified -- README.md Makefile\n+7798034171030be0909c56377a4e0e10e6d2df93\tMakefile\n+c50fbb2dd225e7e82abba4380423ae105089f4d7\tREADME.md\n+----\n+\n+We can pipe the result to this command in order to replace the object\n+name with the commit author.\n+\n+----\n+$ git last-modified -- README.md Makefile |\n+    git format-rev --stdin-mode=text --format=%an\n+Junio C Hamano\tMakefile\n+Todd Zullinger\tREADME.md\n+----\n+\n+Another example is _formatting commits in commit messages_. Given this commit message:\n+\n+----\n+Fix off-by-one error\n+\n+Fix off-by-one error introduced in\n+e83c5163316f89bfbde7d9ab23ca2e25604af290.\n+\n+We thought we fixed this in 5569bf9bbedd63a00780fc5c110e0cfab3aa97b9 but\n+that only covered 1/3 of the faulty cases.\n+----\n+\n+We can format the commits and use par(1) to reflow the text, say in a\n+`commit-msg` hook:\n+\n+----\n+$ git config set hook.reference-commits.event commit-msg\n+$ git config set hook.reference-commits.command reference-commits\n+$ cat $(which reference-commits)\n+#/bin/sh\n+\n+msg=\"$1\"\n+rewritten=$(mktemp)\n+git format-rev --stdin-mode=text --format=reference <\"$msg\" |\n+    par >\"$rewritten\"\n+mv \"$rewritten\" \"$msg\"\n+----\n+\n+Which will produce something like this:\n+\n+----\n+Fix off-by-one error\n+\n+Fix off-by-one error introduced in e83c5163316 (Implement better memory\n+allocator, 2005-04-07).\n+\n+We thought we fixed this in 5569bf9bbed (Fix memory allocator,\n+2005-06-22) but that only covered 1/3 of the faulty cases.\n+----\n+\n+DISCUSSION\n+----------\n+\n+This command lets you format any number of revisions in any order\n+through one command invocation. Consider the\n+linkgit:git-last-modified[1] case from the \"EXAMPLES\" section above:\n+\n+1. There might be hundreds of files\n+2. Commits can be repeated, i.e. two or more files were last modified in\n+   the same commit\n+\n+Two widely-used commands which pretty formats commits are\n+linkgit:git-log[1] and linkgit:git-show[1]. It turns out that they are\n+not a good fit for the above use case.\n+\n+- The output of linkgit:git-last-modified[1] would have to be processed\n+  in stages since you need to transform the first column separately and\n+  then link the author to the filename. But this is surmountable.\n+- You can feed each commit to `git show` or `git show --no-walk -1`. But\n+  that means that you need to create a process for each line.\n+- Let’s say that you want to use one process, not one per line. So you\n+  want to feed all the commits to the command. Now you face the problem\n+  that you have to feed all the commits to the commands before you get\n+  any output (this is also the case for the `--stdin` modes). In other\n+  words, you cannot loop through each line, get the author for the\n+  commit, and output the author and the filename. You need to feed all\n+  the commits, get back all the output, and match the output with the\n+  filename.\n+- But the next problem is that commands will deduplicate the input and\n+  only output one commit one single time only. Thus you cannot make the\n+  output order match the input order, since a commit could have been\n+  repeated in the original input.\n+\n+In short, it is straightforward to use these two commands if you use one\n+process per line. It is much more work if you just want to use one\n+process, but still doable. In contrast, this problem is just another\n+shell pipeline with this command.\n+\n+GIT\n+---\n+Part of the linkgit:git[1] suite\ndiff --git a/Documentation/git-name-rev.adoc b/Documentation/git-name-rev.adoc\nindex 65348690c8c..d4f1c4d5945 100644\n--- a/Documentation/git-name-rev.adoc\n+++ b/Documentation/git-name-rev.adoc\n@@ -9,7 +9,7 @@ git-name-rev - Find symbolic names for given revs\n SYNOPSIS\n --------\n [verse]\n-'git name-rev' [--tags] [--refs=<pattern>] [--format=<pretty>]\n+'git name-rev' [--tags] [--refs=<pattern>]\n \t       ( --all | --annotate-stdin | <commit-ish>... )\n \n DESCRIPTION\n@@ -21,14 +21,6 @@ format parsable by 'git rev-parse'.\n OPTIONS\n -------\n \n---format=<pretty>::\n---no-format::\n-\tFormat revisions instead of outputting symbolic names. The\n-\tdefault is `--no-format`.\n-+\n-Implies `--name-only`. The negation `--no-format` implies\n-`--no-name-only` (the default for the command).\n-\n --tags::\n \tDo not use branch names, but only tags to name the commits\n \ndiff --git a/Makefile b/Makefile\nindex 15b1ded1a0b..cbaf91fd846 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -895,6 +895,7 @@ BUILT_INS += $(patsubst builtin/%.o,git-%$X,$(BUILTIN_OBJS))\n BUILT_INS += git-cherry$X\n BUILT_INS += git-cherry-pick$X\n BUILT_INS += git-format-patch$X\n+BUILT_INS += git-format-rev$X\n BUILT_INS += git-fsck-objects$X\n BUILT_INS += git-init$X\n BUILT_INS += git-maintenance$X\ndiff --git a/builtin.h b/builtin.h\nindex 235c51f30e5..63813c90125 100644\n--- a/builtin.h\n+++ b/builtin.h\n@@ -189,6 +189,7 @@ int cmd_fmt_merge_msg(int argc, const char **argv, const char *prefix, struct re\n int cmd_for_each_ref(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_for_each_repo(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_format_patch(int argc, const char **argv, const char *prefix, struct repository *repo);\n+int cmd_format_rev(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_fsck(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_fsmonitor__daemon(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_gc(int argc, const char **argv, const char *prefix, struct repository *repo);\ndiff --git a/builtin/name-rev.c b/builtin/name-rev.c\nindex 9a008d8b7a8..b60cc766279 100644\n--- a/builtin/name-rev.c\n+++ b/builtin/name-rev.c\n@@ -36,16 +36,6 @@ struct rev_name {\n \tint from_tag;\n };\n \n-struct pretty_format {\n-\tstruct pretty_print_context ctx;\n-\tstruct userformat_want want;\n-};\n-\n-struct format_cb_data {\n-    const char *format;\n-    int *name_only;\n-};\n-\n define_commit_slab(commit_rev_name, struct rev_name);\n \n static timestamp_t generation_cutoff = GENERATION_NUMBER_INFINITY;\n@@ -285,6 +275,43 @@ struct name_ref_data {\n \tstruct string_list exclude_filters;\n };\n \n+struct pretty_format {\n+\tstruct pretty_print_context ctx;\n+\tstruct userformat_want want;\n+};\n+\n+enum command_type {\n+\tNAME_REV = 1,\n+\tFORMAT_REV = 2,\n+};\n+\n+enum stdin_mode {\n+    TEXT = 1,\n+    REVS = 2,\n+};\n+\n+struct command {\n+\tenum command_type type;\n+\tunion {\n+\t\tint name_only;\n+\t\tstruct pretty_format *pretty_format;\n+\t} u;\n+};\n+\n+static void init_name_rev_command(struct command *cmd,\n+\t\t\t\t  int name_only)\n+{\n+\tcmd->type = NAME_REV;\n+\tcmd->u.name_only = name_only;\n+}\n+\n+static void init_format_rev_command(struct command *cmd,\n+\t\t\t\t    struct pretty_format *pretty_format)\n+{\n+\tcmd->type = FORMAT_REV;\n+\tcmd->u.pretty_format = pretty_format;\n+}\n+\n static struct tip_table {\n \tstruct tip_table_entry {\n \t\tstruct object_id oid;\n@@ -467,9 +494,7 @@ static const char *get_exact_ref_match(const struct object *o)\n }\n \n /* may return a constant string or use \"buf\" as scratch space */\n-static const char *get_rev_name(const struct object *o,\n-\t\t\t\tstruct pretty_format *format_ctx,\n-\t\t\t\tstruct strbuf *buf)\n+static const char *get_rev_name(const struct object *o, struct strbuf *buf)\n {\n \tstruct rev_name *n;\n \tconst struct commit *c;\n@@ -477,25 +502,6 @@ static const char *get_rev_name(const struct object *o,\n \tif (o->type != OBJ_COMMIT)\n \t\treturn get_exact_ref_match(o);\n \tc = (const struct commit *) o;\n-\n-\tif (format_ctx) {\n-\t\tstrbuf_reset(buf);\n-\n-\t\tif (format_ctx->want.notes) {\n-\t\t\tstruct strbuf notebuf = STRBUF_INIT;\n-\n-\t\t\tformat_display_notes(&c->object.oid, &notebuf,\n-\t\t\t\t\t     get_log_output_encoding(),\n-\t\t\t\t\t     format_ctx->ctx.fmt == CMIT_FMT_USERFORMAT);\n-\t\t\tformat_ctx->ctx.notes_message = strbuf_detach(&notebuf, NULL);\n-\t\t}\n-\n-\t\tpretty_print_commit(&format_ctx->ctx, c, buf);\n-\t\tFREE_AND_NULL(format_ctx->ctx.notes_message);\n-\n-\t\treturn buf->buf;\n-\t}\n-\n \tn = get_commit_rev_name(c);\n \tif (!n)\n \t\treturn NULL;\n@@ -511,9 +517,29 @@ static const char *get_rev_name(const struct object *o,\n \t}\n }\n \n+static const char *get_format_rev(const struct commit *c,\n+\t\t\t\t  struct pretty_format *format_ctx,\n+\t\t\t\t  struct strbuf *buf)\n+{\n+\tstrbuf_reset(buf);\n+\n+\tif (format_ctx->want.notes) {\n+\t\tstruct strbuf notebuf = STRBUF_INIT;\n+\n+\t\tformat_display_notes(&c->object.oid, &notebuf,\n+\t\t\t\t     get_log_output_encoding(),\n+\t\t\t\t     format_ctx->ctx.fmt == CMIT_FMT_USERFORMAT);\n+\t\tformat_ctx->ctx.notes_message = strbuf_detach(&notebuf, NULL);\n+\t}\n+\n+\tpretty_print_commit(&format_ctx->ctx, c, buf);\n+\tFREE_AND_NULL(format_ctx->ctx.notes_message);\n+\n+\treturn buf->buf;\n+}\n+\n static void show_name(const struct object *obj,\n \t\t      const char *caller_name,\n-\t\t      struct pretty_format *format_ctx,\n \t\t      int always, int allow_undefined, int name_only)\n {\n \tconst char *name;\n@@ -522,7 +548,7 @@ static void show_name(const struct object *obj,\n \n \tif (!name_only)\n \t\tprintf(\"%s \", caller_name ? caller_name : oid_to_hex(oid));\n-\tname = get_rev_name(obj, format_ctx, &buf);\n+\tname = get_rev_name(obj, &buf);\n \tif (name)\n \t\tprintf(\"%s\\n\", name);\n \telse if (allow_undefined)\n@@ -542,9 +568,7 @@ static char const * const name_rev_usage[] = {\n \tNULL\n };\n \n-static void name_rev_line(char *p,\n-\t\t\t  struct name_ref_data *data,\n-\t\t\t  struct pretty_format *format_ctx)\n+static void name_rev_line(char *p, struct command *cmd)\n {\n \tstruct strbuf buf = STRBUF_INIT;\n \tint counter = 0;\n@@ -553,33 +577,54 @@ static void name_rev_line(char *p,\n \n \tfor (p_start = p; *p; p++) {\n #define ishex(x) (isdigit((x)) || ((x) >= 'a' && (x) <= 'f'))\n+\tstart:\n \t\tif (!ishex(*p)) {\n \t\t\tcounter = 0;\n \t\t} else if (++counter == hexsz &&\n-\t\t\t !ishex(*(p+1))) {\n+\t\t\t   !ishex(*(p + 1))) {\n \t\t\tstruct object_id oid;\n \t\t\tconst char *name = NULL;\n-\t\t\tchar c = *(p+1);\n+\t\t\tchar c = *(p + 1);\n \t\t\tint p_len = p - p_start + 1;\n+\t\t\tstruct object *o = NULL;\n+\t\t\tint oid_ret = 1;\n \n \t\t\tcounter = 0;\n \n-\t\t\t*(p+1) = 0;\n-\t\t\tif (!repo_get_oid(the_repository, p - (hexsz - 1), &oid)) {\n-\t\t\t\tstruct object *o =\n-\t\t\t\t\tlookup_object(the_repository, &oid);\n+\t\t\t*(p + 1) = 0;\n+\t\t\toid_ret = repo_get_oid(the_repository, p - (hexsz - 1), &oid);\n+\n+\t\t\tswitch (cmd->type) {\n+\t\t\tcase NAME_REV:\n+\t\t\t\tif (!oid_ret)\n+\t\t\t\t\to = lookup_object(the_repository, &oid);\n \t\t\t\tif (o)\n-\t\t\t\t\tname = get_rev_name(o, format_ctx, &buf);\n+\t\t\t\t\tname = get_rev_name(o, &buf);\n+\t\t\t\t*(p + 1) = c;\n+\t\t\t\tif (!name)\n+\t\t\t\t\tgoto start;\n+\t\t\t\tif (cmd->u.name_only)\n+\t\t\t\t\tprintf(\"%.*s%s\", p_len - hexsz, p_start, name);\n+\t\t\t\telse\n+\t\t\t\t\tprintf(\"%.*s (%s)\", p_len, p_start, name);\n+\t\t\t\tbreak;\n+\t\t\tcase FORMAT_REV:\n+\t\t\t\tif (!oid_ret)\n+\t\t\t\t\to = parse_object(the_repository, &oid);\n+\t\t\t\tif (o && o->type == OBJ_COMMIT)\n+\t\t\t\t\tname = get_format_rev((const struct commit *)o,\n+\t\t\t\t\t\t\t      cmd->u.pretty_format,\n+\t\t\t\t\t\t\t      &buf);\n+\t\t\t\t*(p + 1) = c;\n+\t\t\t\tif (name)\n+\t\t\t\t\tprintf(\"%.*s%s\", p_len - hexsz, p_start, name);\n+\t\t\t\telse\n+\t\t\t\t\tprintf(\"%.*s\", p_len, p_start);\n+\t\t\t\tbreak;\n+\t\t\tdefault:\n+\t\t\t\tBUG(\"uncovered case: %d\", cmd->type);\n \t\t\t}\n-\t\t\t*(p+1) = c;\n \n-\t\t\tif (!name)\n-\t\t\t\tcontinue;\n-\n-\t\t\tif (data->name_only)\n-\t\t\t\tprintf(\"%.*s%s\", p_len - hexsz, p_start, name);\n-\t\t\telse\n-\t\t\t\tprintf(\"%.*s (%s)\", p_len, p_start, name);\n \t\t\tp_start = p + 1;\n \t\t}\n \t}\n@@ -591,16 +636,6 @@ static void name_rev_line(char *p,\n \tstrbuf_release(&buf);\n }\n \n-static int format_cb(const struct option *option,\n-\t\t     const char *arg,\n-\t\t     int unset)\n-{\n-\tstruct format_cb_data *data = option->value;\n-\tdata->format = arg;\n-\t*data->name_only = !unset;\n-\treturn 0;\n-}\n-\n int cmd_name_rev(int argc,\n \t\t const char **argv,\n \t\t const char *prefix,\n@@ -614,19 +649,14 @@ int cmd_name_rev(int argc,\n #endif\n \tint all = 0, annotate_stdin = 0, allow_undefined = 1, always = 0, peel_tag = 0;\n \tstruct name_ref_data data = { 0, 0, STRING_LIST_INIT_NODUP, STRING_LIST_INIT_NODUP };\n-\tstatic struct format_cb_data format_cb_data = { 0 };\n-\tstruct display_notes_opt format_notes_opt;\n-\tstruct rev_info format_rev = REV_INFO_INIT;\n-\tstruct pretty_format *format_ctx = NULL;\n-\tstruct pretty_format format_pp = { 0 };\n-\tstruct string_list notes = STRING_LIST_INIT_NODUP;\n+\tstruct command cmd;\n \tstruct option opts[] = {\n \t\tOPT_BOOL(0, \"name-only\", &data.name_only, N_(\"print only ref-based names (no object names)\")),\n \t\tOPT_BOOL(0, \"tags\", &data.tags_only, N_(\"only use tags to name the commits\")),\n \t\tOPT_STRING_LIST(0, \"refs\", &data.ref_filters, N_(\"pattern\"),\n-\t\t\t\t   N_(\"only use refs matching <pattern>\")),\n+\t\t\t\tN_(\"only use refs matching <pattern>\")),\n \t\tOPT_STRING_LIST(0, \"exclude\", &data.exclude_filters, N_(\"pattern\"),\n-\t\t\t\t   N_(\"ignore refs matching <pattern>\")),\n+\t\t\t\tN_(\"ignore refs matching <pattern>\")),\n \t\tOPT_GROUP(\"\"),\n \t\tOPT_BOOL(0, \"all\", &all, N_(\"list all commits reachable from all refs\")),\n #ifndef WITH_BREAKING_CHANGES\n@@ -637,24 +667,19 @@ int cmd_name_rev(int argc,\n \t\t\t   PARSE_OPT_HIDDEN),\n #endif /* WITH_BREAKING_CHANGES */\n \t\tOPT_BOOL(0, \"annotate-stdin\", &annotate_stdin, N_(\"annotate text from stdin\")),\n-\t\tOPT_CALLBACK(0, \"format\", &format_cb_data, N_(\"format\"),\n-\t\t\t     N_(\"pretty-print output instead\"), format_cb),\n-\t\tOPT_STRING_LIST(0, \"notes\", &notes, N_(\"notes\"),\n-\t\t\t\tN_(\"display notes for --format\")),\n \t\tOPT_BOOL(0, \"undefined\", &allow_undefined, N_(\"allow to print `undefined` names (default)\")),\n-\t\tOPT_BOOL(0, \"always\",     &always,\n-\t\t\t   N_(\"show abbreviated commit object as fallback\")),\n+\t\tOPT_BOOL(0, \"always\", &always,\n+\t\t\t N_(\"show abbreviated commit object as fallback\")),\n \t\tOPT_HIDDEN_BOOL(0, \"peel-tag\", &peel_tag,\n-\t\t\t   N_(\"dereference tags in the input (internal use)\")),\n+\t\t\t\tN_(\"dereference tags in the input (internal use)\")),\n \t\tOPT_END(),\n \t};\n \n-\tinit_display_notes(&format_notes_opt);\n-\tformat_cb_data.name_only = &data.name_only;\n \tmem_pool_init(&string_pool, 0);\n \tinit_commit_rev_name(&rev_names);\n \trepo_config(the_repository, git_default_config, NULL);\n \targc = parse_options(argc, argv, prefix, opts, name_rev_usage, 0);\n+\tinit_name_rev_command(&cmd, data.name_only);\n \n #ifndef WITH_BREAKING_CHANGES\n \tif (transform_stdin) {\n@@ -665,31 +690,6 @@ int cmd_name_rev(int argc,\n \t}\n #endif\n \n-\tif (format_cb_data.format) {\n-\t\tget_commit_format(format_cb_data.format, &format_rev);\n-\t\tformat_pp.ctx.rev = &format_rev;\n-\t\tformat_pp.ctx.fmt = format_rev.commit_format;\n-\t\tformat_pp.ctx.abbrev = format_rev.abbrev;\n-\t\tformat_pp.ctx.date_mode_explicit = format_rev.date_mode_explicit;\n-\t\tformat_pp.ctx.date_mode = format_rev.date_mode;\n-\t\tformat_pp.ctx.color = GIT_COLOR_AUTO;\n-\n-\t\tuserformat_find_requirements(format_cb_data.format,\n-\t\t\t\t\t     &format_pp.want);\n-\t\tif (format_pp.want.notes) {\n-\t\t\tint ignore_show_notes = 0;\n-\t\t\tstruct string_list_item *n;\n-\n-\t\t\tfor_each_string_list_item(n, &notes)\n-\t\t\t\tenable_ref_display_notes(&format_notes_opt,\n-\t\t\t\t\t\t\t &ignore_show_notes,\n-\t\t\t\t\t\t\t n->string);\n-\t\t\tload_display_notes(&format_notes_opt);\n-\t\t}\n-\n-\t\tformat_ctx = &format_pp;\n-\t}\n-\n \tif (all + annotate_stdin + !!argc > 1) {\n \t\terror(\"Specify either a list, or --all, not both!\");\n \t\tusage_with_options(name_rev_usage, opts);\n@@ -747,7 +747,7 @@ int cmd_name_rev(int argc,\n \n \t\twhile (strbuf_getline(&sb, stdin) != EOF) {\n \t\t\tstrbuf_addch(&sb, '\\n');\n-\t\t\tname_rev_line(sb.buf, &data, format_ctx);\n+\t\t\tname_rev_line(sb.buf, &cmd);\n \t\t}\n \t\tstrbuf_release(&sb);\n \t} else if (all) {\n@@ -758,21 +758,149 @@ int cmd_name_rev(int argc,\n \t\t\tstruct object *obj = get_indexed_object(the_repository, i);\n \t\t\tif (!obj || obj->type != OBJ_COMMIT)\n \t\t\t\tcontinue;\n-\t\t\tshow_name(obj, NULL, format_ctx,\n+\t\t\tshow_name(obj, NULL,\n \t\t\t\t  always, allow_undefined, data.name_only);\n \t\t}\n \t} else {\n \t\tint i;\n \t\tfor (i = 0; i < revs.nr; i++)\n-\t\t\tshow_name(revs.objects[i].item, revs.objects[i].name, format_ctx,\n+\t\t\tshow_name(revs.objects[i].item, revs.objects[i].name,\n \t\t\t\t  always, allow_undefined, data.name_only);\n \t}\n \n \tstring_list_clear(&data.ref_filters, 0);\n \tstring_list_clear(&data.exclude_filters, 0);\n-\tstring_list_clear(&notes, 0);\n-\trelease_display_notes(&format_notes_opt);\n \tmem_pool_discard(&string_pool, 0);\n \tobject_array_clear(&revs);\n \treturn 0;\n }\n+\n+static enum stdin_mode parse_stdin_mode(const char *stdin_mode)\n+{\n+\tif (!strcmp(stdin_mode, \"text\"))\n+\t\treturn TEXT;\n+\telse if (!strcmp(stdin_mode, \"revs\") ||\n+\t\t !strcmp(stdin_mode, \"rev\"))\n+\t\treturn REVS;\n+\telse\n+\t\tdie(_(\"'%s' needs to be either text, revs, or rev\"),\n+\t\t    \"--stdin-mode\");\n+}\n+\n+static char const *const format_rev_usage[] = {\n+\tN_(\"(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> --format=<pretty> [--notes=<ref>]\"),\n+\tNULL\n+};\n+\n+int cmd_format_rev(int argc,\n+\t\t   const char **argv,\n+\t\t   const char *prefix,\n+\t\t   struct repository *repo UNUSED)\n+{\n+\tconst char *format = NULL;\n+\tenum stdin_mode stdin_mode;\n+\tconst char *stdin_mode_arg = NULL;\n+\tstruct display_notes_opt format_notes_opt;\n+\tstruct rev_info format_rev = REV_INFO_INIT;\n+\tstruct pretty_format format_pp = { 0 };\n+\tstruct string_list notes = STRING_LIST_INIT_NODUP;\n+\tstruct strbuf scratch_buf = STRBUF_INIT;\n+\tstruct command cmd;\n+\tstruct option opts[] = {\n+\t\tOPT_STRING(0, \"format\", &format, N_(\"format\"),\n+\t\t\t   N_(\"pretty format to use\")),\n+\t\tOPT_STRING(0, \"stdin-mode\", &stdin_mode_arg, N_(\"stdin-mode\"),\n+\t\t\t   N_(\"how revs are processed\")),\n+\t\tOPT_STRING_LIST(0, \"notes\", &notes, N_(\"notes\"),\n+\t\t\t\tN_(\"display notes for pretty format\")),\n+\t\tOPT_END(),\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, opts, format_rev_usage, 0);\n+\n+\tif (argc > 0) {\n+\t\terror(_(\"too many arguments\"));\n+\t\tusage_with_options(format_rev_usage, opts);\n+\t}\n+\n+\tif (!format)\n+\t\tdie(_(\"'%s' is required\"), \"--format\");\n+\tif (!stdin_mode_arg)\n+\t\tdie(_(\"'%s' is required\"), \"--stdin-mode\");\n+\n+\tinit_display_notes(&format_notes_opt);\n+\tstdin_mode = parse_stdin_mode(stdin_mode_arg);\n+\n+\tget_commit_format(format, &format_rev);\n+\tformat_pp.ctx.rev = &format_rev;\n+\tformat_pp.ctx.fmt = format_rev.commit_format;\n+\tformat_pp.ctx.abbrev = format_rev.abbrev;\n+\tformat_pp.ctx.date_mode_explicit = format_rev.date_mode_explicit;\n+\tformat_pp.ctx.date_mode = format_rev.date_mode;\n+\tformat_pp.ctx.color = GIT_COLOR_AUTO;\n+\n+\tuserformat_find_requirements(format,\n+\t\t\t\t     &format_pp.want);\n+\tif (format_pp.want.notes) {\n+\t\tint ignore_show_notes = 0;\n+\t\tstruct string_list_item *n;\n+\n+\t\tfor_each_string_list_item(n, &notes)\n+\t\t\tenable_ref_display_notes(&format_notes_opt,\n+\t\t\t\t\t\t &ignore_show_notes,\n+\t\t\t\t\t\t n->string);\n+\t\tload_display_notes(&format_notes_opt);\n+\t}\n+\n+\tinit_format_rev_command(&cmd, &format_pp);\n+\n+\tswitch (stdin_mode) {\n+\tcase TEXT:\n+\t\twhile (strbuf_getline(&scratch_buf, stdin) != EOF) {\n+\t\t\tstrbuf_addch(&scratch_buf, '\\n');\n+\t\t\tname_rev_line(scratch_buf.buf, &cmd);\n+\t\t}\n+\t\tbreak;\n+\tcase REVS:\n+\t\twhile (strbuf_getline(&scratch_buf, stdin) != EOF) {\n+\t\t\tstruct object_id oid;\n+\t\t\tstruct object *object;\n+\t\t\tstruct object *peeled;\n+\t\t\tstruct commit *commit;\n+\n+\t\t\tif (repo_get_oid(the_repository, scratch_buf.buf, &oid)) {\n+\t\t\t\tfprintf(stderr, \"Could not get sha1 for %s. Skipping.\\n\",\n+\t\t\t\t\tscratch_buf.buf);\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\tobject = parse_object(the_repository, &oid);\n+\t\t\tif (!object) {\n+\t\t\t\tfprintf(stderr, \"Could not get object for %s. Skipping.\\n\",\n+\t\t\t\t\tscratch_buf.buf);\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\tpeeled = deref_tag(the_repository, object, scratch_buf.buf, 0);\n+\t\t\tif (peeled && peeled->type == OBJ_COMMIT)\n+\t\t\t\tcommit = (struct commit *)peeled;\n+\t\t\tif (!commit) {\n+\t\t\t\tfprintf(stderr, \"Could not get commit for %s. Skipping.\\n\",\n+\t\t\t\t\t*argv);\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\tget_format_rev(commit, &format_pp, &scratch_buf);\n+\t\t\tprintf(\"%s\\n\", scratch_buf.buf);\n+\t\t\tstrbuf_release(&scratch_buf);\n+\t\t}\n+\t\tbreak;\n+\tdefault:\n+\t\tBUG(\"uncovered case: %d\", stdin_mode);\n+\t}\n+\n+\tstrbuf_release(&scratch_buf);\n+\tstring_list_clear(&notes, 0);\n+\trelease_display_notes(&format_notes_opt);\n+\treturn 0;\n+}\ndiff --git a/command-list.txt b/command-list.txt\nindex f9005cf4597..df729872dca 100644\n--- a/command-list.txt\n+++ b/command-list.txt\n@@ -108,6 +108,7 @@ git-fmt-merge-msg                       purehelpers\n git-for-each-ref                        plumbinginterrogators\n git-for-each-repo                       plumbinginterrogators\n git-format-patch                        mainporcelain\n+git-format-rev                          plumbinginterrogators\n git-fsck                                ancillaryinterrogators          complete\n git-gc                                  mainporcelain\n git-get-tar-commit-id                   plumbinginterrogators\ndiff --git a/git.c b/git.c\nindex 2b212e6675d..af5b0422b00 100644\n--- a/git.c\n+++ b/git.c\n@@ -578,6 +578,7 @@ static struct cmd_struct commands[] = {\n \t{ \"for-each-ref\", cmd_for_each_ref, RUN_SETUP },\n \t{ \"for-each-repo\", cmd_for_each_repo, RUN_SETUP_GENTLY },\n \t{ \"format-patch\", cmd_format_patch, RUN_SETUP },\n+\t{ \"format-rev\", cmd_format_rev, RUN_SETUP },\n \t{ \"fsck\", cmd_fsck, RUN_SETUP },\n \t{ \"fsck-objects\", cmd_fsck, RUN_SETUP },\n \t{ \"fsmonitor--daemon\", cmd_fsmonitor__daemon, RUN_SETUP },\ndiff --git a/t/t1517-outside-repo.sh b/t/t1517-outside-repo.sh\nindex c824c1a25cf..360a9323343 100755\n--- a/t/t1517-outside-repo.sh\n+++ b/t/t1517-outside-repo.sh\n@@ -114,7 +114,8 @@ do\n \tarchimport | citool | credential-netrc | credential-libsecret | \\\n \tcredential-osxkeychain | cvsexportcommit | cvsimport | cvsserver | \\\n \tdaemon | \\\n-\tdifftool--helper | filter-branch | fsck-objects | get-tar-commit-id | \\\n+\tdifftool--helper | filter-branch | format-rev | fsck-objects | \\\n+\tget-tar-commit-id | \\\n \tgui | gui--askpass | \\\n \thttp-backend | http-fetch | http-push | init-db | \\\n \tmerge-octopus | merge-one-file | merge-resolve | mergetool | \\\ndiff --git a/t/t6120-describe.sh b/t/t6120-describe.sh\nindex 0b7e9fe396d..725f7d81b6b 100755\n--- a/t/t6120-describe.sh\n+++ b/t/t6120-describe.sh\n@@ -298,6 +298,20 @@ test_expect_success 'name-rev --annotate-stdin' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'name-rev --annotate-stdin --name-only' '\n+\t>expect.unsorted &&\n+\tfor rev in $(git rev-list --all)\n+\tdo\n+\t\tname=$(git name-rev --name-only $rev) &&\n+\t\techo \"$name\" >>expect.unsorted || return 1\n+\tdone &&\n+\tsort <expect.unsorted >expect &&\n+\tgit name-rev --annotate-stdin --name-only \\\n+\t\t<list >actual.unsorted &&\n+\tsort <actual.unsorted >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success 'name-rev --stdin deprecated' '\n \tgit rev-list --all >list &&\n \tif ! test_have_prereq WITH_BREAKING_CHANGES\n@@ -658,102 +672,6 @@ test_expect_success 'name-rev --annotate-stdin works with commitGraph' '\n \t)\n '\n \n-test_expect_success 'name-rev --format setup' '\n-\tmkdir repo-format &&\n-\tgit -C repo-format init &&\n-\ttest_commit -C repo-format first &&\n-\ttest_commit -C repo-format second &&\n-\ttest_commit -C repo-format third &&\n-\ttest_commit -C repo-format fourth &&\n-\ttest_commit -C repo-format fifth &&\n-\ttest_commit -C repo-format sixth &&\n-\ttest_commit -C repo-format seventh &&\n-\ttest_commit -C repo-format eighth\n-'\n-\n-test_expect_success 'name-rev --format --no-name-only' '\n-\tcat >expect <<-\\EOF &&\n-\tHEAD~3 [fifth]\n-\tHEAD [eighth]\n-\tHEAD~5 [third]\n-\tEOF\n-\tgit -C repo-format name-rev --format=\"[%s]\" \\\n-\t\t--no-name-only HEAD~3 HEAD HEAD~5 >actual &&\n-\ttest_cmp expect actual\n-'\n-\n-test_expect_success 'name-rev --format --no-format is the same as regular name-rev' '\n-\tgit -C repo-format name-rev HEAD~2 HEAD~3 >expect &&\n-\ttest_file_not_empty expect &&\n-\tgit -C repo-format name-rev --format=\"huh?\" \\\n-\t\t--no-format HEAD~2 HEAD~3 >actual &&\n-\ttest_cmp expect actual\n-'\n-\n-test_expect_success 'name-rev --format=%s for argument revs' '\n-\tcat >expect <<-\\EOF &&\n-\teighth\n-\tseventh\n-\tfifth\n-\tEOF\n-\tgit -C repo-format name-rev --format=%s \\\n-\t\tHEAD HEAD~ HEAD~3 >actual &&\n-\ttest_cmp expect actual\n-'\n-\n-test_expect_success '--name-rev --format=reference --annotate-stdin from rev-list same as log' '\n-\tgit -C repo-format log --format=reference >expect &&\n-\ttest_file_not_empty expect &&\n-\tgit -C repo-format rev-list HEAD >list &&\n-\tgit -C repo-format name-rev --format=reference \\\n-\t\t--annotate-stdin <list >actual &&\n-\ttest_cmp expect actual\n-'\n-\n-test_expect_success '--name-rev --format=<pretty> --annotate-stdin with running text and tree oid' '\n-\tcmit_oid=$(git -C repo-format rev-parse :/fifth) &&\n-\treference=$(git -C repo-format log -n1 --format=reference :/fifth) &&\n-\ttree=$(git -C repo-format rev-parse HEAD^{tree}) &&\n-\tcat >expect <<-EOF &&\n-\tWe thought we fixed this in ${reference}.\n-\tBut look at this tree: ${tree}.\n-\tEOF\n-\tgit -C repo-format name-rev --format=reference --annotate-stdin \\\n-\t\t>actual <<-EOF &&\n-\tWe thought we fixed this in ${cmit_oid}.\n-\tBut look at this tree: ${tree}.\n-\tEOF\n-\ttest_cmp expect actual\n-'\n-\n-test_expect_success 'name-rev --format=<pretty> with %N (note)' '\n-\ttest_when_finished \"git -C repo-format notes remove\" &&\n-\tgit -C repo-format notes add -m\"Make a note\" &&\n-\tprintf \"Make a note\\n\\n\\n\" >expect &&\n-\tgit -C repo-format name-rev --format=\"tformat:%N\" \\\n-\t\tHEAD HEAD~ >actual &&\n-\ttest_cmp expect actual\n-'\n-\n-test_expect_success 'name-rev --format=<pretty> --notes<ref>' '\n-\t# One custom notes ref\n-\ttest_when_finished \"git -C repo-format notes remove\" &&\n-\ttest_when_finished \"git -C repo-format notes --ref=word remove\" &&\n-\tgit -C repo-format notes add -m\"default\" &&\n-\tgit -C repo-format notes --ref=word add -m\"custom\" &&\n-\tprintf \"custom\\n\\n\" >expect &&\n-\tgit -C repo-format name-rev --format=\"tformat:%N\" \\\n-\t\t--notes=word \\\n-\t\tHEAD >actual &&\n-\ttest_cmp expect actual &&\n-\t# Glob all\n-\tprintf \"default\\ncustom\\n\\n\" >expect &&\n-\tgit -C repo-format name-rev --format=\"tformat:%N\" \\\n-\t\t--notes=* \\\n-\t\tHEAD >actual &&\n-\ttest_cmp expect actual\n-'\n-\n #               B\n #               o\n #  H             \\\n@@ -883,4 +801,108 @@ test_expect_success 'do not be fooled by invalid describe format ' '\n \ttest_must_fail git cat-file -t \"refs/tags/super-invalid/./../...../ ~^:/?*[////\\\\\\\\\\\\&}/busted.lock-42-g\"$(cat out)\n '\n \n+test_expect_success 'name-rev --format setup' '\n+\tmkdir repo-format &&\n+\tgit -C repo-format init &&\n+\ttest_commit -C repo-format first &&\n+\ttest_commit -C repo-format second &&\n+\ttest_commit -C repo-format third &&\n+\ttest_commit -C repo-format fourth &&\n+\ttest_commit -C repo-format fifth &&\n+\ttest_commit -C repo-format sixth &&\n+\ttest_commit -C repo-format seventh &&\n+\ttest_commit -C repo-format eighth\n+'\n+\n+test_expect_success 'format-rev --stdin-mode=revs' '\n+\tcat >expect <<-\\EOF &&\n+\teighth\n+\tseventh\n+\tfifth\n+\tEOF\n+\tgit -C repo-format format-rev --stdin-mode=revs \\\n+\t\t--format=%s >actual <<-\\EOF &&\n+\tHEAD\n+\tHEAD~\n+\tHEAD~3\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'format-rev --stdin-mode=text from rev-list same as log' '\n+\tgit -C repo-format log --format=reference >expect &&\n+\ttest_file_not_empty expect &&\n+\tgit -C repo-format rev-list HEAD >list &&\n+\tgit -C repo-format format-rev --stdin-mode=text \\\n+\t\t--format=reference <list >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'format-rev --stdin-mode=text with running text and tree oid' '\n+\tcmit_oid=$(git -C repo-format rev-parse :/fifth) &&\n+\treference=$(git -C repo-format log -n1 --format=reference :/fifth) &&\n+\ttree=$(git -C repo-format rev-parse HEAD^{tree}) &&\n+\tcat >expect <<-EOF &&\n+\tWe thought we fixed this in ${reference}.\n+\tBut look at this tree: ${tree}.\n+\tEOF\n+\tgit -C repo-format format-rev --stdin-mode=text --format=reference \\\n+\t\t>actual <<-EOF &&\n+\tWe thought we fixed this in ${cmit_oid}.\n+\tBut look at this tree: ${tree}.\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'format-rev with %N (note)' '\n+\ttest_when_finished \"git -C repo-format notes remove\" &&\n+\tgit -C repo-format notes add -m\"Make a note\" &&\n+\tprintf \"Make a note\\n\\n\\n\" >expect &&\n+\tgit -C repo-format format-rev --stdin-mode=revs \\\n+\t\t--format=\"tformat:%N\" \\\n+\t\t>actual <<-\\EOF &&\n+\tHEAD\n+\tHEAD~\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'format-rev --notes<ref> (custom notes ref)' '\n+\t# One custom notes ref\n+\ttest_when_finished \"git -C repo-format notes remove\" &&\n+\ttest_when_finished \"git -C repo-format notes --ref=word remove\" &&\n+\tgit -C repo-format notes add -m\"default\" &&\n+\tgit -C repo-format notes --ref=word add -m\"custom\" &&\n+\tprintf \"custom\\n\\n\" >expect &&\n+\tgit -C repo-format format-rev --stdin-mode=revs \\\n+\t\t--format=\"tformat:%N\" \\\n+\t\t--notes=word \\\n+\t\t>actual <<-\\EOF &&\n+\tHEAD\n+\tEOF\n+\ttest_cmp expect actual &&\n+\t# Glob all\n+\tprintf \"default\\ncustom\\n\\n\" >expect &&\n+\tgit -C repo-format format-rev --stdin-mode=revs \\\n+\t\t--format=\"tformat:%N\" \\\n+\t\t--notes=* >actual <<-\\EOF &&\n+\tHEAD\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'format-rev --stdin-mode=revs on annotated tag peels to commit' '\n+\ttest_when_finished \"git -C repo-format tag -d version\" &&\n+\tgit -C repo-format tag -a -m\"new version\" version &&\n+\tcat >expect <<-\\EOF &&\n+\teighth\n+\tEOF\n+\tgit -C repo-format format-rev --stdin-mode=revs \\\n+\t\t--format=%s \\\n+\t\t--notes=* >actual <<-\\EOF &&\n+\tversion\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n test_done\nRange-diff against v2:\n1:  9cb5cfd1ec3 = 1:  9cb5cfd1ec3 name-rev: wrap both blocks in braces\n-:  ----------- > 2:  14900271321 name-rev: run clang-format before factoring code\n-:  ----------- > 3:  1f17f0b0090 name-rev: factor code for sharing with a new command\n-:  ----------- > 4:  40989b3672a name-rev: make dedicated --annotate-stdin --name-only test\n2:  52a52060776 ! 5:  bb54e8f753e name-rev: learn --format=<pretty>\n    @@ Metadata\n     Author: Kristoffer Haugsbakk <code@khaugsbakk.name>\n     \n      ## Commit message ##\n    -    name-rev: learn --format=<pretty>\n    +    format-rev: introduce builtin for on-demand pretty formatting\n     \n    -    Teach git-name-rev(1) to format the given revisions instead of creating\n    -    symbolic names.\n    +    Introduce a new builtin for pretty formatting one revision expression\n    +    per line or commit object names found in running text.\n     \n    -    Sometimes you want to format commits. Most of the time you’re walking\n    -    the graph, e.g. getting a range of commits like `master..topic`. That’s\n    -    a job for git-log(1).\n    +    Sometimes you want to format commits. Most of the time you’re\n    +    walking the graph, e.g. getting a range of commits like\n    +    `master..topic`. That’s a job for git-log(1).\n     \n    -    But sometimes you might want to format commits that you encounter\n    +    But there are times when you want to format commits that you encounter\n         on demand:\n     \n         • Full hashes in running text that you might want to pretty-print\n    -    • git-last-modified(1) outputs full hashes that you can do the same with\n    +    • git-last-modified(1) outputs full hashes that you can do the same\n    +      with\n         • git-cherry(1) has `-v` for commit subject, but maybe you want\n           something else?\n     \n         But now you can’t use git-log(1), git-show(1), or git-rev-list(1):\n     \n    -    • You can’t feed commits piecemeal to these commands, one input for one\n    -      output; they block until standard in is closed\n    +    • You can’t feed commits piecemeal to these commands, one input\n    +      for one output; they block until standard in is closed\n         • You can’t feed a list of possibly duplicate commits, like the output\n           of git-last-modified(1); they effectively deduplicate the output\n     \n         Beyond these two points there’s also the input massage problem: you\n         cannot feed mixed input (revisions mixed with arbitrary text).\n     \n    -    One might hope that git-cat-file(1) can save us. But it doesn’t support\n    -    pretty formats.\n    +    One might hope that git-cat-file(1) can save us. But it doesn’t\n    +    support pretty formats.\n     \n         But there is one command that already both handles revisions as\n    -    arguments, revisions on standard input, and even revisions mixed\n    -    in with arbitrary text. Namely git-name-rev(1).\n    +    arguments, revisions on standard input, and even revisions mixed in\n    +    with arbitrary text. Namely git-name-rev(1): the command for outputting\n    +    symbolic names for commits.\n     \n    -    Teach it to work in a format mode where the output for each revision is\n    -    the pretty output (implies `--name-only`). This can be used to format\n    -    any revision expression when given as arguments, and all full commit\n    -    hashes in running text on stdin.\n    +    We made some room in `builtin/name-rev.c` two commits ago. Let’s\n    +    now add this new git-format-rev(1) command. Taking inspiration from\n    +    git-name-rev(1), there are two modes:\n     \n    -    Just bring the hashes (to the pipeline). We will pretty print them.\n    +    • revs: like git-name-rev(1) in argv mode, but one revision per line\n    +      on standard in\n    +    • text: like git-name-rev(1) with `--annotate-stdin`\n    +\n    +    ***\n    +\n    +    We need to add this command to the exception list in\n    +    `t/t1517-outside-repo.sh` because it uses “EXPERIMENTAL!”\n    +    in the usage line.\n     \n         Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>\n     \n    - ## Documentation/git-name-rev.adoc ##\n    -@@ Documentation/git-name-rev.adoc: git-name-rev - Find symbolic names for given revs\n    - SYNOPSIS\n    - --------\n    - [verse]\n    --'git name-rev' [--tags] [--refs=<pattern>]\n    -+'git name-rev' [--tags] [--refs=<pattern>] [--format=<pretty>]\n    - \t       ( --all | --annotate-stdin | <commit-ish>... )\n    - \n    - DESCRIPTION\n    -@@ Documentation/git-name-rev.adoc: format parsable by 'git rev-parse'.\n    - OPTIONS\n    - -------\n    - \n    -+--format=<pretty>::\n    -+--no-format::\n    -+\tFormat revisions instead of outputting symbolic names. The\n    -+\tdefault is `--no-format`.\n    + ## Documentation/git-format-rev.adoc (new) ##\n    +@@\n    ++git-format-rev(1)\n    ++=================\n    ++\n    ++NAME\n    ++----\n    ++git-format-rev - EXPERIMENTAL: Pretty format revisions on demand\n    ++\n    ++\n    ++SYNOPSIS\n    ++--------\n    ++[synopsis]\n    ++(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> --format=<pretty> [--notes=<ref>]\n    ++\n    ++DESCRIPTION\n    ++-----------\n    ++\n    ++Pretty format revisions from standard input.\n    ++\n    ++THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.\n    ++\n    ++OPTIONS\n    ++-------\n    ++\n    ++`--stdin-mode=<mode>`::\n    ++\tHow to interpret standard input data:\n    +++\n    ++--\n    ++`revs`:: Each line is interpreted as a commit. Any kind of revision\n    ++\texpression can be used (see linkgit:gitrevisions[7]). Annotated\n    ++\ttags are peeled (see linkgit:gitglossary[7]).\n     ++\n    -+Implies `--name-only`. The negation `--no-format` implies\n    -+`--no-name-only` (the default for the command).\n    ++The argument `rev` is also accepted.\n    ++`text`:: Formats all commit object names found in freeform text. These\n    ++\tmust the full object names, i.e. abbreviated hexidecimal object\n    ++\tnames will not be interpreted.\n    ++--\n     +\n    - --tags::\n    - \tDo not use branch names, but only tags to name the commits\n    - \n    ++`--format=<pretty>`::\n    ++\tPretty format string.\n    ++\n    ++`--notes=<ref>`::\n    ++`--no-notes`::\n    ++\tCustom notes ref. Notes are displayed when using the `%N`\n    ++\tatom. See linkgit:git-notes[1].\n    ++\n    ++EXAMPLES\n    ++--------\n    ++\n    ++The command linkgit:git-last-modified[1] shows the commit that each file\n    ++was last modified in.\n    ++\n    ++----\n    ++$ git last-modified -- README.md Makefile\n    ++7798034171030be0909c56377a4e0e10e6d2df93\tMakefile\n    ++c50fbb2dd225e7e82abba4380423ae105089f4d7\tREADME.md\n    ++----\n    ++\n    ++We can pipe the result to this command in order to replace the object\n    ++name with the commit author.\n    ++\n    ++----\n    ++$ git last-modified -- README.md Makefile |\n    ++    git format-rev --stdin-mode=text --format=%an\n    ++Junio C Hamano\tMakefile\n    ++Todd Zullinger\tREADME.md\n    ++----\n    ++\n    ++Another example is _formatting commits in commit messages_. Given this commit message:\n    ++\n    ++----\n    ++Fix off-by-one error\n    ++\n    ++Fix off-by-one error introduced in\n    ++e83c5163316f89bfbde7d9ab23ca2e25604af290.\n    ++\n    ++We thought we fixed this in 5569bf9bbedd63a00780fc5c110e0cfab3aa97b9 but\n    ++that only covered 1/3 of the faulty cases.\n    ++----\n    ++\n    ++We can format the commits and use par(1) to reflow the text, say in a\n    ++`commit-msg` hook:\n    ++\n    ++----\n    ++$ git config set hook.reference-commits.event commit-msg\n    ++$ git config set hook.reference-commits.command reference-commits\n    ++$ cat $(which reference-commits)\n    ++#/bin/sh\n    ++\n    ++msg=\"$1\"\n    ++rewritten=$(mktemp)\n    ++git format-rev --stdin-mode=text --format=reference <\"$msg\" |\n    ++    par >\"$rewritten\"\n    ++mv \"$rewritten\" \"$msg\"\n    ++----\n    ++\n    ++Which will produce something like this:\n    ++\n    ++----\n    ++Fix off-by-one error\n    ++\n    ++Fix off-by-one error introduced in e83c5163316 (Implement better memory\n    ++allocator, 2005-04-07).\n    ++\n    ++We thought we fixed this in 5569bf9bbed (Fix memory allocator,\n    ++2005-06-22) but that only covered 1/3 of the faulty cases.\n    ++----\n    ++\n    ++DISCUSSION\n    ++----------\n    ++\n    ++This command lets you format any number of revisions in any order\n    ++through one command invocation. Consider the\n    ++linkgit:git-last-modified[1] case from the \"EXAMPLES\" section above:\n    ++\n    ++1. There might be hundreds of files\n    ++2. Commits can be repeated, i.e. two or more files were last modified in\n    ++   the same commit\n    ++\n    ++Two widely-used commands which pretty formats commits are\n    ++linkgit:git-log[1] and linkgit:git-show[1]. It turns out that they are\n    ++not a good fit for the above use case.\n    ++\n    ++- The output of linkgit:git-last-modified[1] would have to be processed\n    ++  in stages since you need to transform the first column separately and\n    ++  then link the author to the filename. But this is surmountable.\n    ++- You can feed each commit to `git show` or `git show --no-walk -1`. But\n    ++  that means that you need to create a process for each line.\n    ++- Let’s say that you want to use one process, not one per line. So you\n    ++  want to feed all the commits to the command. Now you face the problem\n    ++  that you have to feed all the commits to the commands before you get\n    ++  any output (this is also the case for the `--stdin` modes). In other\n    ++  words, you cannot loop through each line, get the author for the\n    ++  commit, and output the author and the filename. You need to feed all\n    ++  the commits, get back all the output, and match the output with the\n    ++  filename.\n    ++- But the next problem is that commands will deduplicate the input and\n    ++  only output one commit one single time only. Thus you cannot make the\n    ++  output order match the input order, since a commit could have been\n    ++  repeated in the original input.\n    ++\n    ++In short, it is straightforward to use these two commands if you use one\n    ++process per line. It is much more work if you just want to use one\n    ++process, but still doable. In contrast, this problem is just another\n    ++shell pipeline with this command.\n    ++\n    ++GIT\n    ++---\n    ++Part of the linkgit:git[1] suite\n    +\n    + ## Makefile ##\n    +@@ Makefile: BUILT_INS += $(patsubst builtin/%.o,git-%$X,$(BUILTIN_OBJS))\n    + BUILT_INS += git-cherry$X\n    + BUILT_INS += git-cherry-pick$X\n    + BUILT_INS += git-format-patch$X\n    ++BUILT_INS += git-format-rev$X\n    + BUILT_INS += git-fsck-objects$X\n    + BUILT_INS += git-init$X\n    + BUILT_INS += git-maintenance$X\n    +\n    + ## builtin.h ##\n    +@@ builtin.h: int cmd_fmt_merge_msg(int argc, const char **argv, const char *prefix, struct re\n    + int cmd_for_each_ref(int argc, const char **argv, const char *prefix, struct repository *repo);\n    + int cmd_for_each_repo(int argc, const char **argv, const char *prefix, struct repository *repo);\n    + int cmd_format_patch(int argc, const char **argv, const char *prefix, struct repository *repo);\n    ++int cmd_format_rev(int argc, const char **argv, const char *prefix, struct repository *repo);\n    + int cmd_fsck(int argc, const char **argv, const char *prefix, struct repository *repo);\n    + int cmd_fsmonitor__daemon(int argc, const char **argv, const char *prefix, struct repository *repo);\n    + int cmd_gc(int argc, const char **argv, const char *prefix, struct repository *repo);\n     \n      ## builtin/name-rev.c ##\n     @@\n    @@ builtin/name-rev.c\n      \n      /*\n       * One day.  See the 'name a rev shortly after epoch' test in t6120 when\n    -@@ builtin/name-rev.c: struct rev_name {\n    - \tint from_tag;\n    +@@ builtin/name-rev.c: struct name_ref_data {\n    + \tstruct string_list exclude_filters;\n      };\n      \n     +struct pretty_format {\n    @@ builtin/name-rev.c: struct rev_name {\n     +\tstruct userformat_want want;\n     +};\n     +\n    -+struct format_cb_data {\n    -+    const char *format;\n    -+    int *name_only;\n    + enum command_type {\n    + \tNAME_REV = 1,\n    ++\tFORMAT_REV = 2,\n     +};\n     +\n    - define_commit_slab(commit_rev_name, struct rev_name);\n    ++enum stdin_mode {\n    ++    TEXT = 1,\n    ++    REVS = 2,\n    + };\n    + \n    + struct command {\n    + \tenum command_type type;\n    + \tunion {\n    + \t\tint name_only;\n    ++\t\tstruct pretty_format *pretty_format;\n    + \t} u;\n    + };\n      \n    - static timestamp_t generation_cutoff = GENERATION_NUMBER_INFINITY;\n    -@@ builtin/name-rev.c: static const char *get_exact_ref_match(const struct object *o)\n    +@@ builtin/name-rev.c: static void init_name_rev_command(struct command *cmd,\n    + \tcmd->u.name_only = name_only;\n      }\n      \n    - /* may return a constant string or use \"buf\" as scratch space */\n    --static const char *get_rev_name(const struct object *o, struct strbuf *buf)\n    -+static const char *get_rev_name(const struct object *o,\n    -+\t\t\t\tstruct pretty_format *format_ctx,\n    -+\t\t\t\tstruct strbuf *buf)\n    - {\n    - \tstruct rev_name *n;\n    - \tconst struct commit *c;\n    -@@ builtin/name-rev.c: static const char *get_rev_name(const struct object *o, struct strbuf *buf)\n    - \tif (o->type != OBJ_COMMIT)\n    - \t\treturn get_exact_ref_match(o);\n    - \tc = (const struct commit *) o;\n    ++static void init_format_rev_command(struct command *cmd,\n    ++\t\t\t\t    struct pretty_format *pretty_format)\n    ++{\n    ++\tcmd->type = FORMAT_REV;\n    ++\tcmd->u.pretty_format = pretty_format;\n    ++}\n     +\n    -+\tif (format_ctx) {\n    -+\t\tstrbuf_reset(buf);\n    + static struct tip_table {\n    + \tstruct tip_table_entry {\n    + \t\tstruct object_id oid;\n    +@@ builtin/name-rev.c: static const char *get_rev_name(const struct object *o, struct strbuf *buf)\n    + \t}\n    + }\n    + \n    ++static const char *get_format_rev(const struct commit *c,\n    ++\t\t\t\t  struct pretty_format *format_ctx,\n    ++\t\t\t\t  struct strbuf *buf)\n    ++{\n    ++\tstrbuf_reset(buf);\n     +\n    -+\t\tif (format_ctx->want.notes) {\n    -+\t\t\tstruct strbuf notebuf = STRBUF_INIT;\n    ++\tif (format_ctx->want.notes) {\n    ++\t\tstruct strbuf notebuf = STRBUF_INIT;\n     +\n    -+\t\t\tformat_display_notes(&c->object.oid, &notebuf,\n    -+\t\t\t\t\t     get_log_output_encoding(),\n    -+\t\t\t\t\t     format_ctx->ctx.fmt == CMIT_FMT_USERFORMAT);\n    -+\t\t\tformat_ctx->ctx.notes_message = strbuf_detach(&notebuf, NULL);\n    -+\t\t}\n    ++\t\tformat_display_notes(&c->object.oid, &notebuf,\n    ++\t\t\t\t     get_log_output_encoding(),\n    ++\t\t\t\t     format_ctx->ctx.fmt == CMIT_FMT_USERFORMAT);\n    ++\t\tformat_ctx->ctx.notes_message = strbuf_detach(&notebuf, NULL);\n    ++\t}\n     +\n    -+\t\tpretty_print_commit(&format_ctx->ctx, c, buf);\n    -+\t\tFREE_AND_NULL(format_ctx->ctx.notes_message);\n    ++\tpretty_print_commit(&format_ctx->ctx, c, buf);\n    ++\tFREE_AND_NULL(format_ctx->ctx.notes_message);\n     +\n    -+\t\treturn buf->buf;\n    -+\t}\n    ++\treturn buf->buf;\n    ++}\n     +\n    - \tn = get_commit_rev_name(c);\n    - \tif (!n)\n    - \t\treturn NULL;\n    -@@ builtin/name-rev.c: static const char *get_rev_name(const struct object *o, struct strbuf *buf)\n    - \n      static void show_name(const struct object *obj,\n      \t\t      const char *caller_name,\n    -+\t\t      struct pretty_format *format_ctx,\n      \t\t      int always, int allow_undefined, int name_only)\n    - {\n    - \tconst char *name;\n    -@@ builtin/name-rev.c: static void show_name(const struct object *obj,\n    - \n    - \tif (!name_only)\n    - \t\tprintf(\"%s \", caller_name ? caller_name : oid_to_hex(oid));\n    --\tname = get_rev_name(obj, &buf);\n    -+\tname = get_rev_name(obj, format_ctx, &buf);\n    - \tif (name)\n    - \t\tprintf(\"%s\\n\", name);\n    - \telse if (allow_undefined)\n    -@@ builtin/name-rev.c: static char const * const name_rev_usage[] = {\n    - \tNULL\n    - };\n    - \n    --static void name_rev_line(char *p, struct name_ref_data *data)\n    -+static void name_rev_line(char *p,\n    -+\t\t\t  struct name_ref_data *data,\n    -+\t\t\t  struct pretty_format *format_ctx)\n    - {\n    - \tstruct strbuf buf = STRBUF_INIT;\n    - \tint counter = 0;\n    -@@ builtin/name-rev.c: static void name_rev_line(char *p, struct name_ref_data *data)\n    - \t\t\t\tstruct object *o =\n    - \t\t\t\t\tlookup_object(the_repository, &oid);\n    - \t\t\t\tif (o)\n    --\t\t\t\t\tname = get_rev_name(o, &buf);\n    -+\t\t\t\t\tname = get_rev_name(o, format_ctx, &buf);\n    +@@ builtin/name-rev.c: static void name_rev_line(char *p, struct command *cmd)\n    + \t\t\t\telse\n    + \t\t\t\t\tprintf(\"%.*s (%s)\", p_len, p_start, name);\n    + \t\t\t\tbreak;\n    ++\t\t\tcase FORMAT_REV:\n    ++\t\t\t\tif (!oid_ret)\n    ++\t\t\t\t\to = parse_object(the_repository, &oid);\n    ++\t\t\t\tif (o && o->type == OBJ_COMMIT)\n    ++\t\t\t\t\tname = get_format_rev((const struct commit *)o,\n    ++\t\t\t\t\t\t\t      cmd->u.pretty_format,\n    ++\t\t\t\t\t\t\t      &buf);\n    ++\t\t\t\t*(p + 1) = c;\n    ++\t\t\t\tif (name)\n    ++\t\t\t\t\tprintf(\"%.*s%s\", p_len - hexsz, p_start, name);\n    ++\t\t\t\telse\n    ++\t\t\t\t\tprintf(\"%.*s\", p_len, p_start);\n    ++\t\t\t\tbreak;\n    + \t\t\tdefault:\n    + \t\t\t\tBUG(\"uncovered case: %d\", cmd->type);\n      \t\t\t}\n    - \t\t\t*(p+1) = c;\n    - \n    -@@ builtin/name-rev.c: static void name_rev_line(char *p, struct name_ref_data *data)\n    - \tstrbuf_release(&buf);\n    +@@ builtin/name-rev.c: int cmd_name_rev(int argc,\n    + \tobject_array_clear(&revs);\n    + \treturn 0;\n      }\n    - \n    -+static int format_cb(const struct option *option,\n    -+\t\t     const char *arg,\n    -+\t\t     int unset)\n    ++\n    ++static enum stdin_mode parse_stdin_mode(const char *stdin_mode)\n     +{\n    -+\tstruct format_cb_data *data = option->value;\n    -+\tdata->format = arg;\n    -+\t*data->name_only = !unset;\n    -+\treturn 0;\n    ++\tif (!strcmp(stdin_mode, \"text\"))\n    ++\t\treturn TEXT;\n    ++\telse if (!strcmp(stdin_mode, \"revs\") ||\n    ++\t\t !strcmp(stdin_mode, \"rev\"))\n    ++\t\treturn REVS;\n    ++\telse\n    ++\t\tdie(_(\"'%s' needs to be either text, revs, or rev\"),\n    ++\t\t    \"--stdin-mode\");\n     +}\n     +\n    - int cmd_name_rev(int argc,\n    - \t\t const char **argv,\n    - \t\t const char *prefix,\n    -@@ builtin/name-rev.c: int cmd_name_rev(int argc,\n    - #endif\n    - \tint all = 0, annotate_stdin = 0, allow_undefined = 1, always = 0, peel_tag = 0;\n    - \tstruct name_ref_data data = { 0, 0, STRING_LIST_INIT_NODUP, STRING_LIST_INIT_NODUP };\n    -+\tstatic struct format_cb_data format_cb_data = { 0 };\n    ++static char const *const format_rev_usage[] = {\n    ++\tN_(\"(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> --format=<pretty> [--notes=<ref>]\"),\n    ++\tNULL\n    ++};\n    ++\n    ++int cmd_format_rev(int argc,\n    ++\t\t   const char **argv,\n    ++\t\t   const char *prefix,\n    ++\t\t   struct repository *repo UNUSED)\n    ++{\n    ++\tconst char *format = NULL;\n    ++\tenum stdin_mode stdin_mode;\n    ++\tconst char *stdin_mode_arg = NULL;\n     +\tstruct display_notes_opt format_notes_opt;\n     +\tstruct rev_info format_rev = REV_INFO_INIT;\n    -+\tstruct pretty_format *format_ctx = NULL;\n     +\tstruct pretty_format format_pp = { 0 };\n     +\tstruct string_list notes = STRING_LIST_INIT_NODUP;\n    - \tstruct option opts[] = {\n    - \t\tOPT_BOOL(0, \"name-only\", &data.name_only, N_(\"print only ref-based names (no object names)\")),\n    - \t\tOPT_BOOL(0, \"tags\", &data.tags_only, N_(\"only use tags to name the commits\")),\n    -@@ builtin/name-rev.c: int cmd_name_rev(int argc,\n    - \t\t\t   PARSE_OPT_HIDDEN),\n    - #endif /* WITH_BREAKING_CHANGES */\n    - \t\tOPT_BOOL(0, \"annotate-stdin\", &annotate_stdin, N_(\"annotate text from stdin\")),\n    -+\t\tOPT_CALLBACK(0, \"format\", &format_cb_data, N_(\"format\"),\n    -+\t\t\t     N_(\"pretty-print output instead\"), format_cb),\n    ++\tstruct strbuf scratch_buf = STRBUF_INIT;\n    ++\tstruct command cmd;\n    ++\tstruct option opts[] = {\n    ++\t\tOPT_STRING(0, \"format\", &format, N_(\"format\"),\n    ++\t\t\t   N_(\"pretty format to use\")),\n    ++\t\tOPT_STRING(0, \"stdin-mode\", &stdin_mode_arg, N_(\"stdin-mode\"),\n    ++\t\t\t   N_(\"how revs are processed\")),\n     +\t\tOPT_STRING_LIST(0, \"notes\", &notes, N_(\"notes\"),\n    -+\t\t\t\tN_(\"display notes for --format\")),\n    - \t\tOPT_BOOL(0, \"undefined\", &allow_undefined, N_(\"allow to print `undefined` names (default)\")),\n    - \t\tOPT_BOOL(0, \"always\",     &always,\n    - \t\t\t   N_(\"show abbreviated commit object as fallback\")),\n    -@@ builtin/name-rev.c: int cmd_name_rev(int argc,\n    - \t\tOPT_END(),\n    - \t};\n    - \n    ++\t\t\t\tN_(\"display notes for pretty format\")),\n    ++\t\tOPT_END(),\n    ++\t};\n    ++\n    ++\targc = parse_options(argc, argv, prefix, opts, format_rev_usage, 0);\n    ++\n    ++\tif (argc > 0) {\n    ++\t\terror(_(\"too many arguments\"));\n    ++\t\tusage_with_options(format_rev_usage, opts);\n    ++\t}\n    ++\n    ++\tif (!format)\n    ++\t\tdie(_(\"'%s' is required\"), \"--format\");\n    ++\tif (!stdin_mode_arg)\n    ++\t\tdie(_(\"'%s' is required\"), \"--stdin-mode\");\n    ++\n     +\tinit_display_notes(&format_notes_opt);\n    -+\tformat_cb_data.name_only = &data.name_only;\n    - \tmem_pool_init(&string_pool, 0);\n    - \tinit_commit_rev_name(&rev_names);\n    - \trepo_config(the_repository, git_default_config, NULL);\n    -@@ builtin/name-rev.c: int cmd_name_rev(int argc,\n    - \t}\n    - #endif\n    - \n    -+\tif (format_cb_data.format) {\n    -+\t\tget_commit_format(format_cb_data.format, &format_rev);\n    -+\t\tformat_pp.ctx.rev = &format_rev;\n    -+\t\tformat_pp.ctx.fmt = format_rev.commit_format;\n    -+\t\tformat_pp.ctx.abbrev = format_rev.abbrev;\n    -+\t\tformat_pp.ctx.date_mode_explicit = format_rev.date_mode_explicit;\n    -+\t\tformat_pp.ctx.date_mode = format_rev.date_mode;\n    -+\t\tformat_pp.ctx.color = GIT_COLOR_AUTO;\n    -+\n    -+\t\tuserformat_find_requirements(format_cb_data.format,\n    -+\t\t\t\t\t     &format_pp.want);\n    -+\t\tif (format_pp.want.notes) {\n    -+\t\t\tint ignore_show_notes = 0;\n    -+\t\t\tstruct string_list_item *n;\n    -+\n    -+\t\t\tfor_each_string_list_item(n, &notes)\n    -+\t\t\t\tenable_ref_display_notes(&format_notes_opt,\n    -+\t\t\t\t\t\t\t &ignore_show_notes,\n    -+\t\t\t\t\t\t\t n->string);\n    -+\t\t\tload_display_notes(&format_notes_opt);\n    ++\tstdin_mode = parse_stdin_mode(stdin_mode_arg);\n    ++\n    ++\tget_commit_format(format, &format_rev);\n    ++\tformat_pp.ctx.rev = &format_rev;\n    ++\tformat_pp.ctx.fmt = format_rev.commit_format;\n    ++\tformat_pp.ctx.abbrev = format_rev.abbrev;\n    ++\tformat_pp.ctx.date_mode_explicit = format_rev.date_mode_explicit;\n    ++\tformat_pp.ctx.date_mode = format_rev.date_mode;\n    ++\tformat_pp.ctx.color = GIT_COLOR_AUTO;\n    ++\n    ++\tuserformat_find_requirements(format,\n    ++\t\t\t\t     &format_pp.want);\n    ++\tif (format_pp.want.notes) {\n    ++\t\tint ignore_show_notes = 0;\n    ++\t\tstruct string_list_item *n;\n    ++\n    ++\t\tfor_each_string_list_item(n, &notes)\n    ++\t\t\tenable_ref_display_notes(&format_notes_opt,\n    ++\t\t\t\t\t\t &ignore_show_notes,\n    ++\t\t\t\t\t\t n->string);\n    ++\t\tload_display_notes(&format_notes_opt);\n    ++\t}\n    ++\n    ++\tinit_format_rev_command(&cmd, &format_pp);\n    ++\n    ++\tswitch (stdin_mode) {\n    ++\tcase TEXT:\n    ++\t\twhile (strbuf_getline(&scratch_buf, stdin) != EOF) {\n    ++\t\t\tstrbuf_addch(&scratch_buf, '\\n');\n    ++\t\t\tname_rev_line(scratch_buf.buf, &cmd);\n     +\t\t}\n    ++\t\tbreak;\n    ++\tcase REVS:\n    ++\t\twhile (strbuf_getline(&scratch_buf, stdin) != EOF) {\n    ++\t\t\tstruct object_id oid;\n    ++\t\t\tstruct object *object;\n    ++\t\t\tstruct object *peeled;\n    ++\t\t\tstruct commit *commit;\n    ++\n    ++\t\t\tif (repo_get_oid(the_repository, scratch_buf.buf, &oid)) {\n    ++\t\t\t\tfprintf(stderr, \"Could not get sha1 for %s. Skipping.\\n\",\n    ++\t\t\t\t\tscratch_buf.buf);\n    ++\t\t\t\tcontinue;\n    ++\t\t\t}\n    ++\n    ++\t\t\tobject = parse_object(the_repository, &oid);\n    ++\t\t\tif (!object) {\n    ++\t\t\t\tfprintf(stderr, \"Could not get object for %s. Skipping.\\n\",\n    ++\t\t\t\t\tscratch_buf.buf);\n    ++\t\t\t\tcontinue;\n    ++\t\t\t}\n     +\n    -+\t\tformat_ctx = &format_pp;\n    ++\t\t\tpeeled = deref_tag(the_repository, object, scratch_buf.buf, 0);\n    ++\t\t\tif (peeled && peeled->type == OBJ_COMMIT)\n    ++\t\t\t\tcommit = (struct commit *)peeled;\n    ++\t\t\tif (!commit) {\n    ++\t\t\t\tfprintf(stderr, \"Could not get commit for %s. Skipping.\\n\",\n    ++\t\t\t\t\t*argv);\n    ++\t\t\t\tcontinue;\n    ++\t\t\t}\n    ++\n    ++\t\t\tget_format_rev(commit, &format_pp, &scratch_buf);\n    ++\t\t\tprintf(\"%s\\n\", scratch_buf.buf);\n    ++\t\t\tstrbuf_release(&scratch_buf);\n    ++\t\t}\n    ++\t\tbreak;\n    ++\tdefault:\n    ++\t\tBUG(\"uncovered case: %d\", stdin_mode);\n     +\t}\n     +\n    - \tif (all + annotate_stdin + !!argc > 1) {\n    - \t\terror(\"Specify either a list, or --all, not both!\");\n    - \t\tusage_with_options(name_rev_usage, opts);\n    -@@ builtin/name-rev.c: int cmd_name_rev(int argc,\n    - \n    - \t\twhile (strbuf_getline(&sb, stdin) != EOF) {\n    - \t\t\tstrbuf_addch(&sb, '\\n');\n    --\t\t\tname_rev_line(sb.buf, &data);\n    -+\t\t\tname_rev_line(sb.buf, &data, format_ctx);\n    - \t\t}\n    - \t\tstrbuf_release(&sb);\n    - \t} else if (all) {\n    -@@ builtin/name-rev.c: int cmd_name_rev(int argc,\n    - \t\t\tstruct object *obj = get_indexed_object(the_repository, i);\n    - \t\t\tif (!obj || obj->type != OBJ_COMMIT)\n    - \t\t\t\tcontinue;\n    --\t\t\tshow_name(obj, NULL,\n    -+\t\t\tshow_name(obj, NULL, format_ctx,\n    - \t\t\t\t  always, allow_undefined, data.name_only);\n    - \t\t}\n    - \t} else {\n    - \t\tint i;\n    - \t\tfor (i = 0; i < revs.nr; i++)\n    --\t\t\tshow_name(revs.objects[i].item, revs.objects[i].name,\n    -+\t\t\tshow_name(revs.objects[i].item, revs.objects[i].name, format_ctx,\n    - \t\t\t\t  always, allow_undefined, data.name_only);\n    - \t}\n    - \n    - \tstring_list_clear(&data.ref_filters, 0);\n    - \tstring_list_clear(&data.exclude_filters, 0);\n    ++\tstrbuf_release(&scratch_buf);\n     +\tstring_list_clear(&notes, 0);\n     +\trelease_display_notes(&format_notes_opt);\n    - \tmem_pool_discard(&string_pool, 0);\n    - \tobject_array_clear(&revs);\n    - \treturn 0;\n    ++\treturn 0;\n    ++}\n    +\n    + ## command-list.txt ##\n    +@@ command-list.txt: git-fmt-merge-msg                       purehelpers\n    + git-for-each-ref                        plumbinginterrogators\n    + git-for-each-repo                       plumbinginterrogators\n    + git-format-patch                        mainporcelain\n    ++git-format-rev                          plumbinginterrogators\n    + git-fsck                                ancillaryinterrogators          complete\n    + git-gc                                  mainporcelain\n    + git-get-tar-commit-id                   plumbinginterrogators\n    +\n    + ## git.c ##\n    +@@ git.c: static struct cmd_struct commands[] = {\n    + \t{ \"for-each-ref\", cmd_for_each_ref, RUN_SETUP },\n    + \t{ \"for-each-repo\", cmd_for_each_repo, RUN_SETUP_GENTLY },\n    + \t{ \"format-patch\", cmd_format_patch, RUN_SETUP },\n    ++\t{ \"format-rev\", cmd_format_rev, RUN_SETUP },\n    + \t{ \"fsck\", cmd_fsck, RUN_SETUP },\n    + \t{ \"fsck-objects\", cmd_fsck, RUN_SETUP },\n    + \t{ \"fsmonitor--daemon\", cmd_fsmonitor__daemon, RUN_SETUP },\n    +\n    + ## t/t1517-outside-repo.sh ##\n    +@@ t/t1517-outside-repo.sh: do\n    + \tarchimport | citool | credential-netrc | credential-libsecret | \\\n    + \tcredential-osxkeychain | cvsexportcommit | cvsimport | cvsserver | \\\n    + \tdaemon | \\\n    +-\tdifftool--helper | filter-branch | fsck-objects | get-tar-commit-id | \\\n    ++\tdifftool--helper | filter-branch | format-rev | fsck-objects | \\\n    ++\tget-tar-commit-id | \\\n    + \tgui | gui--askpass | \\\n    + \thttp-backend | http-fetch | http-push | init-db | \\\n    + \tmerge-octopus | merge-one-file | merge-resolve | mergetool | \\\n     \n      ## t/t6120-describe.sh ##\n    -@@ t/t6120-describe.sh: test_expect_success 'name-rev --annotate-stdin works with commitGraph' '\n    - \t)\n    +@@ t/t6120-describe.sh: test_expect_success 'do not be fooled by invalid describe format ' '\n    + \ttest_must_fail git cat-file -t \"refs/tags/super-invalid/./../...../ ~^:/?*[////\\\\\\\\\\\\&}/busted.lock-42-g\"$(cat out)\n      '\n      \n     +test_expect_success 'name-rev --format setup' '\n    @@ t/t6120-describe.sh: test_expect_success 'name-rev --annotate-stdin works with c\n     +\ttest_commit -C repo-format eighth\n     +'\n     +\n    -+test_expect_success 'name-rev --format --no-name-only' '\n    -+\tcat >expect <<-\\EOF &&\n    -+\tHEAD~3 [fifth]\n    -+\tHEAD [eighth]\n    -+\tHEAD~5 [third]\n    -+\tEOF\n    -+\tgit -C repo-format name-rev --format=\"[%s]\" \\\n    -+\t\t--no-name-only HEAD~3 HEAD HEAD~5 >actual &&\n    -+\ttest_cmp expect actual\n    -+'\n    -+\n    -+test_expect_success 'name-rev --format --no-format is the same as regular name-rev' '\n    -+\tgit -C repo-format name-rev HEAD~2 HEAD~3 >expect &&\n    -+\ttest_file_not_empty expect &&\n    -+\tgit -C repo-format name-rev --format=\"huh?\" \\\n    -+\t\t--no-format HEAD~2 HEAD~3 >actual &&\n    -+\ttest_cmp expect actual\n    -+'\n    -+\n    -+test_expect_success 'name-rev --format=%s for argument revs' '\n    ++test_expect_success 'format-rev --stdin-mode=revs' '\n     +\tcat >expect <<-\\EOF &&\n     +\teighth\n     +\tseventh\n     +\tfifth\n     +\tEOF\n    -+\tgit -C repo-format name-rev --format=%s \\\n    -+\t\tHEAD HEAD~ HEAD~3 >actual &&\n    ++\tgit -C repo-format format-rev --stdin-mode=revs \\\n    ++\t\t--format=%s >actual <<-\\EOF &&\n    ++\tHEAD\n    ++\tHEAD~\n    ++\tHEAD~3\n    ++\tEOF\n     +\ttest_cmp expect actual\n     +'\n     +\n    -+test_expect_success '--name-rev --format=reference --annotate-stdin from rev-list same as log' '\n    ++test_expect_success 'format-rev --stdin-mode=text from rev-list same as log' '\n     +\tgit -C repo-format log --format=reference >expect &&\n     +\ttest_file_not_empty expect &&\n     +\tgit -C repo-format rev-list HEAD >list &&\n    -+\tgit -C repo-format name-rev --format=reference \\\n    -+\t\t--annotate-stdin <list >actual &&\n    ++\tgit -C repo-format format-rev --stdin-mode=text \\\n    ++\t\t--format=reference <list >actual &&\n     +\ttest_cmp expect actual\n     +'\n     +\n    -+test_expect_success '--name-rev --format=<pretty> --annotate-stdin with running text and tree oid' '\n    ++test_expect_success 'format-rev --stdin-mode=text with running text and tree oid' '\n     +\tcmit_oid=$(git -C repo-format rev-parse :/fifth) &&\n     +\treference=$(git -C repo-format log -n1 --format=reference :/fifth) &&\n     +\ttree=$(git -C repo-format rev-parse HEAD^{tree}) &&\n    @@ t/t6120-describe.sh: test_expect_success 'name-rev --annotate-stdin works with c\n     +\tWe thought we fixed this in ${reference}.\n     +\tBut look at this tree: ${tree}.\n     +\tEOF\n    -+\tgit -C repo-format name-rev --format=reference --annotate-stdin \\\n    ++\tgit -C repo-format format-rev --stdin-mode=text --format=reference \\\n     +\t\t>actual <<-EOF &&\n     +\tWe thought we fixed this in ${cmit_oid}.\n     +\tBut look at this tree: ${tree}.\n    @@ t/t6120-describe.sh: test_expect_success 'name-rev --annotate-stdin works with c\n     +\ttest_cmp expect actual\n     +'\n     +\n    -+test_expect_success 'name-rev --format=<pretty> with %N (note)' '\n    ++test_expect_success 'format-rev with %N (note)' '\n     +\ttest_when_finished \"git -C repo-format notes remove\" &&\n     +\tgit -C repo-format notes add -m\"Make a note\" &&\n     +\tprintf \"Make a note\\n\\n\\n\" >expect &&\n    -+\tgit -C repo-format name-rev --format=\"tformat:%N\" \\\n    -+\t\tHEAD HEAD~ >actual &&\n    ++\tgit -C repo-format format-rev --stdin-mode=revs \\\n    ++\t\t--format=\"tformat:%N\" \\\n    ++\t\t>actual <<-\\EOF &&\n    ++\tHEAD\n    ++\tHEAD~\n    ++\tEOF\n     +\ttest_cmp expect actual\n     +'\n     +\n    -+test_expect_success 'name-rev --format=<pretty> --notes<ref>' '\n    ++test_expect_success 'format-rev --notes<ref> (custom notes ref)' '\n     +\t# One custom notes ref\n     +\ttest_when_finished \"git -C repo-format notes remove\" &&\n     +\ttest_when_finished \"git -C repo-format notes --ref=word remove\" &&\n     +\tgit -C repo-format notes add -m\"default\" &&\n     +\tgit -C repo-format notes --ref=word add -m\"custom\" &&\n     +\tprintf \"custom\\n\\n\" >expect &&\n    -+\tgit -C repo-format name-rev --format=\"tformat:%N\" \\\n    ++\tgit -C repo-format format-rev --stdin-mode=revs \\\n    ++\t\t--format=\"tformat:%N\" \\\n     +\t\t--notes=word \\\n    -+\t\tHEAD >actual &&\n    ++\t\t>actual <<-\\EOF &&\n    ++\tHEAD\n    ++\tEOF\n     +\ttest_cmp expect actual &&\n     +\t# Glob all\n     +\tprintf \"default\\ncustom\\n\\n\" >expect &&\n    -+\tgit -C repo-format name-rev --format=\"tformat:%N\" \\\n    -+\t\t--notes=* \\\n    -+\t\tHEAD >actual &&\n    ++\tgit -C repo-format format-rev --stdin-mode=revs \\\n    ++\t\t--format=\"tformat:%N\" \\\n    ++\t\t--notes=* >actual <<-\\EOF &&\n    ++\tHEAD\n    ++\tEOF\n    ++\ttest_cmp expect actual\n    ++'\n    ++\n    ++test_expect_success 'format-rev --stdin-mode=revs on annotated tag peels to commit' '\n    ++\ttest_when_finished \"git -C repo-format tag -d version\" &&\n    ++\tgit -C repo-format tag -a -m\"new version\" version &&\n    ++\tcat >expect <<-\\EOF &&\n    ++\teighth\n    ++\tEOF\n    ++\tgit -C repo-format format-rev --stdin-mode=revs \\\n    ++\t\t--format=%s \\\n    ++\t\t--notes=* >actual <<-\\EOF &&\n    ++\tversion\n    ++\tEOF\n     +\ttest_cmp expect actual\n     +'\n     +\n    - #               B\n    - #               o\n    - #  H             \\\n    + test_done\n\nbase-commit: 67006b9db8b772423ad0706029286096307d2567\n-- \n2.54.0.13.g9c7419e39f8\n\n"},{"id":"542453","messageId":"V3_name-rev_braces.66b@msgid.xyz","threadId":"65237","inReplyTo":"V3_CV_format-rev.66a@msgid.xyz","subject":"[PATCH v3 1/5] name-rev: wrap both blocks in braces","fromName":"","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-04-28T22:25:52Z","receivedAt":"2026-04-28T22:26:48Z","isPatch":true,"body":"From: Kristoffer Haugsbakk <code@khaugsbakk.name>\n\nSee `CodingGuidelines`:\n\n    - When there are multiple arms to a conditional and some of them\n      require braces, enclose even a single line block in braces for\n      consistency. [...]\n\nSigned-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>\n---\n\nNotes (series):\n    v2:\n    Fix stray formatting of `(p+1)`\n\n builtin/name-rev.c | 8 ++++----\n 1 file changed, 4 insertions(+), 4 deletions(-)\n\ndiff --git a/builtin/name-rev.c b/builtin/name-rev.c\nindex 6188cf98ce0..171e7bd0e98 100644\n--- a/builtin/name-rev.c\n+++ b/builtin/name-rev.c\n@@ -466,9 +466,9 @@ static const char *get_rev_name(const struct object *o, struct strbuf *buf)\n \tif (!n)\n \t\treturn NULL;\n \n-\tif (!n->generation)\n+\tif (!n->generation) {\n \t\treturn n->tip_name;\n-\telse {\n+\t} else {\n \t\tstrbuf_reset(buf);\n \t\tstrbuf_addstr(buf, n->tip_name);\n \t\tstrbuf_strip_suffix(buf, \"^0\");\n@@ -516,9 +516,9 @@ static void name_rev_line(char *p, struct name_ref_data *data)\n \n \tfor (p_start = p; *p; p++) {\n #define ishex(x) (isdigit((x)) || ((x) >= 'a' && (x) <= 'f'))\n-\t\tif (!ishex(*p))\n+\t\tif (!ishex(*p)) {\n \t\t\tcounter = 0;\n-\t\telse if (++counter == hexsz &&\n+\t\t} else if (++counter == hexsz &&\n \t\t\t !ishex(*(p+1))) {\n \t\t\tstruct object_id oid;\n \t\t\tconst char *name = NULL;\n-- \n2.54.0.13.g9c7419e39f8\n\n"},{"id":"542454","messageId":"V3_name-rev_clang-format.66c@msgid.xyz","threadId":"65237","inReplyTo":"V3_CV_format-rev.66a@msgid.xyz","subject":"[PATCH v3 2/5] name-rev: run clang-format before factoring code","fromName":"","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-04-28T22:25:53Z","receivedAt":"2026-04-28T22:27:07Z","isPatch":true,"body":"From: Kristoffer Haugsbakk <code@khaugsbakk.name>\n\nWe are about to move code around to prepare for adding a new\ncommand. Let’s deal with clang-format changes first in the affected\nareas.\n\nSigned-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>\n---\n builtin/name-rev.c | 18 +++++++++---------\n 1 file changed, 9 insertions(+), 9 deletions(-)\n\ndiff --git a/builtin/name-rev.c b/builtin/name-rev.c\nindex 171e7bd0e98..6357eaa76d0 100644\n--- a/builtin/name-rev.c\n+++ b/builtin/name-rev.c\n@@ -519,22 +519,22 @@ static void name_rev_line(char *p, struct name_ref_data *data)\n \t\tif (!ishex(*p)) {\n \t\t\tcounter = 0;\n \t\t} else if (++counter == hexsz &&\n-\t\t\t !ishex(*(p+1))) {\n+\t\t\t   !ishex(*(p + 1))) {\n \t\t\tstruct object_id oid;\n \t\t\tconst char *name = NULL;\n-\t\t\tchar c = *(p+1);\n+\t\t\tchar c = *(p + 1);\n \t\t\tint p_len = p - p_start + 1;\n \n \t\t\tcounter = 0;\n \n-\t\t\t*(p+1) = 0;\n+\t\t\t*(p + 1) = 0;\n \t\t\tif (!repo_get_oid(the_repository, p - (hexsz - 1), &oid)) {\n \t\t\t\tstruct object *o =\n \t\t\t\t\tlookup_object(the_repository, &oid);\n \t\t\t\tif (o)\n \t\t\t\t\tname = get_rev_name(o, &buf);\n \t\t\t}\n-\t\t\t*(p+1) = c;\n+\t\t\t*(p + 1) = c;\n \n \t\t\tif (!name)\n \t\t\t\tcontinue;\n@@ -571,9 +571,9 @@ int cmd_name_rev(int argc,\n \t\tOPT_BOOL(0, \"name-only\", &data.name_only, N_(\"print only ref-based names (no object names)\")),\n \t\tOPT_BOOL(0, \"tags\", &data.tags_only, N_(\"only use tags to name the commits\")),\n \t\tOPT_STRING_LIST(0, \"refs\", &data.ref_filters, N_(\"pattern\"),\n-\t\t\t\t   N_(\"only use refs matching <pattern>\")),\n+\t\t\t\tN_(\"only use refs matching <pattern>\")),\n \t\tOPT_STRING_LIST(0, \"exclude\", &data.exclude_filters, N_(\"pattern\"),\n-\t\t\t\t   N_(\"ignore refs matching <pattern>\")),\n+\t\t\t\tN_(\"ignore refs matching <pattern>\")),\n \t\tOPT_GROUP(\"\"),\n \t\tOPT_BOOL(0, \"all\", &all, N_(\"list all commits reachable from all refs\")),\n #ifndef WITH_BREAKING_CHANGES\n@@ -585,10 +585,10 @@ int cmd_name_rev(int argc,\n #endif /* WITH_BREAKING_CHANGES */\n \t\tOPT_BOOL(0, \"annotate-stdin\", &annotate_stdin, N_(\"annotate text from stdin\")),\n \t\tOPT_BOOL(0, \"undefined\", &allow_undefined, N_(\"allow to print `undefined` names (default)\")),\n-\t\tOPT_BOOL(0, \"always\",     &always,\n-\t\t\t   N_(\"show abbreviated commit object as fallback\")),\n+\t\tOPT_BOOL(0, \"always\", &always,\n+\t\t\t N_(\"show abbreviated commit object as fallback\")),\n \t\tOPT_HIDDEN_BOOL(0, \"peel-tag\", &peel_tag,\n-\t\t\t   N_(\"dereference tags in the input (internal use)\")),\n+\t\t\t\tN_(\"dereference tags in the input (internal use)\")),\n \t\tOPT_END(),\n \t};\n \n-- \n2.54.0.13.g9c7419e39f8\n\n"},{"id":"542455","messageId":"V3_name-rev_factor.66d@msgid.xyz","threadId":"65237","inReplyTo":"V3_CV_format-rev.66a@msgid.xyz","subject":"[PATCH v3 3/5] name-rev: factor code for sharing with a new command","fromName":"","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-04-28T22:25:54Z","receivedAt":"2026-04-28T22:27:25Z","isPatch":true,"body":"From: Kristoffer Haugsbakk <code@khaugsbakk.name>\n\nWe are about to introduce a new command git-format-rev(1) to this\nfile. Let’s factor some code so that we can share it with the new\ncommand.\n\nWe want to be able to format commits found in freeform text, and\ngit-name-rev(1) already has a function for that but for symbolic\nnames. Let’s use a tagged union for the command-specific payload.\n\nNo functional changes.\n\nSigned-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>\n---\n builtin/name-rev.c | 54 +++++++++++++++++++++++++++++++++++-----------\n 1 file changed, 41 insertions(+), 13 deletions(-)\n\ndiff --git a/builtin/name-rev.c b/builtin/name-rev.c\nindex 6357eaa76d0..dc4136f4de3 100644\n--- a/builtin/name-rev.c\n+++ b/builtin/name-rev.c\n@@ -272,6 +272,24 @@ struct name_ref_data {\n \tstruct string_list exclude_filters;\n };\n \n+enum command_type {\n+\tNAME_REV = 1,\n+};\n+\n+struct command {\n+\tenum command_type type;\n+\tunion {\n+\t\tint name_only;\n+\t} u;\n+};\n+\n+static void init_name_rev_command(struct command *cmd,\n+\t\t\t\t  int name_only)\n+{\n+\tcmd->type = NAME_REV;\n+\tcmd->u.name_only = name_only;\n+}\n+\n static struct tip_table {\n \tstruct tip_table_entry {\n \t\tstruct object_id oid;\n@@ -507,7 +525,7 @@ static char const * const name_rev_usage[] = {\n \tNULL\n };\n \n-static void name_rev_line(char *p, struct name_ref_data *data)\n+static void name_rev_line(char *p, struct command *cmd)\n {\n \tstruct strbuf buf = STRBUF_INIT;\n \tint counter = 0;\n@@ -516,6 +534,7 @@ static void name_rev_line(char *p, struct name_ref_data *data)\n \n \tfor (p_start = p; *p; p++) {\n #define ishex(x) (isdigit((x)) || ((x) >= 'a' && (x) <= 'f'))\n+\tstart:\n \t\tif (!ishex(*p)) {\n \t\t\tcounter = 0;\n \t\t} else if (++counter == hexsz &&\n@@ -524,25 +543,32 @@ static void name_rev_line(char *p, struct name_ref_data *data)\n \t\t\tconst char *name = NULL;\n \t\t\tchar c = *(p + 1);\n \t\t\tint p_len = p - p_start + 1;\n+\t\t\tstruct object *o = NULL;\n+\t\t\tint oid_ret = 1;\n \n \t\t\tcounter = 0;\n \n \t\t\t*(p + 1) = 0;\n-\t\t\tif (!repo_get_oid(the_repository, p - (hexsz - 1), &oid)) {\n-\t\t\t\tstruct object *o =\n-\t\t\t\t\tlookup_object(the_repository, &oid);\n+\t\t\toid_ret = repo_get_oid(the_repository, p - (hexsz - 1), &oid);\n+\n+\t\t\tswitch (cmd->type) {\n+\t\t\tcase NAME_REV:\n+\t\t\t\tif (!oid_ret)\n+\t\t\t\t\to = lookup_object(the_repository, &oid);\n \t\t\t\tif (o)\n \t\t\t\t\tname = get_rev_name(o, &buf);\n+\t\t\t\t*(p + 1) = c;\n+\t\t\t\tif (!name)\n+\t\t\t\t\tgoto start;\n+\t\t\t\tif (cmd->u.name_only)\n+\t\t\t\t\tprintf(\"%.*s%s\", p_len - hexsz, p_start, name);\n+\t\t\t\telse\n+\t\t\t\t\tprintf(\"%.*s (%s)\", p_len, p_start, name);\n+\t\t\t\tbreak;\n+\t\t\tdefault:\n+\t\t\t\tBUG(\"uncovered case: %d\", cmd->type);\n \t\t\t}\n-\t\t\t*(p + 1) = c;\n-\n-\t\t\tif (!name)\n-\t\t\t\tcontinue;\n \n-\t\t\tif (data->name_only)\n-\t\t\t\tprintf(\"%.*s%s\", p_len - hexsz, p_start, name);\n-\t\t\telse\n-\t\t\t\tprintf(\"%.*s (%s)\", p_len, p_start, name);\n \t\t\tp_start = p + 1;\n \t\t}\n \t}\n@@ -567,6 +593,7 @@ int cmd_name_rev(int argc,\n #endif\n \tint all = 0, annotate_stdin = 0, allow_undefined = 1, always = 0, peel_tag = 0;\n \tstruct name_ref_data data = { 0, 0, STRING_LIST_INIT_NODUP, STRING_LIST_INIT_NODUP };\n+\tstruct command cmd;\n \tstruct option opts[] = {\n \t\tOPT_BOOL(0, \"name-only\", &data.name_only, N_(\"print only ref-based names (no object names)\")),\n \t\tOPT_BOOL(0, \"tags\", &data.tags_only, N_(\"only use tags to name the commits\")),\n@@ -596,6 +623,7 @@ int cmd_name_rev(int argc,\n \tinit_commit_rev_name(&rev_names);\n \trepo_config(the_repository, git_default_config, NULL);\n \targc = parse_options(argc, argv, prefix, opts, name_rev_usage, 0);\n+\tinit_name_rev_command(&cmd, data.name_only);\n \n #ifndef WITH_BREAKING_CHANGES\n \tif (transform_stdin) {\n@@ -663,7 +691,7 @@ int cmd_name_rev(int argc,\n \n \t\twhile (strbuf_getline(&sb, stdin) != EOF) {\n \t\t\tstrbuf_addch(&sb, '\\n');\n-\t\t\tname_rev_line(sb.buf, &data);\n+\t\t\tname_rev_line(sb.buf, &cmd);\n \t\t}\n \t\tstrbuf_release(&sb);\n \t} else if (all) {\n-- \n2.54.0.13.g9c7419e39f8\n\n"},{"id":"542456","messageId":"V3_name-rev_dedicated_test.66e@msgid.xyz","threadId":"65237","inReplyTo":"V3_CV_format-rev.66a@msgid.xyz","subject":"[PATCH v3 4/5] name-rev: make dedicated --annotate-stdin --name-only test","fromName":"","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-04-28T22:25:55Z","receivedAt":"2026-04-28T22:27:44Z","isPatch":true,"body":"From: Kristoffer Haugsbakk <code@khaugsbakk.name>\n\nThe previous commit split the `--name-only` handling:\n\n1. `--annotate-stdin`: uses the new `struct command`\n2. The rest: uses `struct name_ref_data`\n\nBut there is no dedicated test for the option combination in (1). That\nmeans that the following tests will fail if you neglect to set\n`command.u.name_only` properly:\n\n    name-rev --annotate-stdin works with commitGraph\n    name-rev --annotate-stdin works with non-monotonic timestamps\n\neven though it has nothing to do with what these tests are supposed\nto test.\n\nLet’s add another regression test now that it is relevant.\n\nSigned-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>\n---\n t/t6120-describe.sh | 14 ++++++++++++++\n 1 file changed, 14 insertions(+)\n\ndiff --git a/t/t6120-describe.sh b/t/t6120-describe.sh\nindex 2c70cc561ad..62789f76381 100755\n--- a/t/t6120-describe.sh\n+++ b/t/t6120-describe.sh\n@@ -298,6 +298,20 @@ test_expect_success 'name-rev --annotate-stdin' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'name-rev --annotate-stdin --name-only' '\n+\t>expect.unsorted &&\n+\tfor rev in $(git rev-list --all)\n+\tdo\n+\t\tname=$(git name-rev --name-only $rev) &&\n+\t\techo \"$name\" >>expect.unsorted || return 1\n+\tdone &&\n+\tsort <expect.unsorted >expect &&\n+\tgit name-rev --annotate-stdin --name-only \\\n+\t\t<list >actual.unsorted &&\n+\tsort <actual.unsorted >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success 'name-rev --stdin deprecated' '\n \tgit rev-list --all >list &&\n \tif ! test_have_prereq WITH_BREAKING_CHANGES\n-- \n2.54.0.13.g9c7419e39f8\n\n"},{"id":"542457","messageId":"V3_format-rev_new_builtin.66f@msgid.xyz","threadId":"65237","inReplyTo":"V3_CV_format-rev.66a@msgid.xyz","subject":"[PATCH v3 5/5] format-rev: introduce builtin for on-demand pretty formatting","fromName":"","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-04-28T22:25:56Z","receivedAt":"2026-04-28T22:28:03Z","isPatch":true,"body":"From: Kristoffer Haugsbakk <code@khaugsbakk.name>\n\nIntroduce a new builtin for pretty formatting one revision expression\nper line or commit object names found in running text.\n\nSometimes you want to format commits. Most of the time you’re\nwalking the graph, e.g. getting a range of commits like\n`master..topic`. That’s a job for git-log(1).\n\nBut there are times when you want to format commits that you encounter\non demand:\n\n• Full hashes in running text that you might want to pretty-print\n• git-last-modified(1) outputs full hashes that you can do the same\n  with\n• git-cherry(1) has `-v` for commit subject, but maybe you want\n  something else?\n\nBut now you can’t use git-log(1), git-show(1), or git-rev-list(1):\n\n• You can’t feed commits piecemeal to these commands, one input\n  for one output; they block until standard in is closed\n• You can’t feed a list of possibly duplicate commits, like the output\n  of git-last-modified(1); they effectively deduplicate the output\n\nBeyond these two points there’s also the input massage problem: you\ncannot feed mixed input (revisions mixed with arbitrary text).\n\nOne might hope that git-cat-file(1) can save us. But it doesn’t\nsupport pretty formats.\n\nBut there is one command that already both handles revisions as\narguments, revisions on standard input, and even revisions mixed in\nwith arbitrary text. Namely git-name-rev(1): the command for outputting\nsymbolic names for commits.\n\nWe made some room in `builtin/name-rev.c` two commits ago. Let’s\nnow add this new git-format-rev(1) command. Taking inspiration from\ngit-name-rev(1), there are two modes:\n\n• revs: like git-name-rev(1) in argv mode, but one revision per line\n  on standard in\n• text: like git-name-rev(1) with `--annotate-stdin`\n\n***\n\nWe need to add this command to the exception list in\n`t/t1517-outside-repo.sh` because it uses “EXPERIMENTAL!”\nin the usage line.\n\nSigned-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>\n---\n\nNotes (series):\n    v3:\n    • And don’t forget to document --notes this time\n    \n          https://lore.kernel.org/git/CALnO6CB5WOTp_e7Kv3CrEbQ+3XE-gDxNVHf7qATBEbyKWfxpLg@mail.gmail.com/\n\n Documentation/git-format-rev.adoc | 148 ++++++++++++++++++++++++\n Makefile                          |   1 +\n builtin.h                         |   1 +\n builtin/name-rev.c                | 186 ++++++++++++++++++++++++++++++\n command-list.txt                  |   1 +\n git.c                             |   1 +\n t/t1517-outside-repo.sh           |   3 +-\n t/t6120-describe.sh               | 104 +++++++++++++++++\n 8 files changed, 444 insertions(+), 1 deletion(-)\n create mode 100644 Documentation/git-format-rev.adoc\n\ndiff --git a/Documentation/git-format-rev.adoc b/Documentation/git-format-rev.adoc\nnew file mode 100644\nindex 00000000000..d960001d750\n--- /dev/null\n+++ b/Documentation/git-format-rev.adoc\n@@ -0,0 +1,148 @@\n+git-format-rev(1)\n+=================\n+\n+NAME\n+----\n+git-format-rev - EXPERIMENTAL: Pretty format revisions on demand\n+\n+\n+SYNOPSIS\n+--------\n+[synopsis]\n+(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> --format=<pretty> [--notes=<ref>]\n+\n+DESCRIPTION\n+-----------\n+\n+Pretty format revisions from standard input.\n+\n+THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.\n+\n+OPTIONS\n+-------\n+\n+`--stdin-mode=<mode>`::\n+\tHow to interpret standard input data:\n++\n+--\n+`revs`:: Each line is interpreted as a commit. Any kind of revision\n+\texpression can be used (see linkgit:gitrevisions[7]). Annotated\n+\ttags are peeled (see linkgit:gitglossary[7]).\n++\n+The argument `rev` is also accepted.\n+`text`:: Formats all commit object names found in freeform text. These\n+\tmust the full object names, i.e. abbreviated hexidecimal object\n+\tnames will not be interpreted.\n+--\n+\n+`--format=<pretty>`::\n+\tPretty format string.\n+\n+`--notes=<ref>`::\n+`--no-notes`::\n+\tCustom notes ref. Notes are displayed when using the `%N`\n+\tatom. See linkgit:git-notes[1].\n+\n+EXAMPLES\n+--------\n+\n+The command linkgit:git-last-modified[1] shows the commit that each file\n+was last modified in.\n+\n+----\n+$ git last-modified -- README.md Makefile\n+7798034171030be0909c56377a4e0e10e6d2df93\tMakefile\n+c50fbb2dd225e7e82abba4380423ae105089f4d7\tREADME.md\n+----\n+\n+We can pipe the result to this command in order to replace the object\n+name with the commit author.\n+\n+----\n+$ git last-modified -- README.md Makefile |\n+    git format-rev --stdin-mode=text --format=%an\n+Junio C Hamano\tMakefile\n+Todd Zullinger\tREADME.md\n+----\n+\n+Another example is _formatting commits in commit messages_. Given this commit message:\n+\n+----\n+Fix off-by-one error\n+\n+Fix off-by-one error introduced in\n+e83c5163316f89bfbde7d9ab23ca2e25604af290.\n+\n+We thought we fixed this in 5569bf9bbedd63a00780fc5c110e0cfab3aa97b9 but\n+that only covered 1/3 of the faulty cases.\n+----\n+\n+We can format the commits and use par(1) to reflow the text, say in a\n+`commit-msg` hook:\n+\n+----\n+$ git config set hook.reference-commits.event commit-msg\n+$ git config set hook.reference-commits.command reference-commits\n+$ cat $(which reference-commits)\n+#/bin/sh\n+\n+msg=\"$1\"\n+rewritten=$(mktemp)\n+git format-rev --stdin-mode=text --format=reference <\"$msg\" |\n+    par >\"$rewritten\"\n+mv \"$rewritten\" \"$msg\"\n+----\n+\n+Which will produce something like this:\n+\n+----\n+Fix off-by-one error\n+\n+Fix off-by-one error introduced in e83c5163316 (Implement better memory\n+allocator, 2005-04-07).\n+\n+We thought we fixed this in 5569bf9bbed (Fix memory allocator,\n+2005-06-22) but that only covered 1/3 of the faulty cases.\n+----\n+\n+DISCUSSION\n+----------\n+\n+This command lets you format any number of revisions in any order\n+through one command invocation. Consider the\n+linkgit:git-last-modified[1] case from the \"EXAMPLES\" section above:\n+\n+1. There might be hundreds of files\n+2. Commits can be repeated, i.e. two or more files were last modified in\n+   the same commit\n+\n+Two widely-used commands which pretty formats commits are\n+linkgit:git-log[1] and linkgit:git-show[1]. It turns out that they are\n+not a good fit for the above use case.\n+\n+- The output of linkgit:git-last-modified[1] would have to be processed\n+  in stages since you need to transform the first column separately and\n+  then link the author to the filename. But this is surmountable.\n+- You can feed each commit to `git show` or `git show --no-walk -1`. But\n+  that means that you need to create a process for each line.\n+- Let’s say that you want to use one process, not one per line. So you\n+  want to feed all the commits to the command. Now you face the problem\n+  that you have to feed all the commits to the commands before you get\n+  any output (this is also the case for the `--stdin` modes). In other\n+  words, you cannot loop through each line, get the author for the\n+  commit, and output the author and the filename. You need to feed all\n+  the commits, get back all the output, and match the output with the\n+  filename.\n+- But the next problem is that commands will deduplicate the input and\n+  only output one commit one single time only. Thus you cannot make the\n+  output order match the input order, since a commit could have been\n+  repeated in the original input.\n+\n+In short, it is straightforward to use these two commands if you use one\n+process per line. It is much more work if you just want to use one\n+process, but still doable. In contrast, this problem is just another\n+shell pipeline with this command.\n+\n+GIT\n+---\n+Part of the linkgit:git[1] suite\ndiff --git a/Makefile b/Makefile\nindex 15b1ded1a0b..cbaf91fd846 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -895,6 +895,7 @@ BUILT_INS += $(patsubst builtin/%.o,git-%$X,$(BUILTIN_OBJS))\n BUILT_INS += git-cherry$X\n BUILT_INS += git-cherry-pick$X\n BUILT_INS += git-format-patch$X\n+BUILT_INS += git-format-rev$X\n BUILT_INS += git-fsck-objects$X\n BUILT_INS += git-init$X\n BUILT_INS += git-maintenance$X\ndiff --git a/builtin.h b/builtin.h\nindex 235c51f30e5..63813c90125 100644\n--- a/builtin.h\n+++ b/builtin.h\n@@ -189,6 +189,7 @@ int cmd_fmt_merge_msg(int argc, const char **argv, const char *prefix, struct re\n int cmd_for_each_ref(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_for_each_repo(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_format_patch(int argc, const char **argv, const char *prefix, struct repository *repo);\n+int cmd_format_rev(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_fsck(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_fsmonitor__daemon(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_gc(int argc, const char **argv, const char *prefix, struct repository *repo);\ndiff --git a/builtin/name-rev.c b/builtin/name-rev.c\nindex dc4136f4de3..b60cc766279 100644\n--- a/builtin/name-rev.c\n+++ b/builtin/name-rev.c\n@@ -18,6 +18,9 @@\n #include \"commit-graph.h\"\n #include \"wildmatch.h\"\n #include \"mem-pool.h\"\n+#include \"pretty.h\"\n+#include \"revision.h\"\n+#include \"notes.h\"\n \n /*\n  * One day.  See the 'name a rev shortly after epoch' test in t6120 when\n@@ -272,14 +275,26 @@ struct name_ref_data {\n \tstruct string_list exclude_filters;\n };\n \n+struct pretty_format {\n+\tstruct pretty_print_context ctx;\n+\tstruct userformat_want want;\n+};\n+\n enum command_type {\n \tNAME_REV = 1,\n+\tFORMAT_REV = 2,\n+};\n+\n+enum stdin_mode {\n+    TEXT = 1,\n+    REVS = 2,\n };\n \n struct command {\n \tenum command_type type;\n \tunion {\n \t\tint name_only;\n+\t\tstruct pretty_format *pretty_format;\n \t} u;\n };\n \n@@ -290,6 +305,13 @@ static void init_name_rev_command(struct command *cmd,\n \tcmd->u.name_only = name_only;\n }\n \n+static void init_format_rev_command(struct command *cmd,\n+\t\t\t\t    struct pretty_format *pretty_format)\n+{\n+\tcmd->type = FORMAT_REV;\n+\tcmd->u.pretty_format = pretty_format;\n+}\n+\n static struct tip_table {\n \tstruct tip_table_entry {\n \t\tstruct object_id oid;\n@@ -495,6 +517,27 @@ static const char *get_rev_name(const struct object *o, struct strbuf *buf)\n \t}\n }\n \n+static const char *get_format_rev(const struct commit *c,\n+\t\t\t\t  struct pretty_format *format_ctx,\n+\t\t\t\t  struct strbuf *buf)\n+{\n+\tstrbuf_reset(buf);\n+\n+\tif (format_ctx->want.notes) {\n+\t\tstruct strbuf notebuf = STRBUF_INIT;\n+\n+\t\tformat_display_notes(&c->object.oid, &notebuf,\n+\t\t\t\t     get_log_output_encoding(),\n+\t\t\t\t     format_ctx->ctx.fmt == CMIT_FMT_USERFORMAT);\n+\t\tformat_ctx->ctx.notes_message = strbuf_detach(&notebuf, NULL);\n+\t}\n+\n+\tpretty_print_commit(&format_ctx->ctx, c, buf);\n+\tFREE_AND_NULL(format_ctx->ctx.notes_message);\n+\n+\treturn buf->buf;\n+}\n+\n static void show_name(const struct object *obj,\n \t\t      const char *caller_name,\n \t\t      int always, int allow_undefined, int name_only)\n@@ -565,6 +608,19 @@ static void name_rev_line(char *p, struct command *cmd)\n \t\t\t\telse\n \t\t\t\t\tprintf(\"%.*s (%s)\", p_len, p_start, name);\n \t\t\t\tbreak;\n+\t\t\tcase FORMAT_REV:\n+\t\t\t\tif (!oid_ret)\n+\t\t\t\t\to = parse_object(the_repository, &oid);\n+\t\t\t\tif (o && o->type == OBJ_COMMIT)\n+\t\t\t\t\tname = get_format_rev((const struct commit *)o,\n+\t\t\t\t\t\t\t      cmd->u.pretty_format,\n+\t\t\t\t\t\t\t      &buf);\n+\t\t\t\t*(p + 1) = c;\n+\t\t\t\tif (name)\n+\t\t\t\t\tprintf(\"%.*s%s\", p_len - hexsz, p_start, name);\n+\t\t\t\telse\n+\t\t\t\t\tprintf(\"%.*s\", p_len, p_start);\n+\t\t\t\tbreak;\n \t\t\tdefault:\n \t\t\t\tBUG(\"uncovered case: %d\", cmd->type);\n \t\t\t}\n@@ -718,3 +774,133 @@ int cmd_name_rev(int argc,\n \tobject_array_clear(&revs);\n \treturn 0;\n }\n+\n+static enum stdin_mode parse_stdin_mode(const char *stdin_mode)\n+{\n+\tif (!strcmp(stdin_mode, \"text\"))\n+\t\treturn TEXT;\n+\telse if (!strcmp(stdin_mode, \"revs\") ||\n+\t\t !strcmp(stdin_mode, \"rev\"))\n+\t\treturn REVS;\n+\telse\n+\t\tdie(_(\"'%s' needs to be either text, revs, or rev\"),\n+\t\t    \"--stdin-mode\");\n+}\n+\n+static char const *const format_rev_usage[] = {\n+\tN_(\"(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> --format=<pretty> [--notes=<ref>]\"),\n+\tNULL\n+};\n+\n+int cmd_format_rev(int argc,\n+\t\t   const char **argv,\n+\t\t   const char *prefix,\n+\t\t   struct repository *repo UNUSED)\n+{\n+\tconst char *format = NULL;\n+\tenum stdin_mode stdin_mode;\n+\tconst char *stdin_mode_arg = NULL;\n+\tstruct display_notes_opt format_notes_opt;\n+\tstruct rev_info format_rev = REV_INFO_INIT;\n+\tstruct pretty_format format_pp = { 0 };\n+\tstruct string_list notes = STRING_LIST_INIT_NODUP;\n+\tstruct strbuf scratch_buf = STRBUF_INIT;\n+\tstruct command cmd;\n+\tstruct option opts[] = {\n+\t\tOPT_STRING(0, \"format\", &format, N_(\"format\"),\n+\t\t\t   N_(\"pretty format to use\")),\n+\t\tOPT_STRING(0, \"stdin-mode\", &stdin_mode_arg, N_(\"stdin-mode\"),\n+\t\t\t   N_(\"how revs are processed\")),\n+\t\tOPT_STRING_LIST(0, \"notes\", &notes, N_(\"notes\"),\n+\t\t\t\tN_(\"display notes for pretty format\")),\n+\t\tOPT_END(),\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, opts, format_rev_usage, 0);\n+\n+\tif (argc > 0) {\n+\t\terror(_(\"too many arguments\"));\n+\t\tusage_with_options(format_rev_usage, opts);\n+\t}\n+\n+\tif (!format)\n+\t\tdie(_(\"'%s' is required\"), \"--format\");\n+\tif (!stdin_mode_arg)\n+\t\tdie(_(\"'%s' is required\"), \"--stdin-mode\");\n+\n+\tinit_display_notes(&format_notes_opt);\n+\tstdin_mode = parse_stdin_mode(stdin_mode_arg);\n+\n+\tget_commit_format(format, &format_rev);\n+\tformat_pp.ctx.rev = &format_rev;\n+\tformat_pp.ctx.fmt = format_rev.commit_format;\n+\tformat_pp.ctx.abbrev = format_rev.abbrev;\n+\tformat_pp.ctx.date_mode_explicit = format_rev.date_mode_explicit;\n+\tformat_pp.ctx.date_mode = format_rev.date_mode;\n+\tformat_pp.ctx.color = GIT_COLOR_AUTO;\n+\n+\tuserformat_find_requirements(format,\n+\t\t\t\t     &format_pp.want);\n+\tif (format_pp.want.notes) {\n+\t\tint ignore_show_notes = 0;\n+\t\tstruct string_list_item *n;\n+\n+\t\tfor_each_string_list_item(n, &notes)\n+\t\t\tenable_ref_display_notes(&format_notes_opt,\n+\t\t\t\t\t\t &ignore_show_notes,\n+\t\t\t\t\t\t n->string);\n+\t\tload_display_notes(&format_notes_opt);\n+\t}\n+\n+\tinit_format_rev_command(&cmd, &format_pp);\n+\n+\tswitch (stdin_mode) {\n+\tcase TEXT:\n+\t\twhile (strbuf_getline(&scratch_buf, stdin) != EOF) {\n+\t\t\tstrbuf_addch(&scratch_buf, '\\n');\n+\t\t\tname_rev_line(scratch_buf.buf, &cmd);\n+\t\t}\n+\t\tbreak;\n+\tcase REVS:\n+\t\twhile (strbuf_getline(&scratch_buf, stdin) != EOF) {\n+\t\t\tstruct object_id oid;\n+\t\t\tstruct object *object;\n+\t\t\tstruct object *peeled;\n+\t\t\tstruct commit *commit;\n+\n+\t\t\tif (repo_get_oid(the_repository, scratch_buf.buf, &oid)) {\n+\t\t\t\tfprintf(stderr, \"Could not get sha1 for %s. Skipping.\\n\",\n+\t\t\t\t\tscratch_buf.buf);\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\tobject = parse_object(the_repository, &oid);\n+\t\t\tif (!object) {\n+\t\t\t\tfprintf(stderr, \"Could not get object for %s. Skipping.\\n\",\n+\t\t\t\t\tscratch_buf.buf);\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\tpeeled = deref_tag(the_repository, object, scratch_buf.buf, 0);\n+\t\t\tif (peeled && peeled->type == OBJ_COMMIT)\n+\t\t\t\tcommit = (struct commit *)peeled;\n+\t\t\tif (!commit) {\n+\t\t\t\tfprintf(stderr, \"Could not get commit for %s. Skipping.\\n\",\n+\t\t\t\t\t*argv);\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\tget_format_rev(commit, &format_pp, &scratch_buf);\n+\t\t\tprintf(\"%s\\n\", scratch_buf.buf);\n+\t\t\tstrbuf_release(&scratch_buf);\n+\t\t}\n+\t\tbreak;\n+\tdefault:\n+\t\tBUG(\"uncovered case: %d\", stdin_mode);\n+\t}\n+\n+\tstrbuf_release(&scratch_buf);\n+\tstring_list_clear(&notes, 0);\n+\trelease_display_notes(&format_notes_opt);\n+\treturn 0;\n+}\ndiff --git a/command-list.txt b/command-list.txt\nindex f9005cf4597..df729872dca 100644\n--- a/command-list.txt\n+++ b/command-list.txt\n@@ -108,6 +108,7 @@ git-fmt-merge-msg                       purehelpers\n git-for-each-ref                        plumbinginterrogators\n git-for-each-repo                       plumbinginterrogators\n git-format-patch                        mainporcelain\n+git-format-rev                          plumbinginterrogators\n git-fsck                                ancillaryinterrogators          complete\n git-gc                                  mainporcelain\n git-get-tar-commit-id                   plumbinginterrogators\ndiff --git a/git.c b/git.c\nindex 2b212e6675d..af5b0422b00 100644\n--- a/git.c\n+++ b/git.c\n@@ -578,6 +578,7 @@ static struct cmd_struct commands[] = {\n \t{ \"for-each-ref\", cmd_for_each_ref, RUN_SETUP },\n \t{ \"for-each-repo\", cmd_for_each_repo, RUN_SETUP_GENTLY },\n \t{ \"format-patch\", cmd_format_patch, RUN_SETUP },\n+\t{ \"format-rev\", cmd_format_rev, RUN_SETUP },\n \t{ \"fsck\", cmd_fsck, RUN_SETUP },\n \t{ \"fsck-objects\", cmd_fsck, RUN_SETUP },\n \t{ \"fsmonitor--daemon\", cmd_fsmonitor__daemon, RUN_SETUP },\ndiff --git a/t/t1517-outside-repo.sh b/t/t1517-outside-repo.sh\nindex c824c1a25cf..360a9323343 100755\n--- a/t/t1517-outside-repo.sh\n+++ b/t/t1517-outside-repo.sh\n@@ -114,7 +114,8 @@ do\n \tarchimport | citool | credential-netrc | credential-libsecret | \\\n \tcredential-osxkeychain | cvsexportcommit | cvsimport | cvsserver | \\\n \tdaemon | \\\n-\tdifftool--helper | filter-branch | fsck-objects | get-tar-commit-id | \\\n+\tdifftool--helper | filter-branch | format-rev | fsck-objects | \\\n+\tget-tar-commit-id | \\\n \tgui | gui--askpass | \\\n \thttp-backend | http-fetch | http-push | init-db | \\\n \tmerge-octopus | merge-one-file | merge-resolve | mergetool | \\\ndiff --git a/t/t6120-describe.sh b/t/t6120-describe.sh\nindex 62789f76381..725f7d81b6b 100755\n--- a/t/t6120-describe.sh\n+++ b/t/t6120-describe.sh\n@@ -801,4 +801,108 @@ test_expect_success 'do not be fooled by invalid describe format ' '\n \ttest_must_fail git cat-file -t \"refs/tags/super-invalid/./../...../ ~^:/?*[////\\\\\\\\\\\\&}/busted.lock-42-g\"$(cat out)\n '\n \n+test_expect_success 'name-rev --format setup' '\n+\tmkdir repo-format &&\n+\tgit -C repo-format init &&\n+\ttest_commit -C repo-format first &&\n+\ttest_commit -C repo-format second &&\n+\ttest_commit -C repo-format third &&\n+\ttest_commit -C repo-format fourth &&\n+\ttest_commit -C repo-format fifth &&\n+\ttest_commit -C repo-format sixth &&\n+\ttest_commit -C repo-format seventh &&\n+\ttest_commit -C repo-format eighth\n+'\n+\n+test_expect_success 'format-rev --stdin-mode=revs' '\n+\tcat >expect <<-\\EOF &&\n+\teighth\n+\tseventh\n+\tfifth\n+\tEOF\n+\tgit -C repo-format format-rev --stdin-mode=revs \\\n+\t\t--format=%s >actual <<-\\EOF &&\n+\tHEAD\n+\tHEAD~\n+\tHEAD~3\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'format-rev --stdin-mode=text from rev-list same as log' '\n+\tgit -C repo-format log --format=reference >expect &&\n+\ttest_file_not_empty expect &&\n+\tgit -C repo-format rev-list HEAD >list &&\n+\tgit -C repo-format format-rev --stdin-mode=text \\\n+\t\t--format=reference <list >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'format-rev --stdin-mode=text with running text and tree oid' '\n+\tcmit_oid=$(git -C repo-format rev-parse :/fifth) &&\n+\treference=$(git -C repo-format log -n1 --format=reference :/fifth) &&\n+\ttree=$(git -C repo-format rev-parse HEAD^{tree}) &&\n+\tcat >expect <<-EOF &&\n+\tWe thought we fixed this in ${reference}.\n+\tBut look at this tree: ${tree}.\n+\tEOF\n+\tgit -C repo-format format-rev --stdin-mode=text --format=reference \\\n+\t\t>actual <<-EOF &&\n+\tWe thought we fixed this in ${cmit_oid}.\n+\tBut look at this tree: ${tree}.\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'format-rev with %N (note)' '\n+\ttest_when_finished \"git -C repo-format notes remove\" &&\n+\tgit -C repo-format notes add -m\"Make a note\" &&\n+\tprintf \"Make a note\\n\\n\\n\" >expect &&\n+\tgit -C repo-format format-rev --stdin-mode=revs \\\n+\t\t--format=\"tformat:%N\" \\\n+\t\t>actual <<-\\EOF &&\n+\tHEAD\n+\tHEAD~\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'format-rev --notes<ref> (custom notes ref)' '\n+\t# One custom notes ref\n+\ttest_when_finished \"git -C repo-format notes remove\" &&\n+\ttest_when_finished \"git -C repo-format notes --ref=word remove\" &&\n+\tgit -C repo-format notes add -m\"default\" &&\n+\tgit -C repo-format notes --ref=word add -m\"custom\" &&\n+\tprintf \"custom\\n\\n\" >expect &&\n+\tgit -C repo-format format-rev --stdin-mode=revs \\\n+\t\t--format=\"tformat:%N\" \\\n+\t\t--notes=word \\\n+\t\t>actual <<-\\EOF &&\n+\tHEAD\n+\tEOF\n+\ttest_cmp expect actual &&\n+\t# Glob all\n+\tprintf \"default\\ncustom\\n\\n\" >expect &&\n+\tgit -C repo-format format-rev --stdin-mode=revs \\\n+\t\t--format=\"tformat:%N\" \\\n+\t\t--notes=* >actual <<-\\EOF &&\n+\tHEAD\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'format-rev --stdin-mode=revs on annotated tag peels to commit' '\n+\ttest_when_finished \"git -C repo-format tag -d version\" &&\n+\tgit -C repo-format tag -a -m\"new version\" version &&\n+\tcat >expect <<-\\EOF &&\n+\teighth\n+\tEOF\n+\tgit -C repo-format format-rev --stdin-mode=revs \\\n+\t\t--format=%s \\\n+\t\t--notes=* >actual <<-\\EOF &&\n+\tversion\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \n2.54.0.13.g9c7419e39f8\n\n"},{"id":"542477","messageId":"374661c1-4676-4538-af24-0564f38469ca@app.fastmail.com","threadId":"65237","inReplyTo":"V3_format-rev_new_builtin.66f@msgid.xyz","subject":"Re: [PATCH v3 5/5] format-rev: introduce builtin for on-demand pretty formatting","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-04-29T13:41:22Z","receivedAt":"2026-04-29T13:41:47Z","isPatch":true,"body":"On Wed, Apr 29, 2026, at 00:25, kristofferhaugsbakk@fastmail.com wrote:\n> From: Kristoffer Haugsbakk <code@khaugsbakk.name>\n>[snip]\n\nCI returned an error.\n\n    builtin/name-rev.c:893:25: error: ‘commit’ may be used uninitialized in this function [-Werror=maybe-uninitialized]\n      893 |                         get_format_rev(commit, &format_pp, &scratch_buf);\n          |                         ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n"},{"id":"542510","messageId":"8cdfbf18-6137-47c0-8134-4cd9f767fa4f@app.fastmail.com","threadId":"65237","inReplyTo":"374661c1-4676-4538-af24-0564f38469ca@app.fastmail.com","subject":"Re: [PATCH v3 5/5] format-rev: introduce builtin for on-demand pretty formatting","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-04-30T06:23:48Z","receivedAt":"2026-04-30T06:24:10Z","isPatch":true,"body":"On Wed, Apr 29, 2026, at 15:41, Kristoffer Haugsbakk wrote:\n> On Wed, Apr 29, 2026, at 00:25, kristofferhaugsbakk@fastmail.com wrote:\n>> From: Kristoffer Haugsbakk <code@khaugsbakk.name>\n>>[snip]\n>\n> CI returned an error.\n>\n>     builtin/name-rev.c:893:25: error: ‘commit’ may be used\n> uninitialized in this function [-Werror=maybe-uninitialized]\n>       893 |                         get_format_rev(commit, &format_pp,\n> &scratch_buf);\n>           |\n> ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nNow Im using `config.mak.dev`.\n"},{"id":"542520","messageId":"0516a53c-d8cf-4a90-9d30-0838f6135642@app.fastmail.com","threadId":"65237","inReplyTo":"V3_format-rev_new_builtin.66f@msgid.xyz","subject":"Re: [PATCH v3 5/5] format-rev: introduce builtin for on-demand pretty formatting","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-04-30T09:21:34Z","receivedAt":"2026-04-30T09:21:56Z","isPatch":true,"body":"On Wed, Apr 29, 2026, at 00:25, kristofferhaugsbakk@fastmail.com wrote:\n> From: Kristoffer Haugsbakk <code@khaugsbakk.name>\n>[snip]\n> +\t\t\tpeeled = deref_tag(the_repository, object, scratch_buf.buf, 0);\n> +\t\t\tif (peeled && peeled->type == OBJ_COMMIT)\n> +\t\t\t\tcommit = (struct commit *)peeled;\n> +\t\t\tif (!commit) {\n> +\t\t\t\tfprintf(stderr, \"Could not get commit for %s. Skipping.\\n\",\n> +\t\t\t\t\t*argv);\n\ns/*argv/scratch_buf.buf/\n\n> +\t\t\t\tcontinue;\n> +\t\t\t}\n>[snip]\n"},{"id":"542532","messageId":"8016697f-9eb7-4c75-be87-d9479186919c@gmail.com","threadId":"65237","inReplyTo":"V3_name-rev_factor.66d@msgid.xyz","subject":"Re: [PATCH v3 3/5] name-rev: factor code for sharing with a new command","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-04-30T13:54:03Z","receivedAt":"2026-04-30T13:54:08Z","isPatch":true,"body":"Hi Kristoffer\n\nOn 28/04/2026 23:25, kristofferhaugsbakk@fastmail.com wrote:\n> From: Kristoffer Haugsbakk <code@khaugsbakk.name>\n> \n> @@ -516,6 +534,7 @@ static void name_rev_line(char *p, struct name_ref_data *data)\n>   \n>   \tfor (p_start = p; *p; p++) {\n>   #define ishex(x) (isdigit((x)) || ((x) >= 'a' && (x) <= 'f'))\n> +\tstart:\n>   \t\tif (!ishex(*p)) {\n>   \t\t\tcounter = 0;\n>   \t\t} else if (++counter == hexsz &&\n> @@ -524,25 +543,32 @@ static void name_rev_line(char *p, struct name_ref_data *data)\n>   \t\t\tconst char *name = NULL;\n>   \t\t\tchar c = *(p + 1);\n>   \t\t\tint p_len = p - p_start + 1;\n> +\t\t\tstruct object *o = NULL;\n> +\t\t\tint oid_ret = 1;\n>   \n>   \t\t\tcounter = 0;\n>   \n>   \t\t\t*(p + 1) = 0;\n> -\t\t\tif (!repo_get_oid(the_repository, p - (hexsz - 1), &oid)) {\n> -\t\t\t\tstruct object *o =\n> -\t\t\t\t\tlookup_object(the_repository, &oid);\n> +\t\t\toid_ret = repo_get_oid(the_repository, p - (hexsz - 1), &oid);\n\nIt would be safer to restore *(p + 1) here rather that relying on each \ncase block to do it.\n\n\t\t\t*(p + 1) = c;\n> +\n> +\t\t\tswitch (cmd->type) {\n> +\t\t\tcase NAME_REV:\n> +\t\t\t\tif (!oid_ret)\n> +\t\t\t\t\to = lookup_object(the_repository, &oid);\n>   \t\t\t\tif (o)\n>   \t\t\t\t\tname = get_rev_name(o, &buf);\n> +\t\t\t\t*(p + 1) = c;\n> +\t\t\t\tif (!name)\n> +\t\t\t\t\tgoto start;\n\nThe pre-image uses \"continue\" which will increment p - why the change in \nbehavior?\n\nThanks\n\nPhillip\n\n> +\t\t\t\tif (cmd->u.name_only)\n> +\t\t\t\t\tprintf(\"%.*s%s\", p_len - hexsz, p_start, name);\n> +\t\t\t\telse\n> +\t\t\t\t\tprintf(\"%.*s (%s)\", p_len, p_start, name);\n> +\t\t\t\tbreak;\n> +\t\t\tdefault:\n> +\t\t\t\tBUG(\"uncovered case: %d\", cmd->type);\n>   \t\t\t}\n> -\t\t\t*(p + 1) = c;\n> -\n> -\t\t\tif (!name)\n> -\t\t\t\tcontinue;\n>   \n> -\t\t\tif (data->name_only)\n> -\t\t\t\tprintf(\"%.*s%s\", p_len - hexsz, p_start, name);\n> -\t\t\telse\n> -\t\t\t\tprintf(\"%.*s (%s)\", p_len, p_start, name);\n>   \t\t\tp_start = p + 1;\n>   \t\t}\n>   \t}\n> @@ -567,6 +593,7 @@ int cmd_name_rev(int argc,\n>   #endif\n>   \tint all = 0, annotate_stdin = 0, allow_undefined = 1, always = 0, peel_tag = 0;\n>   \tstruct name_ref_data data = { 0, 0, STRING_LIST_INIT_NODUP, STRING_LIST_INIT_NODUP };\n> +\tstruct command cmd;\n>   \tstruct option opts[] = {\n>   \t\tOPT_BOOL(0, \"name-only\", &data.name_only, N_(\"print only ref-based names (no object names)\")),\n>   \t\tOPT_BOOL(0, \"tags\", &data.tags_only, N_(\"only use tags to name the commits\")),\n> @@ -596,6 +623,7 @@ int cmd_name_rev(int argc,\n>   \tinit_commit_rev_name(&rev_names);\n>   \trepo_config(the_repository, git_default_config, NULL);\n>   \targc = parse_options(argc, argv, prefix, opts, name_rev_usage, 0);\n> +\tinit_name_rev_command(&cmd, data.name_only);\n>   \n>   #ifndef WITH_BREAKING_CHANGES\n>   \tif (transform_stdin) {\n> @@ -663,7 +691,7 @@ int cmd_name_rev(int argc,\n>   \n>   \t\twhile (strbuf_getline(&sb, stdin) != EOF) {\n>   \t\t\tstrbuf_addch(&sb, '\\n');\n> -\t\t\tname_rev_line(sb.buf, &data);\n> +\t\t\tname_rev_line(sb.buf, &cmd);\n>   \t\t}\n>   \t\tstrbuf_release(&sb);\n>   \t} else if (all) {\n\n"},{"id":"542544","messageId":"e8bb57a2-8eff-4f72-96ca-72d8880901e0@gmail.com","threadId":"65237","inReplyTo":"V3_format-rev_new_builtin.66f@msgid.xyz","subject":"Re: [PATCH v3 5/5] format-rev: introduce builtin for on-demand pretty formatting","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-05-01T10:16:10Z","receivedAt":"2026-05-01T10:16:13Z","isPatch":true,"body":"Hi Kristoffer\n\nOn 28/04/2026 23:25, kristofferhaugsbakk@fastmail.com wrote:\n> From: Kristoffer Haugsbakk <code@khaugsbakk.name>\n> \n> Introduce a new builtin for pretty formatting one revision expression\n> per line or commit object names found in running text.\n> \n> Sometimes you want to format commits. Most of the time you’re\n> walking the graph, e.g. getting a range of commits like\n> `master..topic`. That’s a job for git-log(1).\n> \n> But there are times when you want to format commits that you encounter\n> on demand:\n> \n> • Full hashes in running text that you might want to pretty-print\n> • git-last-modified(1) outputs full hashes that you can do the same\n>    with\n> • git-cherry(1) has `-v` for commit subject, but maybe you want\n>    something else?\n> \n> But now you can’t use git-log(1), git-show(1), or git-rev-list(1):\n> \n> • You can’t feed commits piecemeal to these commands, one input\n>    for one output; they block until standard in is closed\n\nSo you can feed them piecemeal but you don't get any output until you \nclose stdin. That can be helpful as it means the calling process can \nwrite to \"git log --stdin\" and then read the output without worrying \nabout getting deadlocked. The Implementation below works fine if there \nare separate processes or threads writing to and reading from \"git \nformat-rev\", but if we want a single process to be able to read from and \nwrite to \"git format-rev --stdin-mode=text\" there will need to be a way \nto delimit message boundaries so that git knows where the input message \nends and the caller knows where the response ends. We'll also need to be \ncareful about flushing the output at the end of a processed message.\n\nFor \"--stdin-mode=revs\" the caller cannot know how many lines the output \nwill span because formats like %(trailers) will produce a variable \nnumber of lines depending on which trailers are present. It is also \npossible for a rev name to span more than one line. The following \nexample finds the most recent commit that mentions 'cherry-pick' in the \nsubject line\n\n:/^[^\n]cherry-pick\n\nso we need a way to delimit the input and output records there as well. \nI think the functionality implemented here is useful (transforming the \noutput of 'git blame' or 'git-last-modified' are convicing examples) and \nit is probably better to do it as a command rather than adding a \n\"--format\" option to name-rev.\n\n> • You can’t feed a list of possibly duplicate commits, like the output\n>    of git-last-modified(1); they effectively deduplicate the output\n\nThat is definitely a problem\n\n> Beyond these two points there’s also the input massage problem: you\n\ns/massagge/message/?\n\nThanks\n\nPhillip\n\n> cannot feed mixed input (revisions mixed with arbitrary text).\n> \n> One might hope that git-cat-file(1) can save us. But it doesn’t\n> support pretty formats.\n> \n> But there is one command that already both handles revisions as\n> arguments, revisions on standard input, and even revisions mixed in\n> with arbitrary text. Namely git-name-rev(1): the command for outputting\n> symbolic names for commits.\n> \n> We made some room in `builtin/name-rev.c` two commits ago. Let’s\n> now add this new git-format-rev(1) command. Taking inspiration from\n> git-name-rev(1), there are two modes:\n> \n> • revs: like git-name-rev(1) in argv mode, but one revision per line\n>    on standard in\n> • text: like git-name-rev(1) with `--annotate-stdin`\n> \n> ***\n> \n> We need to add this command to the exception list in\n> `t/t1517-outside-repo.sh` because it uses “EXPERIMENTAL!”\n> in the usage line.\n> \n> Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>\n> ---\n> \n> Notes (series):\n>      v3:\n>      • And don’t forget to document --notes this time\n>      \n>            https://lore.kernel.org/git/CALnO6CB5WOTp_e7Kv3CrEbQ+3XE-gDxNVHf7qATBEbyKWfxpLg@mail.gmail.com/\n> \n>   Documentation/git-format-rev.adoc | 148 ++++++++++++++++++++++++\n>   Makefile                          |   1 +\n>   builtin.h                         |   1 +\n>   builtin/name-rev.c                | 186 ++++++++++++++++++++++++++++++\n>   command-list.txt                  |   1 +\n>   git.c                             |   1 +\n>   t/t1517-outside-repo.sh           |   3 +-\n>   t/t6120-describe.sh               | 104 +++++++++++++++++\n>   8 files changed, 444 insertions(+), 1 deletion(-)\n>   create mode 100644 Documentation/git-format-rev.adoc\n> \n> diff --git a/Documentation/git-format-rev.adoc b/Documentation/git-format-rev.adoc\n> new file mode 100644\n> index 00000000000..d960001d750\n> --- /dev/null\n> +++ b/Documentation/git-format-rev.adoc\n> @@ -0,0 +1,148 @@\n> +git-format-rev(1)\n> +=================\n> +\n> +NAME\n> +----\n> +git-format-rev - EXPERIMENTAL: Pretty format revisions on demand\n> +\n> +\n> +SYNOPSIS\n> +--------\n> +[synopsis]\n> +(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> --format=<pretty> [--notes=<ref>]\n> +\n> +DESCRIPTION\n> +-----------\n> +\n> +Pretty format revisions from standard input.\n> +\n> +THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.\n> +\n> +OPTIONS\n> +-------\n> +\n> +`--stdin-mode=<mode>`::\n> +\tHow to interpret standard input data:\n> ++\n> +--\n> +`revs`:: Each line is interpreted as a commit. Any kind of revision\n> +\texpression can be used (see linkgit:gitrevisions[7]). Annotated\n> +\ttags are peeled (see linkgit:gitglossary[7]).\n> ++\n> +The argument `rev` is also accepted.\n> +`text`:: Formats all commit object names found in freeform text. These\n> +\tmust the full object names, i.e. abbreviated hexidecimal object\n> +\tnames will not be interpreted.\n> +--\n> +\n> +`--format=<pretty>`::\n> +\tPretty format string.\n> +\n> +`--notes=<ref>`::\n> +`--no-notes`::\n> +\tCustom notes ref. Notes are displayed when using the `%N`\n> +\tatom. See linkgit:git-notes[1].\n> +\n> +EXAMPLES\n> +--------\n> +\n> +The command linkgit:git-last-modified[1] shows the commit that each file\n> +was last modified in.\n> +\n> +----\n> +$ git last-modified -- README.md Makefile\n> +7798034171030be0909c56377a4e0e10e6d2df93\tMakefile\n> +c50fbb2dd225e7e82abba4380423ae105089f4d7\tREADME.md\n> +----\n> +\n> +We can pipe the result to this command in order to replace the object\n> +name with the commit author.\n> +\n> +----\n> +$ git last-modified -- README.md Makefile |\n> +    git format-rev --stdin-mode=text --format=%an\n> +Junio C Hamano\tMakefile\n> +Todd Zullinger\tREADME.md\n> +----\n> +\n> +Another example is _formatting commits in commit messages_. Given this commit message:\n> +\n> +----\n> +Fix off-by-one error\n> +\n> +Fix off-by-one error introduced in\n> +e83c5163316f89bfbde7d9ab23ca2e25604af290.\n> +\n> +We thought we fixed this in 5569bf9bbedd63a00780fc5c110e0cfab3aa97b9 but\n> +that only covered 1/3 of the faulty cases.\n> +----\n> +\n> +We can format the commits and use par(1) to reflow the text, say in a\n> +`commit-msg` hook:\n> +\n> +----\n> +$ git config set hook.reference-commits.event commit-msg\n> +$ git config set hook.reference-commits.command reference-commits\n> +$ cat $(which reference-commits)\n> +#/bin/sh\n> +\n> +msg=\"$1\"\n> +rewritten=$(mktemp)\n> +git format-rev --stdin-mode=text --format=reference <\"$msg\" |\n> +    par >\"$rewritten\"\n> +mv \"$rewritten\" \"$msg\"\n> +----\n> +\n> +Which will produce something like this:\n> +\n> +----\n> +Fix off-by-one error\n> +\n> +Fix off-by-one error introduced in e83c5163316 (Implement better memory\n> +allocator, 2005-04-07).\n> +\n> +We thought we fixed this in 5569bf9bbed (Fix memory allocator,\n> +2005-06-22) but that only covered 1/3 of the faulty cases.\n> +----\n> +\n> +DISCUSSION\n> +----------\n> +\n> +This command lets you format any number of revisions in any order\n> +through one command invocation. Consider the\n> +linkgit:git-last-modified[1] case from the \"EXAMPLES\" section above:\n> +\n> +1. There might be hundreds of files\n> +2. Commits can be repeated, i.e. two or more files were last modified in\n> +   the same commit\n> +\n> +Two widely-used commands which pretty formats commits are\n> +linkgit:git-log[1] and linkgit:git-show[1]. It turns out that they are\n> +not a good fit for the above use case.\n> +\n> +- The output of linkgit:git-last-modified[1] would have to be processed\n> +  in stages since you need to transform the first column separately and\n> +  then link the author to the filename. But this is surmountable.\n> +- You can feed each commit to `git show` or `git show --no-walk -1`. But\n> +  that means that you need to create a process for each line.\n> +- Let’s say that you want to use one process, not one per line. So you\n> +  want to feed all the commits to the command. Now you face the problem\n> +  that you have to feed all the commits to the commands before you get\n> +  any output (this is also the case for the `--stdin` modes). In other\n> +  words, you cannot loop through each line, get the author for the\n> +  commit, and output the author and the filename. You need to feed all\n> +  the commits, get back all the output, and match the output with the\n> +  filename.\n> +- But the next problem is that commands will deduplicate the input and\n> +  only output one commit one single time only. Thus you cannot make the\n> +  output order match the input order, since a commit could have been\n> +  repeated in the original input.\n> +\n> +In short, it is straightforward to use these two commands if you use one\n> +process per line. It is much more work if you just want to use one\n> +process, but still doable. In contrast, this problem is just another\n> +shell pipeline with this command.\n> +\n> +GIT\n> +---\n> +Part of the linkgit:git[1] suite\n> diff --git a/Makefile b/Makefile\n> index 15b1ded1a0b..cbaf91fd846 100644\n> --- a/Makefile\n> +++ b/Makefile\n> @@ -895,6 +895,7 @@ BUILT_INS += $(patsubst builtin/%.o,git-%$X,$(BUILTIN_OBJS))\n>   BUILT_INS += git-cherry$X\n>   BUILT_INS += git-cherry-pick$X\n>   BUILT_INS += git-format-patch$X\n> +BUILT_INS += git-format-rev$X\n>   BUILT_INS += git-fsck-objects$X\n>   BUILT_INS += git-init$X\n>   BUILT_INS += git-maintenance$X\n> diff --git a/builtin.h b/builtin.h\n> index 235c51f30e5..63813c90125 100644\n> --- a/builtin.h\n> +++ b/builtin.h\n> @@ -189,6 +189,7 @@ int cmd_fmt_merge_msg(int argc, const char **argv, const char *prefix, struct re\n>   int cmd_for_each_ref(int argc, const char **argv, const char *prefix, struct repository *repo);\n>   int cmd_for_each_repo(int argc, const char **argv, const char *prefix, struct repository *repo);\n>   int cmd_format_patch(int argc, const char **argv, const char *prefix, struct repository *repo);\n> +int cmd_format_rev(int argc, const char **argv, const char *prefix, struct repository *repo);\n>   int cmd_fsck(int argc, const char **argv, const char *prefix, struct repository *repo);\n>   int cmd_fsmonitor__daemon(int argc, const char **argv, const char *prefix, struct repository *repo);\n>   int cmd_gc(int argc, const char **argv, const char *prefix, struct repository *repo);\n> diff --git a/builtin/name-rev.c b/builtin/name-rev.c\n> index dc4136f4de3..b60cc766279 100644\n> --- a/builtin/name-rev.c\n> +++ b/builtin/name-rev.c\n> @@ -18,6 +18,9 @@\n>   #include \"commit-graph.h\"\n>   #include \"wildmatch.h\"\n>   #include \"mem-pool.h\"\n> +#include \"pretty.h\"\n> +#include \"revision.h\"\n> +#include \"notes.h\"\n>   \n>   /*\n>    * One day.  See the 'name a rev shortly after epoch' test in t6120 when\n> @@ -272,14 +275,26 @@ struct name_ref_data {\n>   \tstruct string_list exclude_filters;\n>   };\n>   \n> +struct pretty_format {\n> +\tstruct pretty_print_context ctx;\n> +\tstruct userformat_want want;\n> +};\n> +\n>   enum command_type {\n>   \tNAME_REV = 1,\n> +\tFORMAT_REV = 2,\n> +};\n> +\n> +enum stdin_mode {\n> +    TEXT = 1,\n> +    REVS = 2,\n>   };\n>   \n>   struct command {\n>   \tenum command_type type;\n>   \tunion {\n>   \t\tint name_only;\n> +\t\tstruct pretty_format *pretty_format;\n>   \t} u;\n>   };\n>   \n> @@ -290,6 +305,13 @@ static void init_name_rev_command(struct command *cmd,\n>   \tcmd->u.name_only = name_only;\n>   }\n>   \n> +static void init_format_rev_command(struct command *cmd,\n> +\t\t\t\t    struct pretty_format *pretty_format)\n> +{\n> +\tcmd->type = FORMAT_REV;\n> +\tcmd->u.pretty_format = pretty_format;\n> +}\n> +\n>   static struct tip_table {\n>   \tstruct tip_table_entry {\n>   \t\tstruct object_id oid;\n> @@ -495,6 +517,27 @@ static const char *get_rev_name(const struct object *o, struct strbuf *buf)\n>   \t}\n>   }\n>   \n> +static const char *get_format_rev(const struct commit *c,\n> +\t\t\t\t  struct pretty_format *format_ctx,\n> +\t\t\t\t  struct strbuf *buf)\n> +{\n> +\tstrbuf_reset(buf);\n> +\n> +\tif (format_ctx->want.notes) {\n> +\t\tstruct strbuf notebuf = STRBUF_INIT;\n> +\n> +\t\tformat_display_notes(&c->object.oid, &notebuf,\n> +\t\t\t\t     get_log_output_encoding(),\n> +\t\t\t\t     format_ctx->ctx.fmt == CMIT_FMT_USERFORMAT);\n> +\t\tformat_ctx->ctx.notes_message = strbuf_detach(&notebuf, NULL);\n> +\t}\n> +\n> +\tpretty_print_commit(&format_ctx->ctx, c, buf);\n> +\tFREE_AND_NULL(format_ctx->ctx.notes_message);\n> +\n> +\treturn buf->buf;\n> +}\n> +\n>   static void show_name(const struct object *obj,\n>   \t\t      const char *caller_name,\n>   \t\t      int always, int allow_undefined, int name_only)\n> @@ -565,6 +608,19 @@ static void name_rev_line(char *p, struct command *cmd)\n>   \t\t\t\telse\n>   \t\t\t\t\tprintf(\"%.*s (%s)\", p_len, p_start, name);\n>   \t\t\t\tbreak;\n> +\t\t\tcase FORMAT_REV:\n> +\t\t\t\tif (!oid_ret)\n> +\t\t\t\t\to = parse_object(the_repository, &oid);\n> +\t\t\t\tif (o && o->type == OBJ_COMMIT)\n> +\t\t\t\t\tname = get_format_rev((const struct commit *)o,\n> +\t\t\t\t\t\t\t      cmd->u.pretty_format,\n> +\t\t\t\t\t\t\t      &buf);\n> +\t\t\t\t*(p + 1) = c;\n> +\t\t\t\tif (name)\n> +\t\t\t\t\tprintf(\"%.*s%s\", p_len - hexsz, p_start, name);\n> +\t\t\t\telse\n> +\t\t\t\t\tprintf(\"%.*s\", p_len, p_start);\n> +\t\t\t\tbreak;\n>   \t\t\tdefault:\n>   \t\t\t\tBUG(\"uncovered case: %d\", cmd->type);\n>   \t\t\t}\n> @@ -718,3 +774,133 @@ int cmd_name_rev(int argc,\n>   \tobject_array_clear(&revs);\n>   \treturn 0;\n>   }\n> +\n> +static enum stdin_mode parse_stdin_mode(const char *stdin_mode)\n> +{\n> +\tif (!strcmp(stdin_mode, \"text\"))\n> +\t\treturn TEXT;\n> +\telse if (!strcmp(stdin_mode, \"revs\") ||\n> +\t\t !strcmp(stdin_mode, \"rev\"))\n> +\t\treturn REVS;\n> +\telse\n> +\t\tdie(_(\"'%s' needs to be either text, revs, or rev\"),\n> +\t\t    \"--stdin-mode\");\n> +}\n> +\n> +static char const *const format_rev_usage[] = {\n> +\tN_(\"(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> --format=<pretty> [--notes=<ref>]\"),\n> +\tNULL\n> +};\n> +\n> +int cmd_format_rev(int argc,\n> +\t\t   const char **argv,\n> +\t\t   const char *prefix,\n> +\t\t   struct repository *repo UNUSED)\n> +{\n> +\tconst char *format = NULL;\n> +\tenum stdin_mode stdin_mode;\n> +\tconst char *stdin_mode_arg = NULL;\n> +\tstruct display_notes_opt format_notes_opt;\n> +\tstruct rev_info format_rev = REV_INFO_INIT;\n> +\tstruct pretty_format format_pp = { 0 };\n> +\tstruct string_list notes = STRING_LIST_INIT_NODUP;\n> +\tstruct strbuf scratch_buf = STRBUF_INIT;\n> +\tstruct command cmd;\n> +\tstruct option opts[] = {\n> +\t\tOPT_STRING(0, \"format\", &format, N_(\"format\"),\n> +\t\t\t   N_(\"pretty format to use\")),\n> +\t\tOPT_STRING(0, \"stdin-mode\", &stdin_mode_arg, N_(\"stdin-mode\"),\n> +\t\t\t   N_(\"how revs are processed\")),\n> +\t\tOPT_STRING_LIST(0, \"notes\", &notes, N_(\"notes\"),\n> +\t\t\t\tN_(\"display notes for pretty format\")),\n> +\t\tOPT_END(),\n> +\t};\n> +\n> +\targc = parse_options(argc, argv, prefix, opts, format_rev_usage, 0);\n> +\n> +\tif (argc > 0) {\n> +\t\terror(_(\"too many arguments\"));\n> +\t\tusage_with_options(format_rev_usage, opts);\n> +\t}\n> +\n> +\tif (!format)\n> +\t\tdie(_(\"'%s' is required\"), \"--format\");\n> +\tif (!stdin_mode_arg)\n> +\t\tdie(_(\"'%s' is required\"), \"--stdin-mode\");\n> +\n> +\tinit_display_notes(&format_notes_opt);\n> +\tstdin_mode = parse_stdin_mode(stdin_mode_arg);\n> +\n> +\tget_commit_format(format, &format_rev);\n> +\tformat_pp.ctx.rev = &format_rev;\n> +\tformat_pp.ctx.fmt = format_rev.commit_format;\n> +\tformat_pp.ctx.abbrev = format_rev.abbrev;\n> +\tformat_pp.ctx.date_mode_explicit = format_rev.date_mode_explicit;\n> +\tformat_pp.ctx.date_mode = format_rev.date_mode;\n> +\tformat_pp.ctx.color = GIT_COLOR_AUTO;\n> +\n> +\tuserformat_find_requirements(format,\n> +\t\t\t\t     &format_pp.want);\n> +\tif (format_pp.want.notes) {\n> +\t\tint ignore_show_notes = 0;\n> +\t\tstruct string_list_item *n;\n> +\n> +\t\tfor_each_string_list_item(n, &notes)\n> +\t\t\tenable_ref_display_notes(&format_notes_opt,\n> +\t\t\t\t\t\t &ignore_show_notes,\n> +\t\t\t\t\t\t n->string);\n> +\t\tload_display_notes(&format_notes_opt);\n> +\t}\n> +\n> +\tinit_format_rev_command(&cmd, &format_pp);\n> +\n> +\tswitch (stdin_mode) {\n> +\tcase TEXT:\n> +\t\twhile (strbuf_getline(&scratch_buf, stdin) != EOF) {\n> +\t\t\tstrbuf_addch(&scratch_buf, '\\n');\n> +\t\t\tname_rev_line(scratch_buf.buf, &cmd);\n> +\t\t}\n> +\t\tbreak;\n> +\tcase REVS:\n> +\t\twhile (strbuf_getline(&scratch_buf, stdin) != EOF) {\n> +\t\t\tstruct object_id oid;\n> +\t\t\tstruct object *object;\n> +\t\t\tstruct object *peeled;\n> +\t\t\tstruct commit *commit;\n> +\n> +\t\t\tif (repo_get_oid(the_repository, scratch_buf.buf, &oid)) {\n> +\t\t\t\tfprintf(stderr, \"Could not get sha1 for %s. Skipping.\\n\",\n> +\t\t\t\t\tscratch_buf.buf);\n> +\t\t\t\tcontinue;\n> +\t\t\t}\n> +\n> +\t\t\tobject = parse_object(the_repository, &oid);\n> +\t\t\tif (!object) {\n> +\t\t\t\tfprintf(stderr, \"Could not get object for %s. Skipping.\\n\",\n> +\t\t\t\t\tscratch_buf.buf);\n> +\t\t\t\tcontinue;\n> +\t\t\t}\n> +\n> +\t\t\tpeeled = deref_tag(the_repository, object, scratch_buf.buf, 0);\n> +\t\t\tif (peeled && peeled->type == OBJ_COMMIT)\n> +\t\t\t\tcommit = (struct commit *)peeled;\n> +\t\t\tif (!commit) {\n> +\t\t\t\tfprintf(stderr, \"Could not get commit for %s. Skipping.\\n\",\n> +\t\t\t\t\t*argv);\n> +\t\t\t\tcontinue;\n> +\t\t\t}\n> +\n> +\t\t\tget_format_rev(commit, &format_pp, &scratch_buf);\n> +\t\t\tprintf(\"%s\\n\", scratch_buf.buf);\n> +\t\t\tstrbuf_release(&scratch_buf);\n> +\t\t}\n> +\t\tbreak;\n> +\tdefault:\n> +\t\tBUG(\"uncovered case: %d\", stdin_mode);\n> +\t}\n> +\n> +\tstrbuf_release(&scratch_buf);\n> +\tstring_list_clear(&notes, 0);\n> +\trelease_display_notes(&format_notes_opt);\n> +\treturn 0;\n> +}\n> diff --git a/command-list.txt b/command-list.txt\n> index f9005cf4597..df729872dca 100644\n> --- a/command-list.txt\n> +++ b/command-list.txt\n> @@ -108,6 +108,7 @@ git-fmt-merge-msg                       purehelpers\n>   git-for-each-ref                        plumbinginterrogators\n>   git-for-each-repo                       plumbinginterrogators\n>   git-format-patch                        mainporcelain\n> +git-format-rev                          plumbinginterrogators\n>   git-fsck                                ancillaryinterrogators          complete\n>   git-gc                                  mainporcelain\n>   git-get-tar-commit-id                   plumbinginterrogators\n> diff --git a/git.c b/git.c\n> index 2b212e6675d..af5b0422b00 100644\n> --- a/git.c\n> +++ b/git.c\n> @@ -578,6 +578,7 @@ static struct cmd_struct commands[] = {\n>   \t{ \"for-each-ref\", cmd_for_each_ref, RUN_SETUP },\n>   \t{ \"for-each-repo\", cmd_for_each_repo, RUN_SETUP_GENTLY },\n>   \t{ \"format-patch\", cmd_format_patch, RUN_SETUP },\n> +\t{ \"format-rev\", cmd_format_rev, RUN_SETUP },\n>   \t{ \"fsck\", cmd_fsck, RUN_SETUP },\n>   \t{ \"fsck-objects\", cmd_fsck, RUN_SETUP },\n>   \t{ \"fsmonitor--daemon\", cmd_fsmonitor__daemon, RUN_SETUP },\n> diff --git a/t/t1517-outside-repo.sh b/t/t1517-outside-repo.sh\n> index c824c1a25cf..360a9323343 100755\n> --- a/t/t1517-outside-repo.sh\n> +++ b/t/t1517-outside-repo.sh\n> @@ -114,7 +114,8 @@ do\n>   \tarchimport | citool | credential-netrc | credential-libsecret | \\\n>   \tcredential-osxkeychain | cvsexportcommit | cvsimport | cvsserver | \\\n>   \tdaemon | \\\n> -\tdifftool--helper | filter-branch | fsck-objects | get-tar-commit-id | \\\n> +\tdifftool--helper | filter-branch | format-rev | fsck-objects | \\\n> +\tget-tar-commit-id | \\\n>   \tgui | gui--askpass | \\\n>   \thttp-backend | http-fetch | http-push | init-db | \\\n>   \tmerge-octopus | merge-one-file | merge-resolve | mergetool | \\\n> diff --git a/t/t6120-describe.sh b/t/t6120-describe.sh\n> index 62789f76381..725f7d81b6b 100755\n> --- a/t/t6120-describe.sh\n> +++ b/t/t6120-describe.sh\n> @@ -801,4 +801,108 @@ test_expect_success 'do not be fooled by invalid describe format ' '\n>   \ttest_must_fail git cat-file -t \"refs/tags/super-invalid/./../...../ ~^:/?*[////\\\\\\\\\\\\&}/busted.lock-42-g\"$(cat out)\n>   '\n>   \n> +test_expect_success 'name-rev --format setup' '\n> +\tmkdir repo-format &&\n> +\tgit -C repo-format init &&\n> +\ttest_commit -C repo-format first &&\n> +\ttest_commit -C repo-format second &&\n> +\ttest_commit -C repo-format third &&\n> +\ttest_commit -C repo-format fourth &&\n> +\ttest_commit -C repo-format fifth &&\n> +\ttest_commit -C repo-format sixth &&\n> +\ttest_commit -C repo-format seventh &&\n> +\ttest_commit -C repo-format eighth\n> +'\n> +\n> +test_expect_success 'format-rev --stdin-mode=revs' '\n> +\tcat >expect <<-\\EOF &&\n> +\teighth\n> +\tseventh\n> +\tfifth\n> +\tEOF\n> +\tgit -C repo-format format-rev --stdin-mode=revs \\\n> +\t\t--format=%s >actual <<-\\EOF &&\n> +\tHEAD\n> +\tHEAD~\n> +\tHEAD~3\n> +\tEOF\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'format-rev --stdin-mode=text from rev-list same as log' '\n> +\tgit -C repo-format log --format=reference >expect &&\n> +\ttest_file_not_empty expect &&\n> +\tgit -C repo-format rev-list HEAD >list &&\n> +\tgit -C repo-format format-rev --stdin-mode=text \\\n> +\t\t--format=reference <list >actual &&\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'format-rev --stdin-mode=text with running text and tree oid' '\n> +\tcmit_oid=$(git -C repo-format rev-parse :/fifth) &&\n> +\treference=$(git -C repo-format log -n1 --format=reference :/fifth) &&\n> +\ttree=$(git -C repo-format rev-parse HEAD^{tree}) &&\n> +\tcat >expect <<-EOF &&\n> +\tWe thought we fixed this in ${reference}.\n> +\tBut look at this tree: ${tree}.\n> +\tEOF\n> +\tgit -C repo-format format-rev --stdin-mode=text --format=reference \\\n> +\t\t>actual <<-EOF &&\n> +\tWe thought we fixed this in ${cmit_oid}.\n> +\tBut look at this tree: ${tree}.\n> +\tEOF\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'format-rev with %N (note)' '\n> +\ttest_when_finished \"git -C repo-format notes remove\" &&\n> +\tgit -C repo-format notes add -m\"Make a note\" &&\n> +\tprintf \"Make a note\\n\\n\\n\" >expect &&\n> +\tgit -C repo-format format-rev --stdin-mode=revs \\\n> +\t\t--format=\"tformat:%N\" \\\n> +\t\t>actual <<-\\EOF &&\n> +\tHEAD\n> +\tHEAD~\n> +\tEOF\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'format-rev --notes<ref> (custom notes ref)' '\n> +\t# One custom notes ref\n> +\ttest_when_finished \"git -C repo-format notes remove\" &&\n> +\ttest_when_finished \"git -C repo-format notes --ref=word remove\" &&\n> +\tgit -C repo-format notes add -m\"default\" &&\n> +\tgit -C repo-format notes --ref=word add -m\"custom\" &&\n> +\tprintf \"custom\\n\\n\" >expect &&\n> +\tgit -C repo-format format-rev --stdin-mode=revs \\\n> +\t\t--format=\"tformat:%N\" \\\n> +\t\t--notes=word \\\n> +\t\t>actual <<-\\EOF &&\n> +\tHEAD\n> +\tEOF\n> +\ttest_cmp expect actual &&\n> +\t# Glob all\n> +\tprintf \"default\\ncustom\\n\\n\" >expect &&\n> +\tgit -C repo-format format-rev --stdin-mode=revs \\\n> +\t\t--format=\"tformat:%N\" \\\n> +\t\t--notes=* >actual <<-\\EOF &&\n> +\tHEAD\n> +\tEOF\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'format-rev --stdin-mode=revs on annotated tag peels to commit' '\n> +\ttest_when_finished \"git -C repo-format tag -d version\" &&\n> +\tgit -C repo-format tag -a -m\"new version\" version &&\n> +\tcat >expect <<-\\EOF &&\n> +\teighth\n> +\tEOF\n> +\tgit -C repo-format format-rev --stdin-mode=revs \\\n> +\t\t--format=%s \\\n> +\t\t--notes=* >actual <<-\\EOF &&\n> +\tversion\n> +\tEOF\n> +\ttest_cmp expect actual\n> +'\n> +\n>   test_done\n\n"},{"id":"542555","messageId":"20260501172450.25037-2-kristofferhaugsbakk@fastmail.com","threadId":"65237","inReplyTo":"8016697f-9eb7-4c75-be87-d9479186919c@gmail.com","subject":"Re: [PATCH v3 3/5] name-rev: factor code for sharing with a new command","fromName":"","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-05-01T17:24:49Z","receivedAt":"2026-05-01T17:25:13Z","isPatch":true,"body":"Hi Phillip. Thanks for taking a look.\n\nOn Thu, Apr 30, 2026, at 15:54, Phillip Wood wrote:\n>>[snip]\n>> @@ -524,25 +543,32 @@ static void name_rev_line(char *p, struct name_ref_data *data)\n>>   \t\t\tconst char *name = NULL;\n>>   \t\t\tchar c = *(p + 1);\n>>   \t\t\tint p_len = p - p_start + 1;\n>> +\t\t\tstruct object *o = NULL;\n>> +\t\t\tint oid_ret = 1;\n>>\n>>   \t\t\tcounter = 0;\n>>\n>>   \t\t\t*(p + 1) = 0;\n>> -\t\t\tif (!repo_get_oid(the_repository, p - (hexsz - 1), &oid)) {\n>> -\t\t\t\tstruct object *o =\n>> -\t\t\t\t\tlookup_object(the_repository, &oid);\n>> +\t\t\toid_ret = repo_get_oid(the_repository, p - (hexsz - 1), &oid);\n>\n> It would be safer to restore *(p + 1) here rather that relying on each\n> case block to do it.\n\nYeah, I didn’t want to repeat that bookkeeping but in some iteration it\nlooked necessary. But it’s good that it isn’t.\n\n>\n> \t\t\t*(p + 1) = c;\n>> +\n>> +\t\t\tswitch (cmd->type) {\n>> +\t\t\tcase NAME_REV:\n>> +\t\t\t\tif (!oid_ret)\n>> +\t\t\t\t\to = lookup_object(the_repository, &oid);\n>>   \t\t\t\tif (o)\n>>   \t\t\t\t\tname = get_rev_name(o, &buf);\n>> +\t\t\t\t*(p + 1) = c;\n>> +\t\t\t\tif (!name)\n>> +\t\t\t\t\tgoto start;\n>\n> The pre-image uses \"continue\" which will increment p - why the change in\n> behavior?\n\nThey looked the same to me. So I will need to think about this some\nmore. Just a lack of C experience on my part.\n\nReplacing the `continue` with a goto at the start of the loop was also\nunnecessary. Of course the `continue` breaks out of the loop and not the\nswitch-block (unlike `break`).\n\nBut I didn’t break `t6120-describe.sh`. So I’ll also take a look to see\nif there are any holes.\n\nThanks again.\n\n>[snip]\n\n-- \nHappy May Day\n"},{"id":"542557","messageId":"20260501182718.27853-2-kristofferhaugsbakk@fastmail.com","threadId":"65237","inReplyTo":"e8bb57a2-8eff-4f72-96ca-72d8880901e0@gmail.com","subject":"Re: [PATCH v3 5/5] format-rev: introduce builtin for on-demand pretty formatting","fromName":"","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-05-01T18:27:16Z","receivedAt":"2026-05-01T18:27:44Z","isPatch":true,"body":"On Fri, May 1, 2026, at 12:16, Phillip Wood wrote:\n>>[snip]\n>> • You can’t feed commits piecemeal to these commands, one input\n>>    for one output; they block until standard in is closed\n>\n> So you can feed them piecemeal but you don't get any output until you\n> close stdin. That can be helpful as it means the calling process can\n> write to \"git log --stdin\" and then read the output without worrying\n> about getting deadlocked.\n\nOkay. I don’t have much experience with concurrent programming.\n\n> The Implementation below works fine if there\n> are separate processes or threads writing to and reading from \"git\n> format-rev\", but if we want a single process to be able to read from and\n> write to \"git format-rev --stdin-mode=text\" there will need to be a way\n> to delimit message boundaries so that git knows where the input message\n> ends and the caller knows where the response ends.\n\nOkay, so I guess a null-terminator mode for output.\n\n> We'll also need to be\n> careful about flushing the output at the end of a processed message.\n\nI don’t get why this takes special care. I’ll think about it.\n\n> For \"--stdin-mode=revs\" the caller cannot know how many lines the output\n> will span because formats like %(trailers) will produce a variable\n> number of lines depending on which trailers are present. It is also\n> possible for a rev name to span more than one line. The following\n> example finds the most recent commit that mentions 'cherry-pick' in the\n> subject line\n>\n> :/^[^\n> ]cherry-pick\n>\n> so we need a way to delimit the input and output records there as well.\n\nOkay, so a null-terminator mode for input as well?\n\n> I think the functionality implemented here is useful (transforming the\n> output of 'git blame' or 'git-last-modified' are convicing examples) and\n> it is probably better to do it as a command rather than adding a\n> \"--format\" option to name-rev.\n>\n>> • You can’t feed a list of possibly duplicate commits, like the output\n>>    of git-last-modified(1); they effectively deduplicate the output\n>\n> That is definitely a problem\n\nGreat, thanks.\n\n>> Beyond these two points there’s also the input massage problem: you\n>\n> s/massagge/message/?\n\nNo. I meant massaging the input so that it can be processed by whatever\ntool you have. :) In this case splitting the object name column and file\ncolumn because tools like git-log(1) can only deal with revision input.\n\nThanks for reviewing the usability design.\n\n-- \nHappy May Day\n"},{"id":"542583","messageId":"0f57e309-62a0-438e-a1d8-7c367379ef01@gmail.com","threadId":"65237","inReplyTo":"20260501182718.27853-2-kristofferhaugsbakk@fastmail.com","subject":"Re: [PATCH v3 5/5] format-rev: introduce builtin for on-demand pretty formatting","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-05-02T10:00:02Z","receivedAt":"2026-05-02T10:00:06Z","isPatch":true,"body":"Hi Kristoffer\n\nOn 01/05/2026 19:27, kristofferhaugsbakk@fastmail.com wrote:\n> On Fri, May 1, 2026, at 12:16, Phillip Wood wrote:\n>>> [snip]\n>>> • You can’t feed commits piecemeal to these commands, one input\n>>>     for one output; they block until standard in is closed\n>>\n>> So you can feed them piecemeal but you don't get any output until you\n>> close stdin. That can be helpful as it means the calling process can\n>> write to \"git log --stdin\" and then read the output without worrying\n>> about getting deadlocked.\n> \n> Okay. I don’t have much experience with concurrent programming.\n> \n>> The Implementation below works fine if there\n>> are separate processes or threads writing to and reading from \"git\n>> format-rev\", but if we want a single process to be able to read from and\n>> write to \"git format-rev --stdin-mode=text\" there will need to be a way\n>> to delimit message boundaries so that git knows where the input message\n>> ends and the caller knows where the response ends.\n> \n> Okay, so I guess a null-terminator mode for output.\n\nI think that's a good idea\n\n>> We'll also need to be\n>> careful about flushing the output at the end of a processed message.\n> \n> I don’t get why this takes special care. I’ll think about it.\n\nBecause the output from printf() is buffered, unless you explicitly \nflush it you can get into a state where git thinks it has printed the \noutput and is waiting for the caller to write more input, but the caller \nis still waiting to read git's output and so they are deadlocked. \nCalling maybe_flush_or_die() is the usual way to handle this I think - \nsee 344a107b55 (merge-tree --stdin: flush stdout to avoid deadlock, \n2025-02-18)\n\n>> For \"--stdin-mode=revs\" the caller cannot know how many lines the output\n>> will span because formats like %(trailers) will produce a variable\n>> number of lines depending on which trailers are present. It is also\n>> possible for a rev name to span more than one line. The following\n>> example finds the most recent commit that mentions 'cherry-pick' in the\n>> subject line\n>>\n>> :/^[^\n>> ]cherry-pick\n>>\n>> so we need a way to delimit the input and output records there as well.\n> \n> Okay, so a null-terminator mode for input as well?\n\nYes I think \"-z\" should mean NUL terminated input and output.\n\n>> I think the functionality implemented here is useful (transforming the\n>> output of 'git blame' or 'git-last-modified' are convicing examples) and\n>> it is probably better to do it as a command rather than adding a\n>> \"--format\" option to name-rev.\n>>\n>>> • You can’t feed a list of possibly duplicate commits, like the output\n>>>     of git-last-modified(1); they effectively deduplicate the output\n>>\n>> That is definitely a problem\n> \n> Great, thanks.\n> \n>>> Beyond these two points there’s also the input massage problem: you\n>>\n>> s/massagge/message/?\n> \n> No. I meant massaging the input so that it can be processed by whatever\n> tool you have. :) In this case splitting the object name column and file\n> column because tools like git-log(1) can only deal with revision input.\n\nOh I see\n\nThanks\n\nPhillip\n\n> Thanks for reviewing the usability design.\n> \n\n"},{"id":"542584","messageId":"65e013cd-5bca-4340-8018-bcbb44371e4f@gmail.com","threadId":"65237","inReplyTo":"20260501172450.25037-2-kristofferhaugsbakk@fastmail.com","subject":"Re: [PATCH v3 3/5] name-rev: factor code for sharing with a new command","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-05-02T10:00:17Z","receivedAt":"2026-05-02T10:00:20Z","isPatch":true,"body":"\n\nOn 01/05/2026 18:24, kristofferhaugsbakk@fastmail.com wrote:\n> Hi Phillip. Thanks for taking a look.\n> \n> On Thu, Apr 30, 2026, at 15:54, Phillip Wood wrote:\n>>> [snip]\n>>> @@ -524,25 +543,32 @@ static void name_rev_line(char *p, struct name_ref_data *data)\n>>>    \t\t\tconst char *name = NULL;\n>>>    \t\t\tchar c = *(p + 1);\n>>>    \t\t\tint p_len = p - p_start + 1;\n>>> +\t\t\tstruct object *o = NULL;\n>>> +\t\t\tint oid_ret = 1;\n>>>\n>>>    \t\t\tcounter = 0;\n>>>\n>>>    \t\t\t*(p + 1) = 0;\n>>> -\t\t\tif (!repo_get_oid(the_repository, p - (hexsz - 1), &oid)) {\n>>> -\t\t\t\tstruct object *o =\n>>> -\t\t\t\t\tlookup_object(the_repository, &oid);\n>>> +\t\t\toid_ret = repo_get_oid(the_repository, p - (hexsz - 1), &oid);\n>>\n>> It would be safer to restore *(p + 1) here rather that relying on each\n>> case block to do it.\n> \n> Yeah, I didn’t want to repeat that bookkeeping but in some iteration it\n> looked necessary. But it’s good that it isn’t.\n\nIt looks like the printing code is shared between the two case blocks in \npatch 5 as well so we should move that outside them as well and just set \n\"name\" inside the switch statement.\n\n>> \t\t\t*(p + 1) = c;\n>>> +\n>>> +\t\t\tswitch (cmd->type) {\n>>> +\t\t\tcase NAME_REV:\n>>> +\t\t\t\tif (!oid_ret)\n>>> +\t\t\t\t\to = lookup_object(the_repository, &oid);\n>>>    \t\t\t\tif (o)\n>>>    \t\t\t\t\tname = get_rev_name(o, &buf);\n>>> +\t\t\t\t*(p + 1) = c;\n>>> +\t\t\t\tif (!name)\n>>> +\t\t\t\t\tgoto start;\n>>\n>> The pre-image uses \"continue\" which will increment p - why the change in\n>> behavior?\n> \n> They looked the same to me. So I will need to think about this some\n> more. Just a lack of C experience on my part.\n> \n> Replacing the `continue` with a goto at the start of the loop was also\n> unnecessary. Of course the `continue` breaks out of the loop and not the\n> switch-block (unlike `break`).\n> \n> But I didn’t break `t6120-describe.sh`. So I’ll also take a look to see\n> if there are any holes.\n\nI think the difference only matters in pathological cases as \"goto \nstart\" means we end up looking at the same character twice but the loop \ncarries on as normal after that. We should just keep using \"continue\", \nI'm not sure we need a new test case.\n\nThanks\n\nPhillip\n\n> \n> Thanks again.\n> \n>> [snip]\n> \n\n"},{"id":"542619","messageId":"xmqqwlxks3l8.fsf@gitster.g","threadId":"65237","inReplyTo":"V3_format-rev_new_builtin.66f@msgid.xyz","subject":"Re: [PATCH v3 5/5] format-rev: introduce builtin for on-demand pretty formatting","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-05-03T19:19:47Z","receivedAt":"2026-05-03T19:19:49Z","isPatch":true,"body":"builtin/name-rev.c:883:8: error: variable 'commit' is used uninitialized whenever 'if' condition is false [-Werror,-Wsometimes-uninitialized]\n  883 |                         if (peeled && peeled->type == OBJ_COMMIT)\n      |                             ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nbuiltin/name-rev.c:885:9: note: uninitialized use occurs here\n  885 |                         if (!commit) {\n      |                              ^~~~~~\nbuiltin/name-rev.c:883:4: note: remove the 'if' if its condition is always true\n  883 |                         if (peeled && peeled->type == OBJ_COMMIT)\n      |                         ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n  884 |                                 commit = (struct commit *)peeled;\nbuiltin/name-rev.c:883:8: error: variable 'commit' is used uninitialized whenever '&&' condition is false [-Werror,-Wsometimes-uninitialized]\n  883 |                         if (peeled && peeled->type == OBJ_COMMIT)\n      |                             ^~~~~~\nbuiltin/name-rev.c:885:9: note: uninitialized use occurs here\n  885 |                         if (!commit) {\n      |                              ^~~~~~\nbuiltin/name-rev.c:883:8: note: remove the '&&' if its condition is always true\n  883 |                         if (peeled && peeled->type == OBJ_COMMIT)\n      |                             ^~~~~~~~~\nbuiltin/name-rev.c:867:25: note: initialize the variable 'commit' to silence this warning\n  867 |                         struct commit *commit;\n      |                                              ^\n      |                                               = NULL\n2 errors generated.\n \n"},{"id":"542769","messageId":"a0712ecf-5daf-4932-acde-1a4983d2d56c@app.fastmail.com","threadId":"65237","inReplyTo":"65e013cd-5bca-4340-8018-bcbb44371e4f@gmail.com","subject":"Re: [PATCH v3 3/5] name-rev: factor code for sharing with a new command","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-05-05T19:21:11Z","receivedAt":"2026-05-05T19:21:33Z","isPatch":true,"body":"On Sat, May 2, 2026, at 12:00, Phillip Wood wrote:\n>>>[snip]\n>>\n>> Yeah, I didn’t want to repeat that bookkeeping but in some iteration it\n>> looked necessary. But it’s good that it isn’t.\n>\n> It looks like the printing code is shared between the two case blocks in\n> patch 5 as well so we should move that outside them as well and just set\n> \"name\" inside the switch statement.\n\nThey’re not shared. Both commands work the same: either the object name\nis consumed and replaced or it is written out as-is, in other words when\ncommit lookup fails. But somehow the name-rev path prints that *failure\nto look up* case before continuing here (I don’t know how):\n\n    if (!name)\n        continue;\n\nBecause the printf(3) only prints when a symbolic name was found. Either\nname-only:\n\n    <symbolic name>\n\nOr not name-only:\n\n    <object name> (<symbolic name>)\n\nOn the other hand format-rev uses those two print statements to output\neither the name lookup case or the lookup failure case.\n\n>>>[snip]\n>>\n>> They looked the same to me. So I will need to think about this some\n>> more. Just a lack of C experience on my part.\n>>\n>> Replacing the `continue` with a goto at the start of the loop was also\n>> unnecessary. Of course the `continue` breaks out of the loop and not the\n>> switch-block (unlike `break`).\n>>\n>> But I didn’t break `t6120-describe.sh`. So I’ll also take a look to see\n>> if there are any holes.\n>\n> I think the difference only matters in pathological cases as \"goto\n> start\" means we end up looking at the same character twice but the loop\n> carries on as normal after that. We should just keep using \"continue\",\n> I'm not sure we need a new test case.\n\nYeah, I have gone back to the sensible `continue`. Thanks.\n\n(To be honest I tried to provoke a parsing bug here but I was unable\nto. Somewhat annoying.)\n"},{"id":"542778","messageId":"ae72e273-c6b0-4946-aedc-84b2e30aca87@app.fastmail.com","threadId":"65237","inReplyTo":"0f57e309-62a0-438e-a1d8-7c367379ef01@gmail.com","subject":"Re: [PATCH v3 5/5] format-rev: introduce builtin for on-demand pretty formatting","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-05-05T19:27:07Z","receivedAt":"2026-05-05T19:27:29Z","isPatch":true,"body":"On Sat, May 2, 2026, at 12:00, Phillip Wood wrote:\n>>>[snip]\n>>> We'll also need to be\n>>> careful about flushing the output at the end of a processed message.\n>>\n>> I don’t get why this takes special care. I’ll think about it.\n>\n> Because the output from printf() is buffered, unless you explicitly\n> flush it you can get into a state where git thinks it has printed the\n> output and is waiting for the caller to write more input, but the caller\n> is still waiting to read git's output and so they are deadlocked.\n> Calling maybe_flush_or_die() is the usual way to handle this I think -\n> see 344a107b55 (merge-tree --stdin: flush stdout to avoid deadlock,\n> 2025-02-18)\n\nAh, I understand now. Very well explained. Thanks :)\n\n>>> For \"--stdin-mode=revs\" the caller cannot know how many lines the output\n>>> will span because formats like %(trailers) will produce a variable\n>>> number of lines depending on which trailers are present. It is also\n>>> possible for a rev name to span more than one line. The following\n>>> example finds the most recent commit that mentions 'cherry-pick' in the\n>>> subject line\n>>>\n>>> :/^[^\n>>> ]cherry-pick\n>>>\n>>> so we need a way to delimit the input and output records there as well.\n>>\n>> Okay, so a null-terminator mode for input as well?\n>\n> Yes I think \"-z\" should mean NUL terminated input and output.\n\nI will use `-z` (and `--null`) to mean NUL terminated input and output\nbased on your recommendation and because I see that it is the approach\nused in other commands that I have found that have NUL termination for\nboth stdin and stdout.\n\nI also want to supply `--null-input` and `--null-output` since I think\n`--null-output` will be more generally useful.\n\n***\n\nThat these long options ended up being called `--null` instead of\n`--nul` by convention is maybe just a historical accident? Considering\none writes NUL byte/character.\n\n>[snip]\n"},{"id":"542867","messageId":"V4_CV_format-rev.6aa@msgid.xyz","threadId":"65237","inReplyTo":"V3_CV_format-rev.66a@msgid.xyz","subject":"[PATCH v4 0/5] format-rev: introduce builtin for on-demand pretty formatting","fromName":"","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-05-07T19:34:19Z","receivedAt":"2026-05-07T19:34:39Z","isPatch":true,"body":"From: Kristoffer Haugsbakk <code@khaugsbakk.name>\n\n(Subject from v2: name-rev: learn --format=<pretty>)\n\nTopic name (applied): kh/name-rev-custom-format\n\nTopic summary: Introduce a new builtin for pretty formatting either (1) one\nrevision expression per line or (2) commit object names found in running\ntext.\n\nSee the last patch for the motivation. In short there isn’t anything that I\nhave found that lets you format however many commits you want through one\nprocess (so looping over `git show` is excepted).\n\nThe other patches prepare for this change.\n\n§ Changes in v4\n\nMain changes are all in the final patch, with details on the Note there:\n\n• Fix `-Werror=maybe-uninitialized` error\n• Add NUL terminator options\n• Use `maybe_flush_or_die(...)` to avoid potential deadlocks.\n\nAlso changes to patch 3/5 (see details on the Note).\n\nThere is also a lot of discussion about the input and output format and how\nit works. I don’t know if it is valuable or just noise. Like this part:\n\n    Regarding input in this mode: using `-z` or `--null-input` makes sure\n    that _NUL_ characters in the input are passed through correctly.\n\nThis is just saying that NUL in the input will cut the processing short\nunless `--null-input` is given (because we loop over a NUL-terminated C\nstring in `name_rev_line(...)`).\n\n§ CI/CB\n\nThis time I ran GitHub actions (in my private fork) and let it\nfinish *before* sending this out.[1]\n\nA lot of failures there but I did not see any errors relating to my\nchanges. I could just see `git-compat-util.h` errors and C11 `Generic`.\n\n† 1: https://lore.kernel.org/git/374661c1-4676-4538-af24-0564f38469ca@app.fastmail.com/\n\n§ Why experimental command? (unchanged in v4)\n\nThis command is marked Experimental three times (like git-replay(1)).[2]\nNot so much because the UI design seems difficult. It’s more so that it can\nbe thrown out if it doesn’t end up being worth having around.\n\n(*Experimental* also implies wholesale trashing. Right? That’s one possible\nchange in behavior.)\n\nOr the behavior here could be moved somewhere else.\n\n† 2: I vaguely recall reading a good argument for emphasizing this on the\n     mailing list. I wasn’t able to find back to it right now.\n\n§ Outstanding work\n\nI could look more into how to do the translation strings for “X is\nrequire” (arg), I’m probably missing something that already exists.\n\n§ Link to v3\n\nhttps://lore.kernel.org/git/V3_format-rev_new_builtin.66f@msgid.xyz/\n\n[1/5] name-rev: wrap both blocks in braces\n[2/5] name-rev: run clang-format before factoring code\n[3/5] name-rev: factor code for sharing with a new command\n[4/5] name-rev: make dedicated --annotate-stdin --name-only test\n[5/5] format-rev: introduce builtin for on-demand pretty formatting\n\n .gitignore                        |   1 +\n Documentation/git-format-rev.adoc | 215 +++++++++++++++++++++\n Documentation/meson.build         |   1 +\n Makefile                          |   1 +\n builtin.h                         |   1 +\n builtin/name-rev.c                | 300 +++++++++++++++++++++++++++---\n command-list.txt                  |   1 +\n git.c                             |   1 +\n t/t1517-outside-repo.sh           |   3 +-\n t/t6120-describe.sh               | 208 +++++++++++++++++++++\n 10 files changed, 706 insertions(+), 26 deletions(-)\n create mode 100644 Documentation/git-format-rev.adoc\n\nInterdiff against v3:\ndiff --git a/.gitignore b/.gitignore\nindex 24635cf2d6f..e406d3741cd 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -71,6 +71,7 @@\n /git-for-each-ref\n /git-for-each-repo\n /git-format-patch\n+/git-format-rev\n /git-fsck\n /git-fsck-objects\n /git-fsmonitor--daemon\ndiff --git a/Documentation/git-format-rev.adoc b/Documentation/git-format-rev.adoc\nindex d960001d750..436980012bc 100644\n--- a/Documentation/git-format-rev.adoc\n+++ b/Documentation/git-format-rev.adoc\n@@ -9,7 +9,7 @@ git-format-rev - EXPERIMENTAL: Pretty format revisions on demand\n SYNOPSIS\n --------\n [synopsis]\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]\n \n DESCRIPTION\n -----------\n@@ -25,7 +25,8 @@ OPTIONS\n \tHow to interpret standard input data:\n +\n --\n-`revs`:: Each line is interpreted as a commit. Any kind of revision\n+`revs`:: Each line or record (see the <<io,INPUT AND OUTPUT FORMATS>>\n+\tsection) is interpreted as a commit. Any kind of revision\n \texpression can be used (see linkgit:gitrevisions[7]). Annotated\n \ttags are peeled (see linkgit:gitglossary[7]).\n +\n@@ -33,6 +34,9 @@ The argument `rev` is also accepted.\n `text`:: Formats all commit object names found in freeform text. These\n \tmust the full object names, i.e. abbreviated hexidecimal object\n \tnames will not be interpreted.\n++\n+Anything that is parsed as an object name but that is not found to be a\n+commit object name is left alone (echoed).\n --\n \n `--format=<pretty>`::\n@@ -43,6 +47,63 @@ The argument `rev` is also accepted.\n \tCustom notes ref. Notes are displayed when using the `%N`\n \tatom. See linkgit:git-notes[1].\n \n+`-z`::\n+`--null`::\n+\tUse _NUL_ character to terminate both input and output instead\n+\tof newline. This option cannot be negated.\n++\n+This is useful if both the input and output could contain newlines or if\n+the input could contain _NUL_ characters; see the <<io,INPUT AND OUTPUT\n+FORMATS>> section.\n+\n+`--null-output`::\n+`--no-null-output`::\n+\tUse _NUL_ character to terminate output instead of newline. The\n+\tdefault is `--no-null-output`.\n++\n+This is useful if the output could contain newlines, for example if the\n+`%n` (newline) atom is used.\n+\n+`--null-input`::\n+`--no-null-input`::\n+\tUse _NUL_ character to terminate input instead of newline. The\n+\tdefault is `--no-null-input`.\n++\n+This is useful if the input revision expressions could contain newlines.\n+It is also useful if the input could contain _NUL_ characters; see the\n+<<io,INPUT AND OUTPUT FORMATS>> section.\n+\n+[[io]]\n+INPUT AND OUTPUT FORMAT\n+-----------------------\n+\n+The command uses newlines for both input and output termination by\n+default. See the `-z`, `--null-output`, and `--null-input` options for\n+using _NUL_ character as the terminator.\n+\n+The mode `--stdin-mode=revs` outputs one formatted commit followed by\n+the terminator. This could either be called a _line_ or a _record_ in\n+case \"line\" is too suggestive of newline termination.\n+\n+Note that this means that the terminator character (newline or _NUL_)\n+acts as a _terminator_, not a _separator_. In other words, the final\n+line or record is also terminated by the terminator character.\n+\n+The mode `--stdin-mode=text` replaces each object name with the\n+formatted commit, i.e. the format `%s` would transform the object name\n+`abcdef012...` to `<subject>` without any termination. Like this:\n+\n+----\n+Did we not fix this in \"<subject>\"?\n+----\n+\n+Regarding input in this mode: using `-z` or `--null-input` makes sure\n+that _NUL_ characters in the input are passed through correctly.\n+\n+It is safe to interactively read and write from this command since each\n+record is immediately flushed.\n+\n+[[examples]]\n EXAMPLES\n --------\n \n@@ -110,7 +171,8 @@ DISCUSSION\n \n This command lets you format any number of revisions in any order\n through one command invocation. Consider the\n-linkgit:git-last-modified[1] case from the \"EXAMPLES\" section above:\n+linkgit:git-last-modified[1] case from the <<examples,EXAMPLES>> section\n+above:\n \n 1. There might be hundreds of files\n 2. Commits can be repeated, i.e. two or more files were last modified in\n@@ -123,7 +185,7 @@ not a good fit for the above use case.\n - The output of linkgit:git-last-modified[1] would have to be processed\n   in stages since you need to transform the first column separately and\n   then link the author to the filename. But this is surmountable.\n-- You can feed each commit to `git show` or `git show --no-walk -1`. But\n+- You can feed each commit to `git show` or `git log --no-walk -1`. But\n   that means that you need to create a process for each line.\n - Let’s say that you want to use one process, not one per line. So you\n   want to feed all the commits to the command. Now you face the problem\n@@ -143,6 +205,11 @@ process per line. It is much more work if you just want to use one\n process, but still doable. In contrast, this problem is just another\n shell pipeline with this command.\n \n+SEE ALSO\n+--------\n+linkgit:git-name-rev[1],\n+linkgit:git-log[1].\n+\n GIT\n ---\n Part of the linkgit:git[1] suite\ndiff --git a/Documentation/meson.build b/Documentation/meson.build\nindex d6365b888bb..58e7c6a0b8a 100644\n--- a/Documentation/meson.build\n+++ b/Documentation/meson.build\n@@ -55,6 +55,7 @@ manpages = {\n   'git-for-each-ref.adoc' : 1,\n   'git-for-each-repo.adoc' : 1,\n   'git-format-patch.adoc' : 1,\n+  'git-format-rev.adoc' : 1,\n   'git-fsck-objects.adoc' : 1,\n   'git-fsck.adoc' : 1,\n   'git-fsmonitor--daemon.adoc' : 1,\ndiff --git a/builtin/name-rev.c b/builtin/name-rev.c\nindex b60cc766279..5494b0424b3 100644\n--- a/builtin/name-rev.c\n+++ b/builtin/name-rev.c\n@@ -21,6 +21,7 @@\n #include \"pretty.h\"\n #include \"revision.h\"\n #include \"notes.h\"\n+#include \"write-or-die.h\"\n \n /*\n  * One day.  See the 'name a rev shortly after epoch' test in t6120 when\n@@ -577,7 +578,6 @@ static void name_rev_line(char *p, struct command *cmd)\n \n \tfor (p_start = p; *p; p++) {\n #define ishex(x) (isdigit((x)) || ((x) >= 'a' && (x) <= 'f'))\n-\tstart:\n \t\tif (!ishex(*p)) {\n \t\t\tcounter = 0;\n \t\t} else if (++counter == hexsz &&\n@@ -593,6 +593,7 @@ static void name_rev_line(char *p, struct command *cmd)\n \n \t\t\t*(p + 1) = 0;\n \t\t\toid_ret = repo_get_oid(the_repository, p - (hexsz - 1), &oid);\n+\t\t\t*(p + 1) = c;\n \n \t\t\tswitch (cmd->type) {\n \t\t\tcase NAME_REV:\n@@ -600,9 +601,8 @@ static void name_rev_line(char *p, struct command *cmd)\n \t\t\t\t\to = lookup_object(the_repository, &oid);\n \t\t\t\tif (o)\n \t\t\t\t\tname = get_rev_name(o, &buf);\n-\t\t\t\t*(p + 1) = c;\n \t\t\t\tif (!name)\n-\t\t\t\t\tgoto start;\n+\t\t\t\t\tcontinue;\n \t\t\t\tif (cmd->u.name_only)\n \t\t\t\t\tprintf(\"%.*s%s\", p_len - hexsz, p_start, name);\n \t\t\t\telse\n@@ -615,7 +615,6 @@ static void name_rev_line(char *p, struct command *cmd)\n \t\t\t\t\tname = get_format_rev((const struct commit *)o,\n \t\t\t\t\t\t\t      cmd->u.pretty_format,\n \t\t\t\t\t\t\t      &buf);\n-\t\t\t\t*(p + 1) = c;\n \t\t\t\tif (name)\n \t\t\t\t\tprintf(\"%.*s%s\", p_len - hexsz, p_start, name);\n \t\t\t\telse\n@@ -775,6 +774,23 @@ int cmd_name_rev(int argc,\n \treturn 0;\n }\n \n+struct format_nul_data {\n+\tbool nul_input;\n+\tbool nul_output;\n+};\n+\n+static int format_nul_cb(const struct option *option,\n+\t\t\t const char *arg,\n+\t\t\t int unset)\n+{\n+\tstruct format_nul_data *data = option->value;\n+\tdata->nul_input = 1;\n+\tdata->nul_output = 1;\n+\tBUG_ON_OPT_NEG(unset);\n+\tBUG_ON_OPT_ARG(arg);\n+\treturn 0;\n+}\n+\n static enum stdin_mode parse_stdin_mode(const char *stdin_mode)\n {\n \tif (!strcmp(stdin_mode, \"text\"))\n@@ -788,7 +804,9 @@ static enum stdin_mode parse_stdin_mode(const char *stdin_mode)\n }\n \n static char const *const format_rev_usage[] = {\n-\tN_(\"(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> --format=<pretty> [--notes=<ref>]\"),\n+\tN_(\"(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> \"\n+\t   \"--format=<pretty> [--[no-]notes=<ref>] \"\n+\t   \"[-z] [--[no-]null-output] [--[no-]null-input]\"),\n \tNULL\n };\n \n@@ -800,6 +818,9 @@ int cmd_format_rev(int argc,\n \tconst char *format = NULL;\n \tenum stdin_mode stdin_mode;\n \tconst char *stdin_mode_arg = NULL;\n+\tstruct format_nul_data nul_data = { 0, 0 };\n+\tchar output_terminator;\n+\tstrbuf_getline_fn getline_fn;\n \tstruct display_notes_opt format_notes_opt;\n \tstruct rev_info format_rev = REV_INFO_INIT;\n \tstruct pretty_format format_pp = { 0 };\n@@ -813,6 +834,13 @@ int cmd_format_rev(int argc,\n \t\t\t   N_(\"how revs are processed\")),\n \t\tOPT_STRING_LIST(0, \"notes\", &notes, N_(\"notes\"),\n \t\t\t\tN_(\"display notes for pretty format\")),\n+\t\tOPT_CALLBACK_F('z', \"null\", &nul_data, N_(\"z\"),\n+\t\t\t       N_(\"Use NUL for input and output termination\"),\n+\t\t\t       PARSE_OPT_NOARG | PARSE_OPT_NONEG, format_nul_cb),\n+\t\tOPT_BOOL(0, \"null-input\", &nul_data.nul_input,\n+\t\t\t N_(\"Use NUL for input termination\")),\n+\t\tOPT_BOOL(0, \"null-output\", &nul_data.nul_output,\n+\t\t\t N_(\"Use NUL for output termination\")),\n \t\tOPT_END(),\n \t};\n \n@@ -828,6 +856,9 @@ int cmd_format_rev(int argc,\n \tif (!stdin_mode_arg)\n \t\tdie(_(\"'%s' is required\"), \"--stdin-mode\");\n \n+\tgetline_fn = nul_data.nul_input ? strbuf_getline_nul : strbuf_getline_lf;\n+\toutput_terminator = nul_data.nul_output ? '\\0' : '\\n';\n+\n \tinit_display_notes(&format_notes_opt);\n \tstdin_mode = parse_stdin_mode(stdin_mode_arg);\n \n@@ -856,20 +887,24 @@ int cmd_format_rev(int argc,\n \n \tswitch (stdin_mode) {\n \tcase TEXT:\n-\t\twhile (strbuf_getline(&scratch_buf, stdin) != EOF) {\n-\t\t\tstrbuf_addch(&scratch_buf, '\\n');\n+\t\twhile (getline_fn(&scratch_buf, stdin) != EOF) {\n \t\t\tname_rev_line(scratch_buf.buf, &cmd);\n+\t\t\t/*\n+\t\t\t * We do not pass on the terminator to name_rev_line,\n+\t\t\t * unlike name-rev.\n+\t\t\t */\n+\t\t\tprintf(\"%c\", output_terminator);\n+\t\t\tmaybe_flush_or_die(stdout, \"stdout\");\n \t\t}\n \t\tbreak;\n \tcase REVS:\n-\t\twhile (strbuf_getline(&scratch_buf, stdin) != EOF) {\n+\t\twhile (getline_fn(&scratch_buf, stdin) != EOF) {\n \t\t\tstruct object_id oid;\n \t\t\tstruct object *object;\n \t\t\tstruct object *peeled;\n-\t\t\tstruct commit *commit;\n \n \t\t\tif (repo_get_oid(the_repository, scratch_buf.buf, &oid)) {\n-\t\t\t\tfprintf(stderr, \"Could not get sha1 for %s. Skipping.\\n\",\n+\t\t\t\tfprintf(stderr, \"Could not get object name for %s. Skipping.\\n\",\n \t\t\t\t\tscratch_buf.buf);\n \t\t\t\tcontinue;\n \t\t\t}\n@@ -882,16 +917,17 @@ int cmd_format_rev(int argc,\n \t\t\t}\n \n \t\t\tpeeled = deref_tag(the_repository, object, scratch_buf.buf, 0);\n-\t\t\tif (peeled && peeled->type == OBJ_COMMIT)\n-\t\t\t\tcommit = (struct commit *)peeled;\n-\t\t\tif (!commit) {\n-\t\t\t\tfprintf(stderr, \"Could not get commit for %s. Skipping.\\n\",\n-\t\t\t\t\t*argv);\n+\t\t\tif (!peeled || peeled->type != OBJ_COMMIT) {\n+\t\t\t\tfprintf(stderr,\n+\t\t\t\t\t\"Could not get commit for %s. Skipping.\\n\",\n+\t\t\t\t\tscratch_buf.buf);\n \t\t\t\tcontinue;\n \t\t\t}\n \n-\t\t\tget_format_rev(commit, &format_pp, &scratch_buf);\n-\t\t\tprintf(\"%s\\n\", scratch_buf.buf);\n+\t\t\tget_format_rev((struct commit *)peeled,\n+\t\t\t\t       &format_pp, &scratch_buf);\n+\t\t\tprintf(\"%s%c\", scratch_buf.buf, output_terminator);\n+\t\t\tmaybe_flush_or_die(stdout, \"stdout\");\n \t\t\tstrbuf_release(&scratch_buf);\n \t\t}\n \t\tbreak;\ndiff --git a/t/t6120-describe.sh b/t/t6120-describe.sh\nindex 725f7d81b6b..8ee3d2c37d0 100755\n--- a/t/t6120-describe.sh\n+++ b/t/t6120-describe.sh\n@@ -801,7 +801,7 @@ test_expect_success 'do not be fooled by invalid describe format ' '\n \ttest_must_fail git cat-file -t \"refs/tags/super-invalid/./../...../ ~^:/?*[////\\\\\\\\\\\\&}/busted.lock-42-g\"$(cat out)\n '\n \n-test_expect_success 'name-rev --format setup' '\n+test_expect_success 'setup: format-rev' '\n \tmkdir repo-format &&\n \tgit -C repo-format init &&\n \ttest_commit -C repo-format first &&\n@@ -839,8 +839,8 @@ test_expect_success 'format-rev --stdin-mode=text from rev-list same as log' '\n '\n \n test_expect_success 'format-rev --stdin-mode=text with running text and tree oid' '\n-\tcmit_oid=$(git -C repo-format rev-parse :/fifth) &&\n-\treference=$(git -C repo-format log -n1 --format=reference :/fifth) &&\n+\tcmit_oid=$(git -C repo-format rev-parse fifth) &&\n+\treference=$(git -C repo-format log -n1 --format=reference fifth) &&\n \ttree=$(git -C repo-format rev-parse HEAD^{tree}) &&\n \tcat >expect <<-EOF &&\n \tWe thought we fixed this in ${reference}.\n@@ -899,10 +899,100 @@ test_expect_success 'format-rev --stdin-mode=revs on annotated tag peels to comm\n \tEOF\n \tgit -C repo-format format-rev --stdin-mode=revs \\\n \t\t--format=%s \\\n-\t\t--notes=* >actual <<-\\EOF &&\n+\t\t>actual <<-\\EOF &&\n \tversion\n \tEOF\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'format-rev --stdin-mode=revs lookup failures' '\n+\ttest_when_finished \"git -C repo-format tag -d tag-to-tree\" &&\n+\tinvalid_syntax=not-valid &&\n+\tnon_existing_oid=${EMPTY_BLOB} &&\n+\ttree=$(git -C repo-format rev-parse eighth^{tree}) &&\n+\tgit -C repo-format tag -a -mmessage tag-to-tree \"$tree\" &&\n+\ttag_to_tree=$(git -C repo-format rev-parse tag-to-tree) &&\n+\tcat >expect <<-EOF &&\n+\tCould not get object name for ${invalid_syntax}. Skipping.\n+\tCould not get object for ${non_existing_oid}. Skipping.\n+\tCould not get commit for ${tree}. Skipping.\n+\tCould not get commit for ${tag_to_tree}. Skipping.\n+\tEOF\n+\tgit -C repo-format format-rev --stdin-mode=revs \\\n+\t\t--format=%s \\\n+\t\t2>actual >out <<-EOF &&\n+\t${invalid_syntax}\n+\t${non_existing_oid}\n+\t${tree}\n+\t${tag_to_tree}\n+\tEOF\n+\ttest_line_count = 0 out &&\n+\ttest_cmp expect actual\n+'\n+\n+\n+test_expect_success 'format-rev -z --stdin-mode=text with object name lookup failures' '\n+\tprintf \"%s\\0\" \"$(git -C repo-format rev-parse HEAD)\" >input &&\n+\tprintf \"%s\\0\" \"$(git -C repo-format rev-parse HEAD^{tree})\" >>input &&\n+\tprintf \"%s\\0\" \"$EMPTY_BLOB\" >>input &&\n+\tprintf \"%s\\0\" \"$(git -C repo-format log --format=%s -1)\" >expect &&\n+\tprintf \"%s\\0\" \"$(git -C repo-format rev-parse HEAD^{tree})\" >>expect &&\n+\tprintf \"%s\\0\" \"$EMPTY_BLOB\" >>expect &&\n+\tgit -C repo-format format-rev --stdin-mode=text \\\n+\t\t--format=%s -z <input >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup: format-rev input and output separators' '\n+\tgit -C repo-format rev-list HEAD >input-lf &&\n+\tgit -C repo-format rev-list -z HEAD >input-nul &&\n+\tgit -C repo-format log --format=%s >output-lf &&\n+\tgit -C repo-format log -z --format=%s >output-nul &&\n+\techo revs >stdin-modes &&\n+\techo text >>stdin-modes\n+'\n+\n+while read mode\n+do\n+\ttest_expect_success \"format-rev -z --stdin-mode=$mode\" '\n+\t\tcat output-nul >expect &&\n+\t\tgit -C repo-format format-rev --stdin-mode=\"$mode\" \\\n+\t\t\t--format=%s -z <input-nul >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"format-rev -z --no-null-input --no-null-output --stdin-mode=$mode\" '\n+\t\tcat output-lf >expect &&\n+\t\tgit -C repo-format format-rev --stdin-mode=\"$mode\" \\\n+\t\t\t--format=%s -z --no-null-input --no-null-output \\\n+\t\t\t<input-lf >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"format-rev ---null-input --stdin-mode=$mode\" '\n+\t\tcat output-lf >expect &&\n+\t\tgit -C repo-format format-rev --stdin-mode=\"$mode\" \\\n+\t\t\t--format=%s --null-input \\\n+\t\t\t<input-nul >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"format-rev --null-output --stdin-mode=$mode\" '\n+\t\tcat output-nul >expect &&\n+\t\tgit -C repo-format format-rev --stdin-mode=\"$mode\" \\\n+\t\t\t--format=%s --null-output \\\n+\t\t\t<input-lf >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"format-rev -z --stdin-mode=$mode with multi-line output\" '\n+\t\tformat=\"%s%n%aI\" &&\n+\t\tgit -C repo-format log -z --format=\"$format\" \\\n+\t\t\t>expect &&\n+\t\tgit -C repo-format format-rev --stdin-mode=\"$mode\" \\\n+\t\t\t--format=\"$format\" -z <input-nul >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+done <stdin-modes\n+\n test_done\nRange-diff against v3:\n1:  9cb5cfd1ec3 = 1:  9cb5cfd1ec3 name-rev: wrap both blocks in braces\n2:  14900271321 = 2:  14900271321 name-rev: run clang-format before factoring code\n3:  1f17f0b0090 ! 3:  724ec022894 name-rev: factor code for sharing with a new command\n    @@ builtin/name-rev.c: static char const * const name_rev_usage[] = {\n      \tstruct strbuf buf = STRBUF_INIT;\n      \tint counter = 0;\n     @@ builtin/name-rev.c: static void name_rev_line(char *p, struct name_ref_data *data)\n    - \n    - \tfor (p_start = p; *p; p++) {\n    - #define ishex(x) (isdigit((x)) || ((x) >= 'a' && (x) <= 'f'))\n    -+\tstart:\n    - \t\tif (!ishex(*p)) {\n    - \t\t\tcounter = 0;\n    - \t\t} else if (++counter == hexsz &&\n    -@@ builtin/name-rev.c: static void name_rev_line(char *p, struct name_ref_data *data)\n      \t\t\tconst char *name = NULL;\n      \t\t\tchar c = *(p + 1);\n      \t\t\tint p_len = p - p_start + 1;\n    @@ builtin/name-rev.c: static void name_rev_line(char *p, struct name_ref_data *dat\n     -\t\t\t\tstruct object *o =\n     -\t\t\t\t\tlookup_object(the_repository, &oid);\n     +\t\t\toid_ret = repo_get_oid(the_repository, p - (hexsz - 1), &oid);\n    ++\t\t\t*(p + 1) = c;\n     +\n     +\t\t\tswitch (cmd->type) {\n     +\t\t\tcase NAME_REV:\n    @@ builtin/name-rev.c: static void name_rev_line(char *p, struct name_ref_data *dat\n     +\t\t\t\t\to = lookup_object(the_repository, &oid);\n      \t\t\t\tif (o)\n      \t\t\t\t\tname = get_rev_name(o, &buf);\n    -+\t\t\t\t*(p + 1) = c;\n     +\t\t\t\tif (!name)\n    -+\t\t\t\t\tgoto start;\n    ++\t\t\t\t\tcontinue;\n     +\t\t\t\tif (cmd->u.name_only)\n     +\t\t\t\t\tprintf(\"%.*s%s\", p_len - hexsz, p_start, name);\n     +\t\t\t\telse\n4:  40989b3672a = 4:  382efc3ddb8 name-rev: make dedicated --annotate-stdin --name-only test\n5:  bb54e8f753e ! 5:  049a45e32bc format-rev: introduce builtin for on-demand pretty formatting\n    @@ Commit message\n         `t/t1517-outside-repo.sh` because it uses “EXPERIMENTAL!”\n         in the usage line.\n     \n    +    Helped-by: Phillip Wood <phillip.wood@dunelm.org.uk>\n    +    Helped-by: Ramsay Jones <ramsay@ramsayjones.plus.com>\n    +    Helped-by: Junio C Hamano <gitster@pobox.com>\n         Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>\n     \n    + ## .gitignore ##\n    +@@\n    + /git-for-each-ref\n    + /git-for-each-repo\n    + /git-format-patch\n    ++/git-format-rev\n    + /git-fsck\n    + /git-fsck-objects\n    + /git-fsmonitor--daemon\n    +\n      ## Documentation/git-format-rev.adoc (new) ##\n     @@\n     +git-format-rev(1)\n    @@ Documentation/git-format-rev.adoc (new)\n     +SYNOPSIS\n     +--------\n     +[synopsis]\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]\n     +\n     +DESCRIPTION\n     +-----------\n    @@ Documentation/git-format-rev.adoc (new)\n     +\tHow to interpret standard input data:\n     ++\n     +--\n    -+`revs`:: Each line is interpreted as a commit. Any kind of revision\n    ++`revs`:: Each line or record (see the <<io,INPUT AND OUTPUT FORMATS>>\n    ++\tsection) is interpreted as a commit. Any kind of revision\n     +\texpression can be used (see linkgit:gitrevisions[7]). Annotated\n     +\ttags are peeled (see linkgit:gitglossary[7]).\n     ++\n    @@ Documentation/git-format-rev.adoc (new)\n     +`text`:: Formats all commit object names found in freeform text. These\n     +\tmust the full object names, i.e. abbreviated hexidecimal object\n     +\tnames will not be interpreted.\n    +++\n    ++Anything that is parsed as an object name but that is not found to be a\n    ++commit object name is left alone (echoed).\n     +--\n     +\n     +`--format=<pretty>`::\n    @@ Documentation/git-format-rev.adoc (new)\n     +\tCustom notes ref. Notes are displayed when using the `%N`\n     +\tatom. See linkgit:git-notes[1].\n     +\n    ++`-z`::\n    ++`--null`::\n    ++\tUse _NUL_ character to terminate both input and output instead\n    ++\tof newline. This option cannot be negated.\n    +++\n    ++This is useful if both the input and output could contain newlines or if\n    ++the input could contain _NUL_ characters; see the <<io,INPUT AND OUTPUT\n    ++FORMATS>> section.\n    ++\n    ++`--null-output`::\n    ++`--no-null-output`::\n    ++\tUse _NUL_ character to terminate output instead of newline. The\n    ++\tdefault is `--no-null-output`.\n    +++\n    ++This is useful if the output could contain newlines, for example if the\n    ++`%n` (newline) atom is used.\n    ++\n    ++`--null-input`::\n    ++`--no-null-input`::\n    ++\tUse _NUL_ character to terminate input instead of newline. The\n    ++\tdefault is `--no-null-input`.\n    +++\n    ++This is useful if the input revision expressions could contain newlines.\n    ++It is also useful if the input could contain _NUL_ characters; see the\n    ++<<io,INPUT AND OUTPUT FORMATS>> section.\n    ++\n    ++[[io]]\n    ++INPUT AND OUTPUT FORMAT\n    ++-----------------------\n    ++\n    ++The command uses newlines for both input and output termination by\n    ++default. See the `-z`, `--null-output`, and `--null-input` options for\n    ++using _NUL_ character as the terminator.\n    ++\n    ++The mode `--stdin-mode=revs` outputs one formatted commit followed by\n    ++the terminator. This could either be called a _line_ or a _record_ in\n    ++case \"line\" is too suggestive of newline termination.\n    ++\n    ++Note that this means that the terminator character (newline or _NUL_)\n    ++acts as a _terminator_, not a _separator_. In other words, the final\n    ++line or record is also terminated by the terminator character.\n    ++\n    ++The mode `--stdin-mode=text` replaces each object name with the\n    ++formatted commit, i.e. the format `%s` would transform the object name\n    ++`abcdef012...` to `<subject>` without any termination. Like this:\n    ++\n    ++----\n    ++Did we not fix this in \"<subject>\"?\n    ++----\n    ++\n    ++Regarding input in this mode: using `-z` or `--null-input` makes sure\n    ++that _NUL_ characters in the input are passed through correctly.\n    ++\n    ++It is safe to interactively read and write from this command since each\n    ++record is immediately flushed.\n    ++\n    ++[[examples]]\n     +EXAMPLES\n     +--------\n     +\n    @@ Documentation/git-format-rev.adoc (new)\n     +\n     +This command lets you format any number of revisions in any order\n     +through one command invocation. Consider the\n    -+linkgit:git-last-modified[1] case from the \"EXAMPLES\" section above:\n    ++linkgit:git-last-modified[1] case from the <<examples,EXAMPLES>> section\n    ++above:\n     +\n     +1. There might be hundreds of files\n     +2. Commits can be repeated, i.e. two or more files were last modified in\n    @@ Documentation/git-format-rev.adoc (new)\n     +- The output of linkgit:git-last-modified[1] would have to be processed\n     +  in stages since you need to transform the first column separately and\n     +  then link the author to the filename. But this is surmountable.\n    -+- You can feed each commit to `git show` or `git show --no-walk -1`. But\n    ++- You can feed each commit to `git show` or `git log --no-walk -1`. But\n     +  that means that you need to create a process for each line.\n     +- Let’s say that you want to use one process, not one per line. So you\n     +  want to feed all the commits to the command. Now you face the problem\n    @@ Documentation/git-format-rev.adoc (new)\n     +process, but still doable. In contrast, this problem is just another\n     +shell pipeline with this command.\n     +\n    ++SEE ALSO\n    ++--------\n    ++linkgit:git-name-rev[1],\n    ++linkgit:git-log[1].\n    ++\n     +GIT\n     +---\n     +Part of the linkgit:git[1] suite\n     \n    + ## Documentation/meson.build ##\n    +@@ Documentation/meson.build: manpages = {\n    +   'git-for-each-ref.adoc' : 1,\n    +   'git-for-each-repo.adoc' : 1,\n    +   'git-format-patch.adoc' : 1,\n    ++  'git-format-rev.adoc' : 1,\n    +   'git-fsck-objects.adoc' : 1,\n    +   'git-fsck.adoc' : 1,\n    +   'git-fsmonitor--daemon.adoc' : 1,\n    +\n      ## Makefile ##\n     @@ Makefile: BUILT_INS += $(patsubst builtin/%.o,git-%$X,$(BUILTIN_OBJS))\n      BUILT_INS += git-cherry$X\n    @@ builtin/name-rev.c\n     +#include \"pretty.h\"\n     +#include \"revision.h\"\n     +#include \"notes.h\"\n    ++#include \"write-or-die.h\"\n      \n      /*\n       * One day.  See the 'name a rev shortly after epoch' test in t6120 when\n    @@ builtin/name-rev.c: static void name_rev_line(char *p, struct command *cmd)\n     +\t\t\t\t\tname = get_format_rev((const struct commit *)o,\n     +\t\t\t\t\t\t\t      cmd->u.pretty_format,\n     +\t\t\t\t\t\t\t      &buf);\n    -+\t\t\t\t*(p + 1) = c;\n     +\t\t\t\tif (name)\n     +\t\t\t\t\tprintf(\"%.*s%s\", p_len - hexsz, p_start, name);\n     +\t\t\t\telse\n    @@ builtin/name-rev.c: int cmd_name_rev(int argc,\n      \treturn 0;\n      }\n     +\n    ++struct format_nul_data {\n    ++\tbool nul_input;\n    ++\tbool nul_output;\n    ++};\n    ++\n    ++static int format_nul_cb(const struct option *option,\n    ++\t\t\t const char *arg,\n    ++\t\t\t int unset)\n    ++{\n    ++\tstruct format_nul_data *data = option->value;\n    ++\tdata->nul_input = 1;\n    ++\tdata->nul_output = 1;\n    ++\tBUG_ON_OPT_NEG(unset);\n    ++\tBUG_ON_OPT_ARG(arg);\n    ++\treturn 0;\n    ++}\n    ++\n     +static enum stdin_mode parse_stdin_mode(const char *stdin_mode)\n     +{\n     +\tif (!strcmp(stdin_mode, \"text\"))\n    @@ builtin/name-rev.c: int cmd_name_rev(int argc,\n     +}\n     +\n     +static char const *const format_rev_usage[] = {\n    -+\tN_(\"(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> --format=<pretty> [--notes=<ref>]\"),\n    ++\tN_(\"(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> \"\n    ++\t   \"--format=<pretty> [--[no-]notes=<ref>] \"\n    ++\t   \"[-z] [--[no-]null-output] [--[no-]null-input]\"),\n     +\tNULL\n     +};\n     +\n    @@ builtin/name-rev.c: int cmd_name_rev(int argc,\n     +\tconst char *format = NULL;\n     +\tenum stdin_mode stdin_mode;\n     +\tconst char *stdin_mode_arg = NULL;\n    ++\tstruct format_nul_data nul_data = { 0, 0 };\n    ++\tchar output_terminator;\n    ++\tstrbuf_getline_fn getline_fn;\n     +\tstruct display_notes_opt format_notes_opt;\n     +\tstruct rev_info format_rev = REV_INFO_INIT;\n     +\tstruct pretty_format format_pp = { 0 };\n    @@ builtin/name-rev.c: int cmd_name_rev(int argc,\n     +\t\t\t   N_(\"how revs are processed\")),\n     +\t\tOPT_STRING_LIST(0, \"notes\", &notes, N_(\"notes\"),\n     +\t\t\t\tN_(\"display notes for pretty format\")),\n    ++\t\tOPT_CALLBACK_F('z', \"null\", &nul_data, N_(\"z\"),\n    ++\t\t\t       N_(\"Use NUL for input and output termination\"),\n    ++\t\t\t       PARSE_OPT_NOARG | PARSE_OPT_NONEG, format_nul_cb),\n    ++\t\tOPT_BOOL(0, \"null-input\", &nul_data.nul_input,\n    ++\t\t\t N_(\"Use NUL for input termination\")),\n    ++\t\tOPT_BOOL(0, \"null-output\", &nul_data.nul_output,\n    ++\t\t\t N_(\"Use NUL for output termination\")),\n     +\t\tOPT_END(),\n     +\t};\n     +\n    @@ builtin/name-rev.c: int cmd_name_rev(int argc,\n     +\tif (!stdin_mode_arg)\n     +\t\tdie(_(\"'%s' is required\"), \"--stdin-mode\");\n     +\n    ++\tgetline_fn = nul_data.nul_input ? strbuf_getline_nul : strbuf_getline_lf;\n    ++\toutput_terminator = nul_data.nul_output ? '\\0' : '\\n';\n    ++\n     +\tinit_display_notes(&format_notes_opt);\n     +\tstdin_mode = parse_stdin_mode(stdin_mode_arg);\n     +\n    @@ builtin/name-rev.c: int cmd_name_rev(int argc,\n     +\n     +\tswitch (stdin_mode) {\n     +\tcase TEXT:\n    -+\t\twhile (strbuf_getline(&scratch_buf, stdin) != EOF) {\n    -+\t\t\tstrbuf_addch(&scratch_buf, '\\n');\n    ++\t\twhile (getline_fn(&scratch_buf, stdin) != EOF) {\n     +\t\t\tname_rev_line(scratch_buf.buf, &cmd);\n    ++\t\t\t/*\n    ++\t\t\t * We do not pass on the terminator to name_rev_line,\n    ++\t\t\t * unlike name-rev.\n    ++\t\t\t */\n    ++\t\t\tprintf(\"%c\", output_terminator);\n    ++\t\t\tmaybe_flush_or_die(stdout, \"stdout\");\n     +\t\t}\n     +\t\tbreak;\n     +\tcase REVS:\n    -+\t\twhile (strbuf_getline(&scratch_buf, stdin) != EOF) {\n    ++\t\twhile (getline_fn(&scratch_buf, stdin) != EOF) {\n     +\t\t\tstruct object_id oid;\n     +\t\t\tstruct object *object;\n     +\t\t\tstruct object *peeled;\n    -+\t\t\tstruct commit *commit;\n     +\n     +\t\t\tif (repo_get_oid(the_repository, scratch_buf.buf, &oid)) {\n    -+\t\t\t\tfprintf(stderr, \"Could not get sha1 for %s. Skipping.\\n\",\n    ++\t\t\t\tfprintf(stderr, \"Could not get object name for %s. Skipping.\\n\",\n     +\t\t\t\t\tscratch_buf.buf);\n     +\t\t\t\tcontinue;\n     +\t\t\t}\n    @@ builtin/name-rev.c: int cmd_name_rev(int argc,\n     +\t\t\t}\n     +\n     +\t\t\tpeeled = deref_tag(the_repository, object, scratch_buf.buf, 0);\n    -+\t\t\tif (peeled && peeled->type == OBJ_COMMIT)\n    -+\t\t\t\tcommit = (struct commit *)peeled;\n    -+\t\t\tif (!commit) {\n    -+\t\t\t\tfprintf(stderr, \"Could not get commit for %s. Skipping.\\n\",\n    -+\t\t\t\t\t*argv);\n    ++\t\t\tif (!peeled || peeled->type != OBJ_COMMIT) {\n    ++\t\t\t\tfprintf(stderr,\n    ++\t\t\t\t\t\"Could not get commit for %s. Skipping.\\n\",\n    ++\t\t\t\t\tscratch_buf.buf);\n     +\t\t\t\tcontinue;\n     +\t\t\t}\n     +\n    -+\t\t\tget_format_rev(commit, &format_pp, &scratch_buf);\n    -+\t\t\tprintf(\"%s\\n\", scratch_buf.buf);\n    ++\t\t\tget_format_rev((struct commit *)peeled,\n    ++\t\t\t\t       &format_pp, &scratch_buf);\n    ++\t\t\tprintf(\"%s%c\", scratch_buf.buf, output_terminator);\n    ++\t\t\tmaybe_flush_or_die(stdout, \"stdout\");\n     +\t\t\tstrbuf_release(&scratch_buf);\n     +\t\t}\n     +\t\tbreak;\n    @@ t/t6120-describe.sh: test_expect_success 'do not be fooled by invalid describe f\n      \ttest_must_fail git cat-file -t \"refs/tags/super-invalid/./../...../ ~^:/?*[////\\\\\\\\\\\\&}/busted.lock-42-g\"$(cat out)\n      '\n      \n    -+test_expect_success 'name-rev --format setup' '\n    ++test_expect_success 'setup: format-rev' '\n     +\tmkdir repo-format &&\n     +\tgit -C repo-format init &&\n     +\ttest_commit -C repo-format first &&\n    @@ t/t6120-describe.sh: test_expect_success 'do not be fooled by invalid describe f\n     +'\n     +\n     +test_expect_success 'format-rev --stdin-mode=text with running text and tree oid' '\n    -+\tcmit_oid=$(git -C repo-format rev-parse :/fifth) &&\n    -+\treference=$(git -C repo-format log -n1 --format=reference :/fifth) &&\n    ++\tcmit_oid=$(git -C repo-format rev-parse fifth) &&\n    ++\treference=$(git -C repo-format log -n1 --format=reference fifth) &&\n     +\ttree=$(git -C repo-format rev-parse HEAD^{tree}) &&\n     +\tcat >expect <<-EOF &&\n     +\tWe thought we fixed this in ${reference}.\n    @@ t/t6120-describe.sh: test_expect_success 'do not be fooled by invalid describe f\n     +\tEOF\n     +\tgit -C repo-format format-rev --stdin-mode=revs \\\n     +\t\t--format=%s \\\n    -+\t\t--notes=* >actual <<-\\EOF &&\n    ++\t\t>actual <<-\\EOF &&\n     +\tversion\n     +\tEOF\n     +\ttest_cmp expect actual\n     +'\n    ++\n    ++test_expect_success 'format-rev --stdin-mode=revs lookup failures' '\n    ++\ttest_when_finished \"git -C repo-format tag -d tag-to-tree\" &&\n    ++\tinvalid_syntax=not-valid &&\n    ++\tnon_existing_oid=${EMPTY_BLOB} &&\n    ++\ttree=$(git -C repo-format rev-parse eighth^{tree}) &&\n    ++\tgit -C repo-format tag -a -mmessage tag-to-tree \"$tree\" &&\n    ++\ttag_to_tree=$(git -C repo-format rev-parse tag-to-tree) &&\n    ++\tcat >expect <<-EOF &&\n    ++\tCould not get object name for ${invalid_syntax}. Skipping.\n    ++\tCould not get object for ${non_existing_oid}. Skipping.\n    ++\tCould not get commit for ${tree}. Skipping.\n    ++\tCould not get commit for ${tag_to_tree}. Skipping.\n    ++\tEOF\n    ++\tgit -C repo-format format-rev --stdin-mode=revs \\\n    ++\t\t--format=%s \\\n    ++\t\t2>actual >out <<-EOF &&\n    ++\t${invalid_syntax}\n    ++\t${non_existing_oid}\n    ++\t${tree}\n    ++\t${tag_to_tree}\n    ++\tEOF\n    ++\ttest_line_count = 0 out &&\n    ++\ttest_cmp expect actual\n    ++'\n    ++\n    ++\n    ++test_expect_success 'format-rev -z --stdin-mode=text with object name lookup failures' '\n    ++\tprintf \"%s\\0\" \"$(git -C repo-format rev-parse HEAD)\" >input &&\n    ++\tprintf \"%s\\0\" \"$(git -C repo-format rev-parse HEAD^{tree})\" >>input &&\n    ++\tprintf \"%s\\0\" \"$EMPTY_BLOB\" >>input &&\n    ++\tprintf \"%s\\0\" \"$(git -C repo-format log --format=%s -1)\" >expect &&\n    ++\tprintf \"%s\\0\" \"$(git -C repo-format rev-parse HEAD^{tree})\" >>expect &&\n    ++\tprintf \"%s\\0\" \"$EMPTY_BLOB\" >>expect &&\n    ++\tgit -C repo-format format-rev --stdin-mode=text \\\n    ++\t\t--format=%s -z <input >actual &&\n    ++\ttest_cmp expect actual\n    ++'\n    ++\n    ++test_expect_success 'setup: format-rev input and output separators' '\n    ++\tgit -C repo-format rev-list HEAD >input-lf &&\n    ++\tgit -C repo-format rev-list -z HEAD >input-nul &&\n    ++\tgit -C repo-format log --format=%s >output-lf &&\n    ++\tgit -C repo-format log -z --format=%s >output-nul &&\n    ++\techo revs >stdin-modes &&\n    ++\techo text >>stdin-modes\n    ++'\n    ++\n    ++while read mode\n    ++do\n    ++\ttest_expect_success \"format-rev -z --stdin-mode=$mode\" '\n    ++\t\tcat output-nul >expect &&\n    ++\t\tgit -C repo-format format-rev --stdin-mode=\"$mode\" \\\n    ++\t\t\t--format=%s -z <input-nul >actual &&\n    ++\t\ttest_cmp expect actual\n    ++\t'\n    ++\n    ++\ttest_expect_success \"format-rev -z --no-null-input --no-null-output --stdin-mode=$mode\" '\n    ++\t\tcat output-lf >expect &&\n    ++\t\tgit -C repo-format format-rev --stdin-mode=\"$mode\" \\\n    ++\t\t\t--format=%s -z --no-null-input --no-null-output \\\n    ++\t\t\t<input-lf >actual &&\n    ++\t\ttest_cmp expect actual\n    ++\t'\n    ++\n    ++\ttest_expect_success \"format-rev ---null-input --stdin-mode=$mode\" '\n    ++\t\tcat output-lf >expect &&\n    ++\t\tgit -C repo-format format-rev --stdin-mode=\"$mode\" \\\n    ++\t\t\t--format=%s --null-input \\\n    ++\t\t\t<input-nul >actual &&\n    ++\t\ttest_cmp expect actual\n    ++\t'\n    ++\n    ++\ttest_expect_success \"format-rev --null-output --stdin-mode=$mode\" '\n    ++\t\tcat output-nul >expect &&\n    ++\t\tgit -C repo-format format-rev --stdin-mode=\"$mode\" \\\n    ++\t\t\t--format=%s --null-output \\\n    ++\t\t\t<input-lf >actual &&\n    ++\t\ttest_cmp expect actual\n    ++\t'\n    ++\n    ++\ttest_expect_success \"format-rev -z --stdin-mode=$mode with multi-line output\" '\n    ++\t\tformat=\"%s%n%aI\" &&\n    ++\t\tgit -C repo-format log -z --format=\"$format\" \\\n    ++\t\t\t>expect &&\n    ++\t\tgit -C repo-format format-rev --stdin-mode=\"$mode\" \\\n    ++\t\t\t--format=\"$format\" -z <input-nul >actual &&\n    ++\t\ttest_cmp expect actual\n    ++\t'\n    ++done <stdin-modes\n     +\n      test_done\n\nbase-commit: 67006b9db8b772423ad0706029286096307d2567\n-- \n2.54.0.13.g9c7419e39f8\n\n"},{"id":"542868","messageId":"V4_name-rev_braces.6ab@msgid.xyz","threadId":"65237","inReplyTo":"V4_CV_format-rev.6aa@msgid.xyz","subject":"[PATCH v4 1/5] name-rev: wrap both blocks in braces","fromName":"","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-05-07T19:34:20Z","receivedAt":"2026-05-07T19:34:58Z","isPatch":true,"body":"From: Kristoffer Haugsbakk <code@khaugsbakk.name>\n\nSee `CodingGuidelines`:\n\n    - When there are multiple arms to a conditional and some of them\n      require braces, enclose even a single line block in braces for\n      consistency. [...]\n\nSigned-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>\n---\n\nNotes (series):\n    v2:\n    Fix stray formatting of `(p+1)`\n\n builtin/name-rev.c | 8 ++++----\n 1 file changed, 4 insertions(+), 4 deletions(-)\n\ndiff --git a/builtin/name-rev.c b/builtin/name-rev.c\nindex 6188cf98ce0..171e7bd0e98 100644\n--- a/builtin/name-rev.c\n+++ b/builtin/name-rev.c\n@@ -466,9 +466,9 @@ static const char *get_rev_name(const struct object *o, struct strbuf *buf)\n \tif (!n)\n \t\treturn NULL;\n \n-\tif (!n->generation)\n+\tif (!n->generation) {\n \t\treturn n->tip_name;\n-\telse {\n+\t} else {\n \t\tstrbuf_reset(buf);\n \t\tstrbuf_addstr(buf, n->tip_name);\n \t\tstrbuf_strip_suffix(buf, \"^0\");\n@@ -516,9 +516,9 @@ static void name_rev_line(char *p, struct name_ref_data *data)\n \n \tfor (p_start = p; *p; p++) {\n #define ishex(x) (isdigit((x)) || ((x) >= 'a' && (x) <= 'f'))\n-\t\tif (!ishex(*p))\n+\t\tif (!ishex(*p)) {\n \t\t\tcounter = 0;\n-\t\telse if (++counter == hexsz &&\n+\t\t} else if (++counter == hexsz &&\n \t\t\t !ishex(*(p+1))) {\n \t\t\tstruct object_id oid;\n \t\t\tconst char *name = NULL;\n-- \n2.54.0.13.g9c7419e39f8\n\n"},{"id":"542869","messageId":"V4_name-rev_clang-format.6ac@msgid.xyz","threadId":"65237","inReplyTo":"V4_CV_format-rev.6aa@msgid.xyz","subject":"[PATCH v4 2/5] name-rev: run clang-format before factoring code","fromName":"","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-05-07T19:34:21Z","receivedAt":"2026-05-07T19:35:18Z","isPatch":true,"body":"From: Kristoffer Haugsbakk <code@khaugsbakk.name>\n\nWe are about to move code around to prepare for adding a new\ncommand. Let’s deal with clang-format changes first in the affected\nareas.\n\nSigned-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>\n---\n builtin/name-rev.c | 18 +++++++++---------\n 1 file changed, 9 insertions(+), 9 deletions(-)\n\ndiff --git a/builtin/name-rev.c b/builtin/name-rev.c\nindex 171e7bd0e98..6357eaa76d0 100644\n--- a/builtin/name-rev.c\n+++ b/builtin/name-rev.c\n@@ -519,22 +519,22 @@ static void name_rev_line(char *p, struct name_ref_data *data)\n \t\tif (!ishex(*p)) {\n \t\t\tcounter = 0;\n \t\t} else if (++counter == hexsz &&\n-\t\t\t !ishex(*(p+1))) {\n+\t\t\t   !ishex(*(p + 1))) {\n \t\t\tstruct object_id oid;\n \t\t\tconst char *name = NULL;\n-\t\t\tchar c = *(p+1);\n+\t\t\tchar c = *(p + 1);\n \t\t\tint p_len = p - p_start + 1;\n \n \t\t\tcounter = 0;\n \n-\t\t\t*(p+1) = 0;\n+\t\t\t*(p + 1) = 0;\n \t\t\tif (!repo_get_oid(the_repository, p - (hexsz - 1), &oid)) {\n \t\t\t\tstruct object *o =\n \t\t\t\t\tlookup_object(the_repository, &oid);\n \t\t\t\tif (o)\n \t\t\t\t\tname = get_rev_name(o, &buf);\n \t\t\t}\n-\t\t\t*(p+1) = c;\n+\t\t\t*(p + 1) = c;\n \n \t\t\tif (!name)\n \t\t\t\tcontinue;\n@@ -571,9 +571,9 @@ int cmd_name_rev(int argc,\n \t\tOPT_BOOL(0, \"name-only\", &data.name_only, N_(\"print only ref-based names (no object names)\")),\n \t\tOPT_BOOL(0, \"tags\", &data.tags_only, N_(\"only use tags to name the commits\")),\n \t\tOPT_STRING_LIST(0, \"refs\", &data.ref_filters, N_(\"pattern\"),\n-\t\t\t\t   N_(\"only use refs matching <pattern>\")),\n+\t\t\t\tN_(\"only use refs matching <pattern>\")),\n \t\tOPT_STRING_LIST(0, \"exclude\", &data.exclude_filters, N_(\"pattern\"),\n-\t\t\t\t   N_(\"ignore refs matching <pattern>\")),\n+\t\t\t\tN_(\"ignore refs matching <pattern>\")),\n \t\tOPT_GROUP(\"\"),\n \t\tOPT_BOOL(0, \"all\", &all, N_(\"list all commits reachable from all refs\")),\n #ifndef WITH_BREAKING_CHANGES\n@@ -585,10 +585,10 @@ int cmd_name_rev(int argc,\n #endif /* WITH_BREAKING_CHANGES */\n \t\tOPT_BOOL(0, \"annotate-stdin\", &annotate_stdin, N_(\"annotate text from stdin\")),\n \t\tOPT_BOOL(0, \"undefined\", &allow_undefined, N_(\"allow to print `undefined` names (default)\")),\n-\t\tOPT_BOOL(0, \"always\",     &always,\n-\t\t\t   N_(\"show abbreviated commit object as fallback\")),\n+\t\tOPT_BOOL(0, \"always\", &always,\n+\t\t\t N_(\"show abbreviated commit object as fallback\")),\n \t\tOPT_HIDDEN_BOOL(0, \"peel-tag\", &peel_tag,\n-\t\t\t   N_(\"dereference tags in the input (internal use)\")),\n+\t\t\t\tN_(\"dereference tags in the input (internal use)\")),\n \t\tOPT_END(),\n \t};\n \n-- \n2.54.0.13.g9c7419e39f8\n\n"},{"id":"542870","messageId":"V4_name-rev_factor.6ad@msgid.xyz","threadId":"65237","inReplyTo":"V4_CV_format-rev.6aa@msgid.xyz","subject":"[PATCH v4 3/5] name-rev: factor code for sharing with a new command","fromName":"","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-05-07T19:34:22Z","receivedAt":"2026-05-07T19:35:38Z","isPatch":true,"body":"From: Kristoffer Haugsbakk <code@khaugsbakk.name>\n\nWe are about to introduce a new command git-format-rev(1) to this\nfile. Let’s factor some code so that we can share it with the new\ncommand.\n\nWe want to be able to format commits found in freeform text, and\ngit-name-rev(1) already has a function for that but for symbolic\nnames. Let’s use a tagged union for the command-specific payload.\n\nNo functional changes.\n\nSigned-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>\n---\n\nNotes (series):\n    v4:\n    • Pull out `*(p + 1)` instead of doing it in every `case` (Phillip)\n    • Go back to using `continue` instead of `goto` (Phillip)\n\n builtin/name-rev.c | 53 ++++++++++++++++++++++++++++++++++------------\n 1 file changed, 40 insertions(+), 13 deletions(-)\n\ndiff --git a/builtin/name-rev.c b/builtin/name-rev.c\nindex 6357eaa76d0..475efb0b82b 100644\n--- a/builtin/name-rev.c\n+++ b/builtin/name-rev.c\n@@ -272,6 +272,24 @@ struct name_ref_data {\n \tstruct string_list exclude_filters;\n };\n \n+enum command_type {\n+\tNAME_REV = 1,\n+};\n+\n+struct command {\n+\tenum command_type type;\n+\tunion {\n+\t\tint name_only;\n+\t} u;\n+};\n+\n+static void init_name_rev_command(struct command *cmd,\n+\t\t\t\t  int name_only)\n+{\n+\tcmd->type = NAME_REV;\n+\tcmd->u.name_only = name_only;\n+}\n+\n static struct tip_table {\n \tstruct tip_table_entry {\n \t\tstruct object_id oid;\n@@ -507,7 +525,7 @@ static char const * const name_rev_usage[] = {\n \tNULL\n };\n \n-static void name_rev_line(char *p, struct name_ref_data *data)\n+static void name_rev_line(char *p, struct command *cmd)\n {\n \tstruct strbuf buf = STRBUF_INIT;\n \tint counter = 0;\n@@ -524,25 +542,32 @@ static void name_rev_line(char *p, struct name_ref_data *data)\n \t\t\tconst char *name = NULL;\n \t\t\tchar c = *(p + 1);\n \t\t\tint p_len = p - p_start + 1;\n+\t\t\tstruct object *o = NULL;\n+\t\t\tint oid_ret = 1;\n \n \t\t\tcounter = 0;\n \n \t\t\t*(p + 1) = 0;\n-\t\t\tif (!repo_get_oid(the_repository, p - (hexsz - 1), &oid)) {\n-\t\t\t\tstruct object *o =\n-\t\t\t\t\tlookup_object(the_repository, &oid);\n+\t\t\toid_ret = repo_get_oid(the_repository, p - (hexsz - 1), &oid);\n+\t\t\t*(p + 1) = c;\n+\n+\t\t\tswitch (cmd->type) {\n+\t\t\tcase NAME_REV:\n+\t\t\t\tif (!oid_ret)\n+\t\t\t\t\to = lookup_object(the_repository, &oid);\n \t\t\t\tif (o)\n \t\t\t\t\tname = get_rev_name(o, &buf);\n+\t\t\t\tif (!name)\n+\t\t\t\t\tcontinue;\n+\t\t\t\tif (cmd->u.name_only)\n+\t\t\t\t\tprintf(\"%.*s%s\", p_len - hexsz, p_start, name);\n+\t\t\t\telse\n+\t\t\t\t\tprintf(\"%.*s (%s)\", p_len, p_start, name);\n+\t\t\t\tbreak;\n+\t\t\tdefault:\n+\t\t\t\tBUG(\"uncovered case: %d\", cmd->type);\n \t\t\t}\n-\t\t\t*(p + 1) = c;\n-\n-\t\t\tif (!name)\n-\t\t\t\tcontinue;\n \n-\t\t\tif (data->name_only)\n-\t\t\t\tprintf(\"%.*s%s\", p_len - hexsz, p_start, name);\n-\t\t\telse\n-\t\t\t\tprintf(\"%.*s (%s)\", p_len, p_start, name);\n \t\t\tp_start = p + 1;\n \t\t}\n \t}\n@@ -567,6 +592,7 @@ int cmd_name_rev(int argc,\n #endif\n \tint all = 0, annotate_stdin = 0, allow_undefined = 1, always = 0, peel_tag = 0;\n \tstruct name_ref_data data = { 0, 0, STRING_LIST_INIT_NODUP, STRING_LIST_INIT_NODUP };\n+\tstruct command cmd;\n \tstruct option opts[] = {\n \t\tOPT_BOOL(0, \"name-only\", &data.name_only, N_(\"print only ref-based names (no object names)\")),\n \t\tOPT_BOOL(0, \"tags\", &data.tags_only, N_(\"only use tags to name the commits\")),\n@@ -596,6 +622,7 @@ int cmd_name_rev(int argc,\n \tinit_commit_rev_name(&rev_names);\n \trepo_config(the_repository, git_default_config, NULL);\n \targc = parse_options(argc, argv, prefix, opts, name_rev_usage, 0);\n+\tinit_name_rev_command(&cmd, data.name_only);\n \n #ifndef WITH_BREAKING_CHANGES\n \tif (transform_stdin) {\n@@ -663,7 +690,7 @@ int cmd_name_rev(int argc,\n \n \t\twhile (strbuf_getline(&sb, stdin) != EOF) {\n \t\t\tstrbuf_addch(&sb, '\\n');\n-\t\t\tname_rev_line(sb.buf, &data);\n+\t\t\tname_rev_line(sb.buf, &cmd);\n \t\t}\n \t\tstrbuf_release(&sb);\n \t} else if (all) {\n-- \n2.54.0.13.g9c7419e39f8\n\n"},{"id":"542871","messageId":"V4_name-rev_dedicated_test.6ae@msgid.xyz","threadId":"65237","inReplyTo":"V4_CV_format-rev.6aa@msgid.xyz","subject":"[PATCH v4 4/5] name-rev: make dedicated --annotate-stdin --name-only test","fromName":"","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-05-07T19:34:23Z","receivedAt":"2026-05-07T19:35:56Z","isPatch":true,"body":"From: Kristoffer Haugsbakk <code@khaugsbakk.name>\n\nThe previous commit split the `--name-only` handling:\n\n1. `--annotate-stdin`: uses the new `struct command`\n2. The rest: uses `struct name_ref_data`\n\nBut there is no dedicated test for the option combination in (1). That\nmeans that the following tests will fail if you neglect to set\n`command.u.name_only` properly:\n\n    name-rev --annotate-stdin works with commitGraph\n    name-rev --annotate-stdin works with non-monotonic timestamps\n\neven though it has nothing to do with what these tests are supposed\nto test.\n\nLet’s add another regression test now that it is relevant.\n\nSigned-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>\n---\n t/t6120-describe.sh | 14 ++++++++++++++\n 1 file changed, 14 insertions(+)\n\ndiff --git a/t/t6120-describe.sh b/t/t6120-describe.sh\nindex 2c70cc561ad..62789f76381 100755\n--- a/t/t6120-describe.sh\n+++ b/t/t6120-describe.sh\n@@ -298,6 +298,20 @@ test_expect_success 'name-rev --annotate-stdin' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'name-rev --annotate-stdin --name-only' '\n+\t>expect.unsorted &&\n+\tfor rev in $(git rev-list --all)\n+\tdo\n+\t\tname=$(git name-rev --name-only $rev) &&\n+\t\techo \"$name\" >>expect.unsorted || return 1\n+\tdone &&\n+\tsort <expect.unsorted >expect &&\n+\tgit name-rev --annotate-stdin --name-only \\\n+\t\t<list >actual.unsorted &&\n+\tsort <actual.unsorted >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success 'name-rev --stdin deprecated' '\n \tgit rev-list --all >list &&\n \tif ! test_have_prereq WITH_BREAKING_CHANGES\n-- \n2.54.0.13.g9c7419e39f8\n\n"},{"id":"542872","messageId":"V4_format-rev_new_builtin.6af@msgid.xyz","threadId":"65237","inReplyTo":"V4_CV_format-rev.6aa@msgid.xyz","subject":"[PATCH v4 5/5] format-rev: introduce builtin for on-demand pretty formatting","fromName":"","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-05-07T19:34:24Z","receivedAt":"2026-05-07T19:36:15Z","isPatch":true,"body":"From: Kristoffer Haugsbakk <code@khaugsbakk.name>\n\nIntroduce a new builtin for pretty formatting one revision expression\nper line or commit object names found in running text.\n\nSometimes you want to format commits. Most of the time you’re\nwalking the graph, e.g. getting a range of commits like\n`master..topic`. That’s a job for git-log(1).\n\nBut there are times when you want to format commits that you encounter\non demand:\n\n• Full hashes in running text that you might want to pretty-print\n• git-last-modified(1) outputs full hashes that you can do the same\n  with\n• git-cherry(1) has `-v` for commit subject, but maybe you want\n  something else?\n\nBut now you can’t use git-log(1), git-show(1), or git-rev-list(1):\n\n• You can’t feed commits piecemeal to these commands, one input\n  for one output; they block until standard in is closed\n• You can’t feed a list of possibly duplicate commits, like the output\n  of git-last-modified(1); they effectively deduplicate the output\n\nBeyond these two points there’s also the input massage problem: you\ncannot feed mixed input (revisions mixed with arbitrary text).\n\nOne might hope that git-cat-file(1) can save us. But it doesn’t\nsupport pretty formats.\n\nBut there is one command that already both handles revisions as\narguments, revisions on standard input, and even revisions mixed in\nwith arbitrary text. Namely git-name-rev(1): the command for outputting\nsymbolic names for commits.\n\nWe made some room in `builtin/name-rev.c` two commits ago. Let’s\nnow add this new git-format-rev(1) command. Taking inspiration from\ngit-name-rev(1), there are two modes:\n\n• revs: like git-name-rev(1) in argv mode, but one revision per line\n  on standard in\n• text: like git-name-rev(1) with `--annotate-stdin`\n\n***\n\nWe need to add this command to the exception list in\n`t/t1517-outside-repo.sh` because it uses “EXPERIMENTAL!”\nin the usage line.\n\nHelped-by: Phillip Wood <phillip.wood@dunelm.org.uk>\nHelped-by: Ramsay Jones <ramsay@ramsayjones.plus.com>\nHelped-by: Junio C Hamano <gitster@pobox.com>\nSigned-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>\n---\n\nNotes (series):\n    v4:\n    • Squash in “SQUASH???” commits from Junio’s branch:\n      • Fix -Werror=maybe-uninitialized at builtin/name-rev.c:893:25\n      • Add this new builtin to Meson build file as well\n    • Flush every output line/record with `maybe_flush_or_die(...)` (Phillip)\n    • Introduce `-z` and other NUL output options (Phillip)\n    • Tests:\n      • More tests: “name lookup failures” (This would have caught my\n        uninitialized mistake from v3)\n      • More tests: for NUL/LF termination\n      • Fix stale setup command which uses “name-rev” in the name\n      • Use `setup:` prefix for both (now two) setup tests\n      • Drop some `:/fifth` expressions in favor just `fifth` (the tag from\n      `test_commit`)\n    • Flesh out documentation\n    • --stdin-mode=revs: Replace “Could not get [sha1][object name]” since the\n      user could be using SHA256\n      • This error string was originally stolen from name-rev\n    • Remember to add this command to `.gitignore` this time\n    • Correct doc to `git [show][log] --no-walk -1` (next to `git show`)\n    • Plus some minor things!\n    v3:\n    • And don’t forget to document --notes this time\n    \n          https://lore.kernel.org/git/CALnO6CB5WOTp_e7Kv3CrEbQ+3XE-gDxNVHf7qATBEbyKWfxpLg@mail.gmail.com/\n\n .gitignore                        |   1 +\n Documentation/git-format-rev.adoc | 215 ++++++++++++++++++++++++++++\n Documentation/meson.build         |   1 +\n Makefile                          |   1 +\n builtin.h                         |   1 +\n builtin/name-rev.c                | 223 ++++++++++++++++++++++++++++++\n command-list.txt                  |   1 +\n git.c                             |   1 +\n t/t1517-outside-repo.sh           |   3 +-\n t/t6120-describe.sh               | 194 ++++++++++++++++++++++++++\n 10 files changed, 640 insertions(+), 1 deletion(-)\n create mode 100644 Documentation/git-format-rev.adoc\n\ndiff --git a/.gitignore b/.gitignore\nindex 24635cf2d6f..e406d3741cd 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -71,6 +71,7 @@\n /git-for-each-ref\n /git-for-each-repo\n /git-format-patch\n+/git-format-rev\n /git-fsck\n /git-fsck-objects\n /git-fsmonitor--daemon\ndiff --git a/Documentation/git-format-rev.adoc b/Documentation/git-format-rev.adoc\nnew file mode 100644\nindex 00000000000..436980012bc\n--- /dev/null\n+++ b/Documentation/git-format-rev.adoc\n@@ -0,0 +1,215 @@\n+git-format-rev(1)\n+=================\n+\n+NAME\n+----\n+git-format-rev - EXPERIMENTAL: Pretty format revisions on demand\n+\n+\n+SYNOPSIS\n+--------\n+[synopsis]\n+(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> --format=<pretty> [--[no-]notes=<ref>] [-z] [--[no-]null-output] [--[no-]null-input]\n+\n+DESCRIPTION\n+-----------\n+\n+Pretty format revisions from standard input.\n+\n+THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.\n+\n+OPTIONS\n+-------\n+\n+`--stdin-mode=<mode>`::\n+\tHow to interpret standard input data:\n++\n+--\n+`revs`:: Each line or record (see the <<io,INPUT AND OUTPUT FORMATS>>\n+\tsection) is interpreted as a commit. Any kind of revision\n+\texpression can be used (see linkgit:gitrevisions[7]). Annotated\n+\ttags are peeled (see linkgit:gitglossary[7]).\n++\n+The argument `rev` is also accepted.\n+`text`:: Formats all commit object names found in freeform text. These\n+\tmust the full object names, i.e. abbreviated hexidecimal object\n+\tnames will not be interpreted.\n++\n+Anything that is parsed as an object name but that is not found to be a\n+commit object name is left alone (echoed).\n+--\n+\n+`--format=<pretty>`::\n+\tPretty format string.\n+\n+`--notes=<ref>`::\n+`--no-notes`::\n+\tCustom notes ref. Notes are displayed when using the `%N`\n+\tatom. See linkgit:git-notes[1].\n+\n+`-z`::\n+`--null`::\n+\tUse _NUL_ character to terminate both input and output instead\n+\tof newline. This option cannot be negated.\n++\n+This is useful if both the input and output could contain newlines or if\n+the input could contain _NUL_ characters; see the <<io,INPUT AND OUTPUT\n+FORMATS>> section.\n+\n+`--null-output`::\n+`--no-null-output`::\n+\tUse _NUL_ character to terminate output instead of newline. The\n+\tdefault is `--no-null-output`.\n++\n+This is useful if the output could contain newlines, for example if the\n+`%n` (newline) atom is used.\n+\n+`--null-input`::\n+`--no-null-input`::\n+\tUse _NUL_ character to terminate input instead of newline. The\n+\tdefault is `--no-null-input`.\n++\n+This is useful if the input revision expressions could contain newlines.\n+It is also useful if the input could contain _NUL_ characters; see the\n+<<io,INPUT AND OUTPUT FORMATS>> section.\n+\n+[[io]]\n+INPUT AND OUTPUT FORMAT\n+-----------------------\n+\n+The command uses newlines for both input and output termination by\n+default. See the `-z`, `--null-output`, and `--null-input` options for\n+using _NUL_ character as the terminator.\n+\n+The mode `--stdin-mode=revs` outputs one formatted commit followed by\n+the terminator. This could either be called a _line_ or a _record_ in\n+case \"line\" is too suggestive of newline termination.\n+\n+Note that this means that the terminator character (newline or _NUL_)\n+acts as a _terminator_, not a _separator_. In other words, the final\n+line or record is also terminated by the terminator character.\n+\n+The mode `--stdin-mode=text` replaces each object name with the\n+formatted commit, i.e. the format `%s` would transform the object name\n+`abcdef012...` to `<subject>` without any termination. Like this:\n+\n+----\n+Did we not fix this in \"<subject>\"?\n+----\n+\n+Regarding input in this mode: using `-z` or `--null-input` makes sure\n+that _NUL_ characters in the input are passed through correctly.\n+\n+It is safe to interactively read and write from this command since each\n+record is immediately flushed.\n+\n+[[examples]]\n+EXAMPLES\n+--------\n+\n+The command linkgit:git-last-modified[1] shows the commit that each file\n+was last modified in.\n+\n+----\n+$ git last-modified -- README.md Makefile\n+7798034171030be0909c56377a4e0e10e6d2df93\tMakefile\n+c50fbb2dd225e7e82abba4380423ae105089f4d7\tREADME.md\n+----\n+\n+We can pipe the result to this command in order to replace the object\n+name with the commit author.\n+\n+----\n+$ git last-modified -- README.md Makefile |\n+    git format-rev --stdin-mode=text --format=%an\n+Junio C Hamano\tMakefile\n+Todd Zullinger\tREADME.md\n+----\n+\n+Another example is _formatting commits in commit messages_. Given this commit message:\n+\n+----\n+Fix off-by-one error\n+\n+Fix off-by-one error introduced in\n+e83c5163316f89bfbde7d9ab23ca2e25604af290.\n+\n+We thought we fixed this in 5569bf9bbedd63a00780fc5c110e0cfab3aa97b9 but\n+that only covered 1/3 of the faulty cases.\n+----\n+\n+We can format the commits and use par(1) to reflow the text, say in a\n+`commit-msg` hook:\n+\n+----\n+$ git config set hook.reference-commits.event commit-msg\n+$ git config set hook.reference-commits.command reference-commits\n+$ cat $(which reference-commits)\n+#/bin/sh\n+\n+msg=\"$1\"\n+rewritten=$(mktemp)\n+git format-rev --stdin-mode=text --format=reference <\"$msg\" |\n+    par >\"$rewritten\"\n+mv \"$rewritten\" \"$msg\"\n+----\n+\n+Which will produce something like this:\n+\n+----\n+Fix off-by-one error\n+\n+Fix off-by-one error introduced in e83c5163316 (Implement better memory\n+allocator, 2005-04-07).\n+\n+We thought we fixed this in 5569bf9bbed (Fix memory allocator,\n+2005-06-22) but that only covered 1/3 of the faulty cases.\n+----\n+\n+DISCUSSION\n+----------\n+\n+This command lets you format any number of revisions in any order\n+through one command invocation. Consider the\n+linkgit:git-last-modified[1] case from the <<examples,EXAMPLES>> section\n+above:\n+\n+1. There might be hundreds of files\n+2. Commits can be repeated, i.e. two or more files were last modified in\n+   the same commit\n+\n+Two widely-used commands which pretty formats commits are\n+linkgit:git-log[1] and linkgit:git-show[1]. It turns out that they are\n+not a good fit for the above use case.\n+\n+- The output of linkgit:git-last-modified[1] would have to be processed\n+  in stages since you need to transform the first column separately and\n+  then link the author to the filename. But this is surmountable.\n+- You can feed each commit to `git show` or `git log --no-walk -1`. But\n+  that means that you need to create a process for each line.\n+- Let’s say that you want to use one process, not one per line. So you\n+  want to feed all the commits to the command. Now you face the problem\n+  that you have to feed all the commits to the commands before you get\n+  any output (this is also the case for the `--stdin` modes). In other\n+  words, you cannot loop through each line, get the author for the\n+  commit, and output the author and the filename. You need to feed all\n+  the commits, get back all the output, and match the output with the\n+  filename.\n+- But the next problem is that commands will deduplicate the input and\n+  only output one commit one single time only. Thus you cannot make the\n+  output order match the input order, since a commit could have been\n+  repeated in the original input.\n+\n+In short, it is straightforward to use these two commands if you use one\n+process per line. It is much more work if you just want to use one\n+process, but still doable. In contrast, this problem is just another\n+shell pipeline with this command.\n+\n+SEE ALSO\n+--------\n+linkgit:git-name-rev[1],\n+linkgit:git-log[1].\n+\n+GIT\n+---\n+Part of the linkgit:git[1] suite\ndiff --git a/Documentation/meson.build b/Documentation/meson.build\nindex d6365b888bb..58e7c6a0b8a 100644\n--- a/Documentation/meson.build\n+++ b/Documentation/meson.build\n@@ -55,6 +55,7 @@ manpages = {\n   'git-for-each-ref.adoc' : 1,\n   'git-for-each-repo.adoc' : 1,\n   'git-format-patch.adoc' : 1,\n+  'git-format-rev.adoc' : 1,\n   'git-fsck-objects.adoc' : 1,\n   'git-fsck.adoc' : 1,\n   'git-fsmonitor--daemon.adoc' : 1,\ndiff --git a/Makefile b/Makefile\nindex 15b1ded1a0b..cbaf91fd846 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -895,6 +895,7 @@ BUILT_INS += $(patsubst builtin/%.o,git-%$X,$(BUILTIN_OBJS))\n BUILT_INS += git-cherry$X\n BUILT_INS += git-cherry-pick$X\n BUILT_INS += git-format-patch$X\n+BUILT_INS += git-format-rev$X\n BUILT_INS += git-fsck-objects$X\n BUILT_INS += git-init$X\n BUILT_INS += git-maintenance$X\ndiff --git a/builtin.h b/builtin.h\nindex 235c51f30e5..63813c90125 100644\n--- a/builtin.h\n+++ b/builtin.h\n@@ -189,6 +189,7 @@ int cmd_fmt_merge_msg(int argc, const char **argv, const char *prefix, struct re\n int cmd_for_each_ref(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_for_each_repo(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_format_patch(int argc, const char **argv, const char *prefix, struct repository *repo);\n+int cmd_format_rev(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_fsck(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_fsmonitor__daemon(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_gc(int argc, const char **argv, const char *prefix, struct repository *repo);\ndiff --git a/builtin/name-rev.c b/builtin/name-rev.c\nindex 475efb0b82b..5494b0424b3 100644\n--- a/builtin/name-rev.c\n+++ b/builtin/name-rev.c\n@@ -18,6 +18,10 @@\n #include \"commit-graph.h\"\n #include \"wildmatch.h\"\n #include \"mem-pool.h\"\n+#include \"pretty.h\"\n+#include \"revision.h\"\n+#include \"notes.h\"\n+#include \"write-or-die.h\"\n \n /*\n  * One day.  See the 'name a rev shortly after epoch' test in t6120 when\n@@ -272,14 +276,26 @@ struct name_ref_data {\n \tstruct string_list exclude_filters;\n };\n \n+struct pretty_format {\n+\tstruct pretty_print_context ctx;\n+\tstruct userformat_want want;\n+};\n+\n enum command_type {\n \tNAME_REV = 1,\n+\tFORMAT_REV = 2,\n+};\n+\n+enum stdin_mode {\n+    TEXT = 1,\n+    REVS = 2,\n };\n \n struct command {\n \tenum command_type type;\n \tunion {\n \t\tint name_only;\n+\t\tstruct pretty_format *pretty_format;\n \t} u;\n };\n \n@@ -290,6 +306,13 @@ static void init_name_rev_command(struct command *cmd,\n \tcmd->u.name_only = name_only;\n }\n \n+static void init_format_rev_command(struct command *cmd,\n+\t\t\t\t    struct pretty_format *pretty_format)\n+{\n+\tcmd->type = FORMAT_REV;\n+\tcmd->u.pretty_format = pretty_format;\n+}\n+\n static struct tip_table {\n \tstruct tip_table_entry {\n \t\tstruct object_id oid;\n@@ -495,6 +518,27 @@ static const char *get_rev_name(const struct object *o, struct strbuf *buf)\n \t}\n }\n \n+static const char *get_format_rev(const struct commit *c,\n+\t\t\t\t  struct pretty_format *format_ctx,\n+\t\t\t\t  struct strbuf *buf)\n+{\n+\tstrbuf_reset(buf);\n+\n+\tif (format_ctx->want.notes) {\n+\t\tstruct strbuf notebuf = STRBUF_INIT;\n+\n+\t\tformat_display_notes(&c->object.oid, &notebuf,\n+\t\t\t\t     get_log_output_encoding(),\n+\t\t\t\t     format_ctx->ctx.fmt == CMIT_FMT_USERFORMAT);\n+\t\tformat_ctx->ctx.notes_message = strbuf_detach(&notebuf, NULL);\n+\t}\n+\n+\tpretty_print_commit(&format_ctx->ctx, c, buf);\n+\tFREE_AND_NULL(format_ctx->ctx.notes_message);\n+\n+\treturn buf->buf;\n+}\n+\n static void show_name(const struct object *obj,\n \t\t      const char *caller_name,\n \t\t      int always, int allow_undefined, int name_only)\n@@ -564,6 +608,18 @@ static void name_rev_line(char *p, struct command *cmd)\n \t\t\t\telse\n \t\t\t\t\tprintf(\"%.*s (%s)\", p_len, p_start, name);\n \t\t\t\tbreak;\n+\t\t\tcase FORMAT_REV:\n+\t\t\t\tif (!oid_ret)\n+\t\t\t\t\to = parse_object(the_repository, &oid);\n+\t\t\t\tif (o && o->type == OBJ_COMMIT)\n+\t\t\t\t\tname = get_format_rev((const struct commit *)o,\n+\t\t\t\t\t\t\t      cmd->u.pretty_format,\n+\t\t\t\t\t\t\t      &buf);\n+\t\t\t\tif (name)\n+\t\t\t\t\tprintf(\"%.*s%s\", p_len - hexsz, p_start, name);\n+\t\t\t\telse\n+\t\t\t\t\tprintf(\"%.*s\", p_len, p_start);\n+\t\t\t\tbreak;\n \t\t\tdefault:\n \t\t\t\tBUG(\"uncovered case: %d\", cmd->type);\n \t\t\t}\n@@ -717,3 +773,170 @@ int cmd_name_rev(int argc,\n \tobject_array_clear(&revs);\n \treturn 0;\n }\n+\n+struct format_nul_data {\n+\tbool nul_input;\n+\tbool nul_output;\n+};\n+\n+static int format_nul_cb(const struct option *option,\n+\t\t\t const char *arg,\n+\t\t\t int unset)\n+{\n+\tstruct format_nul_data *data = option->value;\n+\tdata->nul_input = 1;\n+\tdata->nul_output = 1;\n+\tBUG_ON_OPT_NEG(unset);\n+\tBUG_ON_OPT_ARG(arg);\n+\treturn 0;\n+}\n+\n+static enum stdin_mode parse_stdin_mode(const char *stdin_mode)\n+{\n+\tif (!strcmp(stdin_mode, \"text\"))\n+\t\treturn TEXT;\n+\telse if (!strcmp(stdin_mode, \"revs\") ||\n+\t\t !strcmp(stdin_mode, \"rev\"))\n+\t\treturn REVS;\n+\telse\n+\t\tdie(_(\"'%s' needs to be either text, revs, or rev\"),\n+\t\t    \"--stdin-mode\");\n+}\n+\n+static char const *const format_rev_usage[] = {\n+\tN_(\"(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> \"\n+\t   \"--format=<pretty> [--[no-]notes=<ref>] \"\n+\t   \"[-z] [--[no-]null-output] [--[no-]null-input]\"),\n+\tNULL\n+};\n+\n+int cmd_format_rev(int argc,\n+\t\t   const char **argv,\n+\t\t   const char *prefix,\n+\t\t   struct repository *repo UNUSED)\n+{\n+\tconst char *format = NULL;\n+\tenum stdin_mode stdin_mode;\n+\tconst char *stdin_mode_arg = NULL;\n+\tstruct format_nul_data nul_data = { 0, 0 };\n+\tchar output_terminator;\n+\tstrbuf_getline_fn getline_fn;\n+\tstruct display_notes_opt format_notes_opt;\n+\tstruct rev_info format_rev = REV_INFO_INIT;\n+\tstruct pretty_format format_pp = { 0 };\n+\tstruct string_list notes = STRING_LIST_INIT_NODUP;\n+\tstruct strbuf scratch_buf = STRBUF_INIT;\n+\tstruct command cmd;\n+\tstruct option opts[] = {\n+\t\tOPT_STRING(0, \"format\", &format, N_(\"format\"),\n+\t\t\t   N_(\"pretty format to use\")),\n+\t\tOPT_STRING(0, \"stdin-mode\", &stdin_mode_arg, N_(\"stdin-mode\"),\n+\t\t\t   N_(\"how revs are processed\")),\n+\t\tOPT_STRING_LIST(0, \"notes\", &notes, N_(\"notes\"),\n+\t\t\t\tN_(\"display notes for pretty format\")),\n+\t\tOPT_CALLBACK_F('z', \"null\", &nul_data, N_(\"z\"),\n+\t\t\t       N_(\"Use NUL for input and output termination\"),\n+\t\t\t       PARSE_OPT_NOARG | PARSE_OPT_NONEG, format_nul_cb),\n+\t\tOPT_BOOL(0, \"null-input\", &nul_data.nul_input,\n+\t\t\t N_(\"Use NUL for input termination\")),\n+\t\tOPT_BOOL(0, \"null-output\", &nul_data.nul_output,\n+\t\t\t N_(\"Use NUL for output termination\")),\n+\t\tOPT_END(),\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, opts, format_rev_usage, 0);\n+\n+\tif (argc > 0) {\n+\t\terror(_(\"too many arguments\"));\n+\t\tusage_with_options(format_rev_usage, opts);\n+\t}\n+\n+\tif (!format)\n+\t\tdie(_(\"'%s' is required\"), \"--format\");\n+\tif (!stdin_mode_arg)\n+\t\tdie(_(\"'%s' is required\"), \"--stdin-mode\");\n+\n+\tgetline_fn = nul_data.nul_input ? strbuf_getline_nul : strbuf_getline_lf;\n+\toutput_terminator = nul_data.nul_output ? '\\0' : '\\n';\n+\n+\tinit_display_notes(&format_notes_opt);\n+\tstdin_mode = parse_stdin_mode(stdin_mode_arg);\n+\n+\tget_commit_format(format, &format_rev);\n+\tformat_pp.ctx.rev = &format_rev;\n+\tformat_pp.ctx.fmt = format_rev.commit_format;\n+\tformat_pp.ctx.abbrev = format_rev.abbrev;\n+\tformat_pp.ctx.date_mode_explicit = format_rev.date_mode_explicit;\n+\tformat_pp.ctx.date_mode = format_rev.date_mode;\n+\tformat_pp.ctx.color = GIT_COLOR_AUTO;\n+\n+\tuserformat_find_requirements(format,\n+\t\t\t\t     &format_pp.want);\n+\tif (format_pp.want.notes) {\n+\t\tint ignore_show_notes = 0;\n+\t\tstruct string_list_item *n;\n+\n+\t\tfor_each_string_list_item(n, &notes)\n+\t\t\tenable_ref_display_notes(&format_notes_opt,\n+\t\t\t\t\t\t &ignore_show_notes,\n+\t\t\t\t\t\t n->string);\n+\t\tload_display_notes(&format_notes_opt);\n+\t}\n+\n+\tinit_format_rev_command(&cmd, &format_pp);\n+\n+\tswitch (stdin_mode) {\n+\tcase TEXT:\n+\t\twhile (getline_fn(&scratch_buf, stdin) != EOF) {\n+\t\t\tname_rev_line(scratch_buf.buf, &cmd);\n+\t\t\t/*\n+\t\t\t * We do not pass on the terminator to name_rev_line,\n+\t\t\t * unlike name-rev.\n+\t\t\t */\n+\t\t\tprintf(\"%c\", output_terminator);\n+\t\t\tmaybe_flush_or_die(stdout, \"stdout\");\n+\t\t}\n+\t\tbreak;\n+\tcase REVS:\n+\t\twhile (getline_fn(&scratch_buf, stdin) != EOF) {\n+\t\t\tstruct object_id oid;\n+\t\t\tstruct object *object;\n+\t\t\tstruct object *peeled;\n+\n+\t\t\tif (repo_get_oid(the_repository, scratch_buf.buf, &oid)) {\n+\t\t\t\tfprintf(stderr, \"Could not get object name for %s. Skipping.\\n\",\n+\t\t\t\t\tscratch_buf.buf);\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\tobject = parse_object(the_repository, &oid);\n+\t\t\tif (!object) {\n+\t\t\t\tfprintf(stderr, \"Could not get object for %s. Skipping.\\n\",\n+\t\t\t\t\tscratch_buf.buf);\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\tpeeled = deref_tag(the_repository, object, scratch_buf.buf, 0);\n+\t\t\tif (!peeled || peeled->type != OBJ_COMMIT) {\n+\t\t\t\tfprintf(stderr,\n+\t\t\t\t\t\"Could not get commit for %s. Skipping.\\n\",\n+\t\t\t\t\tscratch_buf.buf);\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\tget_format_rev((struct commit *)peeled,\n+\t\t\t\t       &format_pp, &scratch_buf);\n+\t\t\tprintf(\"%s%c\", scratch_buf.buf, output_terminator);\n+\t\t\tmaybe_flush_or_die(stdout, \"stdout\");\n+\t\t\tstrbuf_release(&scratch_buf);\n+\t\t}\n+\t\tbreak;\n+\tdefault:\n+\t\tBUG(\"uncovered case: %d\", stdin_mode);\n+\t}\n+\n+\tstrbuf_release(&scratch_buf);\n+\tstring_list_clear(&notes, 0);\n+\trelease_display_notes(&format_notes_opt);\n+\treturn 0;\n+}\ndiff --git a/command-list.txt b/command-list.txt\nindex f9005cf4597..df729872dca 100644\n--- a/command-list.txt\n+++ b/command-list.txt\n@@ -108,6 +108,7 @@ git-fmt-merge-msg                       purehelpers\n git-for-each-ref                        plumbinginterrogators\n git-for-each-repo                       plumbinginterrogators\n git-format-patch                        mainporcelain\n+git-format-rev                          plumbinginterrogators\n git-fsck                                ancillaryinterrogators          complete\n git-gc                                  mainporcelain\n git-get-tar-commit-id                   plumbinginterrogators\ndiff --git a/git.c b/git.c\nindex 2b212e6675d..af5b0422b00 100644\n--- a/git.c\n+++ b/git.c\n@@ -578,6 +578,7 @@ static struct cmd_struct commands[] = {\n \t{ \"for-each-ref\", cmd_for_each_ref, RUN_SETUP },\n \t{ \"for-each-repo\", cmd_for_each_repo, RUN_SETUP_GENTLY },\n \t{ \"format-patch\", cmd_format_patch, RUN_SETUP },\n+\t{ \"format-rev\", cmd_format_rev, RUN_SETUP },\n \t{ \"fsck\", cmd_fsck, RUN_SETUP },\n \t{ \"fsck-objects\", cmd_fsck, RUN_SETUP },\n \t{ \"fsmonitor--daemon\", cmd_fsmonitor__daemon, RUN_SETUP },\ndiff --git a/t/t1517-outside-repo.sh b/t/t1517-outside-repo.sh\nindex c824c1a25cf..360a9323343 100755\n--- a/t/t1517-outside-repo.sh\n+++ b/t/t1517-outside-repo.sh\n@@ -114,7 +114,8 @@ do\n \tarchimport | citool | credential-netrc | credential-libsecret | \\\n \tcredential-osxkeychain | cvsexportcommit | cvsimport | cvsserver | \\\n \tdaemon | \\\n-\tdifftool--helper | filter-branch | fsck-objects | get-tar-commit-id | \\\n+\tdifftool--helper | filter-branch | format-rev | fsck-objects | \\\n+\tget-tar-commit-id | \\\n \tgui | gui--askpass | \\\n \thttp-backend | http-fetch | http-push | init-db | \\\n \tmerge-octopus | merge-one-file | merge-resolve | mergetool | \\\ndiff --git a/t/t6120-describe.sh b/t/t6120-describe.sh\nindex 62789f76381..8ee3d2c37d0 100755\n--- a/t/t6120-describe.sh\n+++ b/t/t6120-describe.sh\n@@ -801,4 +801,198 @@ test_expect_success 'do not be fooled by invalid describe format ' '\n \ttest_must_fail git cat-file -t \"refs/tags/super-invalid/./../...../ ~^:/?*[////\\\\\\\\\\\\&}/busted.lock-42-g\"$(cat out)\n '\n \n+test_expect_success 'setup: format-rev' '\n+\tmkdir repo-format &&\n+\tgit -C repo-format init &&\n+\ttest_commit -C repo-format first &&\n+\ttest_commit -C repo-format second &&\n+\ttest_commit -C repo-format third &&\n+\ttest_commit -C repo-format fourth &&\n+\ttest_commit -C repo-format fifth &&\n+\ttest_commit -C repo-format sixth &&\n+\ttest_commit -C repo-format seventh &&\n+\ttest_commit -C repo-format eighth\n+'\n+\n+test_expect_success 'format-rev --stdin-mode=revs' '\n+\tcat >expect <<-\\EOF &&\n+\teighth\n+\tseventh\n+\tfifth\n+\tEOF\n+\tgit -C repo-format format-rev --stdin-mode=revs \\\n+\t\t--format=%s >actual <<-\\EOF &&\n+\tHEAD\n+\tHEAD~\n+\tHEAD~3\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'format-rev --stdin-mode=text from rev-list same as log' '\n+\tgit -C repo-format log --format=reference >expect &&\n+\ttest_file_not_empty expect &&\n+\tgit -C repo-format rev-list HEAD >list &&\n+\tgit -C repo-format format-rev --stdin-mode=text \\\n+\t\t--format=reference <list >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'format-rev --stdin-mode=text with running text and tree oid' '\n+\tcmit_oid=$(git -C repo-format rev-parse fifth) &&\n+\treference=$(git -C repo-format log -n1 --format=reference fifth) &&\n+\ttree=$(git -C repo-format rev-parse HEAD^{tree}) &&\n+\tcat >expect <<-EOF &&\n+\tWe thought we fixed this in ${reference}.\n+\tBut look at this tree: ${tree}.\n+\tEOF\n+\tgit -C repo-format format-rev --stdin-mode=text --format=reference \\\n+\t\t>actual <<-EOF &&\n+\tWe thought we fixed this in ${cmit_oid}.\n+\tBut look at this tree: ${tree}.\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'format-rev with %N (note)' '\n+\ttest_when_finished \"git -C repo-format notes remove\" &&\n+\tgit -C repo-format notes add -m\"Make a note\" &&\n+\tprintf \"Make a note\\n\\n\\n\" >expect &&\n+\tgit -C repo-format format-rev --stdin-mode=revs \\\n+\t\t--format=\"tformat:%N\" \\\n+\t\t>actual <<-\\EOF &&\n+\tHEAD\n+\tHEAD~\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'format-rev --notes<ref> (custom notes ref)' '\n+\t# One custom notes ref\n+\ttest_when_finished \"git -C repo-format notes remove\" &&\n+\ttest_when_finished \"git -C repo-format notes --ref=word remove\" &&\n+\tgit -C repo-format notes add -m\"default\" &&\n+\tgit -C repo-format notes --ref=word add -m\"custom\" &&\n+\tprintf \"custom\\n\\n\" >expect &&\n+\tgit -C repo-format format-rev --stdin-mode=revs \\\n+\t\t--format=\"tformat:%N\" \\\n+\t\t--notes=word \\\n+\t\t>actual <<-\\EOF &&\n+\tHEAD\n+\tEOF\n+\ttest_cmp expect actual &&\n+\t# Glob all\n+\tprintf \"default\\ncustom\\n\\n\" >expect &&\n+\tgit -C repo-format format-rev --stdin-mode=revs \\\n+\t\t--format=\"tformat:%N\" \\\n+\t\t--notes=* >actual <<-\\EOF &&\n+\tHEAD\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'format-rev --stdin-mode=revs on annotated tag peels to commit' '\n+\ttest_when_finished \"git -C repo-format tag -d version\" &&\n+\tgit -C repo-format tag -a -m\"new version\" version &&\n+\tcat >expect <<-\\EOF &&\n+\teighth\n+\tEOF\n+\tgit -C repo-format format-rev --stdin-mode=revs \\\n+\t\t--format=%s \\\n+\t\t>actual <<-\\EOF &&\n+\tversion\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'format-rev --stdin-mode=revs lookup failures' '\n+\ttest_when_finished \"git -C repo-format tag -d tag-to-tree\" &&\n+\tinvalid_syntax=not-valid &&\n+\tnon_existing_oid=${EMPTY_BLOB} &&\n+\ttree=$(git -C repo-format rev-parse eighth^{tree}) &&\n+\tgit -C repo-format tag -a -mmessage tag-to-tree \"$tree\" &&\n+\ttag_to_tree=$(git -C repo-format rev-parse tag-to-tree) &&\n+\tcat >expect <<-EOF &&\n+\tCould not get object name for ${invalid_syntax}. Skipping.\n+\tCould not get object for ${non_existing_oid}. Skipping.\n+\tCould not get commit for ${tree}. Skipping.\n+\tCould not get commit for ${tag_to_tree}. Skipping.\n+\tEOF\n+\tgit -C repo-format format-rev --stdin-mode=revs \\\n+\t\t--format=%s \\\n+\t\t2>actual >out <<-EOF &&\n+\t${invalid_syntax}\n+\t${non_existing_oid}\n+\t${tree}\n+\t${tag_to_tree}\n+\tEOF\n+\ttest_line_count = 0 out &&\n+\ttest_cmp expect actual\n+'\n+\n+\n+test_expect_success 'format-rev -z --stdin-mode=text with object name lookup failures' '\n+\tprintf \"%s\\0\" \"$(git -C repo-format rev-parse HEAD)\" >input &&\n+\tprintf \"%s\\0\" \"$(git -C repo-format rev-parse HEAD^{tree})\" >>input &&\n+\tprintf \"%s\\0\" \"$EMPTY_BLOB\" >>input &&\n+\tprintf \"%s\\0\" \"$(git -C repo-format log --format=%s -1)\" >expect &&\n+\tprintf \"%s\\0\" \"$(git -C repo-format rev-parse HEAD^{tree})\" >>expect &&\n+\tprintf \"%s\\0\" \"$EMPTY_BLOB\" >>expect &&\n+\tgit -C repo-format format-rev --stdin-mode=text \\\n+\t\t--format=%s -z <input >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup: format-rev input and output separators' '\n+\tgit -C repo-format rev-list HEAD >input-lf &&\n+\tgit -C repo-format rev-list -z HEAD >input-nul &&\n+\tgit -C repo-format log --format=%s >output-lf &&\n+\tgit -C repo-format log -z --format=%s >output-nul &&\n+\techo revs >stdin-modes &&\n+\techo text >>stdin-modes\n+'\n+\n+while read mode\n+do\n+\ttest_expect_success \"format-rev -z --stdin-mode=$mode\" '\n+\t\tcat output-nul >expect &&\n+\t\tgit -C repo-format format-rev --stdin-mode=\"$mode\" \\\n+\t\t\t--format=%s -z <input-nul >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"format-rev -z --no-null-input --no-null-output --stdin-mode=$mode\" '\n+\t\tcat output-lf >expect &&\n+\t\tgit -C repo-format format-rev --stdin-mode=\"$mode\" \\\n+\t\t\t--format=%s -z --no-null-input --no-null-output \\\n+\t\t\t<input-lf >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"format-rev ---null-input --stdin-mode=$mode\" '\n+\t\tcat output-lf >expect &&\n+\t\tgit -C repo-format format-rev --stdin-mode=\"$mode\" \\\n+\t\t\t--format=%s --null-input \\\n+\t\t\t<input-nul >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"format-rev --null-output --stdin-mode=$mode\" '\n+\t\tcat output-nul >expect &&\n+\t\tgit -C repo-format format-rev --stdin-mode=\"$mode\" \\\n+\t\t\t--format=%s --null-output \\\n+\t\t\t<input-lf >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"format-rev -z --stdin-mode=$mode with multi-line output\" '\n+\t\tformat=\"%s%n%aI\" &&\n+\t\tgit -C repo-format log -z --format=\"$format\" \\\n+\t\t\t>expect &&\n+\t\tgit -C repo-format format-rev --stdin-mode=\"$mode\" \\\n+\t\t\t--format=\"$format\" -z <input-nul >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+done <stdin-modes\n+\n test_done\n-- \n2.54.0.13.g9c7419e39f8\n\n"},{"id":"542919","messageId":"c04d9cf9-e6a9-4e12-8025-9baededfdafc@app.fastmail.com","threadId":"65237","inReplyTo":"V4_format-rev_new_builtin.6af@msgid.xyz","subject":"Re: [PATCH v4 5/5] format-rev: introduce builtin for on-demand pretty formatting","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-05-08T13:25:26Z","receivedAt":"2026-05-08T13:25:48Z","isPatch":true,"body":"On Thu, May 7, 2026, at 21:34, me myself wrote:\n> From: Kristoffer Haugsbakk <code@khaugsbakk.name>\n>[snip]\n> +[[io]]\n> +INPUT AND OUTPUT FORMAT\n> +-----------------------\n> +\n> +The command uses newlines for both input and output termination by\n> +default. See the `-z`, `--null-output`, and `--null-input` options for\n> +using _NUL_ character as the terminator.\n> +\n> +The mode `--stdin-mode=revs` outputs one formatted commit followed by\n> +the terminator. This could either be called a _line_ or a _record_ in\n> +case \"line\" is too suggestive of newline termination.\n> +\n> +Note that this means that the terminator character (newline or _NUL_)\n> +acts as a _terminator_, not a _separator_. In other words, the final\n> +line or record is also terminated by the terminator character.\n> +\n> +The mode `--stdin-mode=text` replaces each object name with the\n> +formatted commit, i.e. the format `%s` would transform the object name\n> +`abcdef012...` to `<subject>` without any termination. Like this:\n> +\n> +----\n> +Did we not fix this in \"<subject>\"?\n> +----\n> +\n> +Regarding input in this mode: using `-z` or `--null-input` makes sure\n> +that _NUL_ characters in the input are passed through correctly.\n\nThis was the paragraph that I was unhappy with (see cover letter).\n\nWe’re supposed to be dealing with text here. So on second thought I\ndon’t think we need to fuzz about passing through NUL characters.\n\nInstead I could mention that `-z` for this mode allows you to further\ntransform running text from other commands that use NUL\ntermination. Like `git last-modified -z` (see the Examples section).\n\n> +\n> +It is safe to interactively read and write from this command since each\n> +record is immediately flushed.\n> +\n> +[[examples]]\n> +EXAMPLES\n> +--------\n> +\n> +The command linkgit:git-last-modified[1] shows the commit that each file\n> +was last modified in.\n>[snip]\n"},{"id":"543057","messageId":"c3da244d-9e01-42dc-a4fe-98b2d1c30570@app.fastmail.com","threadId":"65237","inReplyTo":"V4_format-rev_new_builtin.6af@msgid.xyz","subject":"Re: [PATCH v4 5/5] format-rev: introduce builtin for on-demand pretty formatting","fromName":"Kristoffer Haugsbakk","fromEmail":"code@khaugsbakk.name","sentAt":"2026-05-11T13:25:57Z","receivedAt":"2026-05-11T13:26:19Z","isPatch":true,"body":"On Thu, May 7, 2026, at 21:34, kristofferhaugsbakk@fastmail.com wrote:\n> From: Kristoffer Haugsbakk <code@khaugsbakk.name>\n>\n> Introduce a new builtin for pretty formatting one revision expression\n> per line or commit object names found in running text.\n>\n>[snip]\n>\n> diff --git a/Documentation/git-format-rev.adoc\n>[snip]\n> +OPTIONS\n> +-------\n> +\n> +`--stdin-mode=<mode>`::\n> +\tHow to interpret standard input data:\n> ++\n> +--\n> +`revs`:: Each line or record (see the <<io,INPUT AND OUTPUT FORMATS>>\n> +\tsection) is interpreted as a commit. Any kind of revision\n> +\texpression can be used (see linkgit:gitrevisions[7]). Annotated\n> +\ttags are peeled (see linkgit:gitglossary[7]).\n> ++\n> +The argument `rev` is also accepted.\n> +`text`:: Formats all commit object names found in freeform text. These\n> +\tmust the full object names, i.e. abbreviated hexidecimal object\n> +\tnames will not be interpreted.\n> ++\n> +Anything that is parsed as an object name but that is not found to be a\n> +commit object name is left alone (echoed).\n> +--\n\nThis nested definition list is malformed. I’ll need to fix that.\n\n> +\n> +`--format=<pretty>`::\n> +\tPretty format string.\n>[snip]\n"},{"id":"543061","messageId":"V5_CV_format-rev.6c9@msgid.xyz","threadId":"65237","inReplyTo":"V4_CV_format-rev.6aa@msgid.xyz","subject":"[PATCH v5 0/5] format-rev: introduce builtin for on-demand pretty formatting","fromName":"","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-05-11T15:45:44Z","receivedAt":"2026-05-11T15:46:08Z","isPatch":true,"body":"From: Kristoffer Haugsbakk <code@khaugsbakk.name>\n\n(Subject from v2: name-rev: learn --format=<pretty>)\n\nTopic name (applied): kh/name-rev-custom-format\n\nTopic summary: Introduce a new builtin for pretty formatting either (1) one\nrevision expression per line or (2) commit object names found in running\ntext.\n\nSee the last patch for the motivation. In short there isn’t anything that I\nhave found that lets you format however many commits you want through one\nprocess (so looping over `git show` is excepted).\n\nThe other patches prepare for this change.\n\n§ Changes in v5\n\nNotes from patch 5/5, all doc changes:\n\n    • Fix definition list mistake\n      • No blank line between the two modes\n      • Also replace `::` delimiters with `;;`. This is not strictly\n        needed since the definition list is inside an open block. But\n        it is consistent with all other definition list in definition\n        lists I’ve seen. (The command option section is a long\n        definition list.)\n    • Rewrite part that I was unhappy about regarding\n      `--stdin-mode=text` and `-z`:\n\n        https://lore.kernel.org/git/c04d9cf9-e6a9-4e12-8025-9baededfdafc@app.fastmail.com/\n    • Replace `abcdef012...` with just “some object name”. The object\n      name is arbitrary and my AsciiDoc output seems to do some weird\n      things for `...` inside backticks for HTML.\n    • Replace “this problem is just another” with “this problem\n      *is solved* with...”\n\n§ Link to v4\n\nhttps://lore.kernel.org/git/V4_CV_format-rev.6aa@msgid.xyz/\n\n[1/5] name-rev: wrap both blocks in braces\n[2/5] name-rev: run clang-format before factoring code\n[3/5] name-rev: factor code for sharing with a new command\n[4/5] name-rev: make dedicated --annotate-stdin --name-only test\n[5/5] format-rev: introduce builtin for on-demand pretty formatting\n\n .gitignore                        |   1 +\n Documentation/git-format-rev.adoc | 215 +++++++++++++++++++++\n Documentation/meson.build         |   1 +\n Makefile                          |   1 +\n builtin.h                         |   1 +\n builtin/name-rev.c                | 300 +++++++++++++++++++++++++++---\n command-list.txt                  |   1 +\n git.c                             |   1 +\n t/t1517-outside-repo.sh           |   3 +-\n t/t6120-describe.sh               | 208 +++++++++++++++++++++\n 10 files changed, 706 insertions(+), 26 deletions(-)\n create mode 100644 Documentation/git-format-rev.adoc\n\nInterdiff against v4:\ndiff --git a/Documentation/git-format-rev.adoc b/Documentation/git-format-rev.adoc\nindex 436980012bc..c40d52e9f6d 100644\n--- a/Documentation/git-format-rev.adoc\n+++ b/Documentation/git-format-rev.adoc\n@@ -25,13 +25,14 @@ OPTIONS\n \tHow to interpret standard input data:\n +\n --\n-`revs`:: Each line or record (see the <<io,INPUT AND OUTPUT FORMATS>>\n+`revs`;; Each line or record (see the <<io,INPUT AND OUTPUT FORMATS>>\n \tsection) is interpreted as a commit. Any kind of revision\n \texpression can be used (see linkgit:gitrevisions[7]). Annotated\n \ttags are peeled (see linkgit:gitglossary[7]).\n +\n The argument `rev` is also accepted.\n-`text`:: Formats all commit object names found in freeform text. These\n+\n+`text`;; Formats all commit object names found in freeform text. These\n \tmust the full object names, i.e. abbreviated hexidecimal object\n \tnames will not be interpreted.\n +\n@@ -53,8 +54,12 @@ commit object name is left alone (echoed).\n \tof newline. This option cannot be negated.\n +\n This is useful if both the input and output could contain newlines or if\n-the input could contain _NUL_ characters; see the <<io,INPUT AND OUTPUT\n-FORMATS>> section.\n+the input to this command also uses _NUL_ character termination; see the\n+<<io,INPUT AND OUTPUT FORMATS>> section below.\n++\n+The mode `--stdin-mode=text` can have use for this option when it needs\n+to process input like for example `git last-modified -z`; see the\n+<<examples,EXAMPLES>> section below.\n \n `--null-output`::\n `--no-null-output`::\n@@ -70,8 +75,6 @@ This is useful if the output could contain newlines, for example if the\n \tdefault is `--no-null-input`.\n +\n This is useful if the input revision expressions could contain newlines.\n-It is also useful if the input could contain _NUL_ characters; see the\n-<<io,INPUT AND OUTPUT FORMATS>> section.\n \n [[io]]\n INPUT AND OUTPUT FORMAT\n@@ -90,16 +93,13 @@ acts as a _terminator_, not a _separator_. In other words, the final\n line or record is also terminated by the terminator character.\n \n The mode `--stdin-mode=text` replaces each object name with the\n-formatted commit, i.e. the format `%s` would transform the object name\n-`abcdef012...` to `<subject>` without any termination. Like this:\n+formatted commit, i.e. the format `%s` would transform some commit\n+object name to `<subject>` without any termination. Like this:\n \n ----\n Did we not fix this in \"<subject>\"?\n ----\n \n-Regarding input in this mode: using `-z` or `--null-input` makes sure\n-that _NUL_ characters in the input are passed through correctly.\n-\n It is safe to interactively read and write from this command since each\n record is immediately flushed.\n \n@@ -202,8 +202,8 @@ not a good fit for the above use case.\n \n In short, it is straightforward to use these two commands if you use one\n process per line. It is much more work if you just want to use one\n-process, but still doable. In contrast, this problem is just another\n-shell pipeline with this command.\n+process, but still doable. In contrast, this problem is solved with just\n+another shell pipeline with this command.\n \n SEE ALSO\n --------\nRange-diff against v4:\n1:  9cb5cfd1ec3 = 1:  9cb5cfd1ec3 name-rev: wrap both blocks in braces\n2:  14900271321 = 2:  14900271321 name-rev: run clang-format before factoring code\n3:  724ec022894 = 3:  724ec022894 name-rev: factor code for sharing with a new command\n4:  382efc3ddb8 = 4:  382efc3ddb8 name-rev: make dedicated --annotate-stdin --name-only test\n5:  049a45e32bc ! 5:  425eb16728c format-rev: introduce builtin for on-demand pretty formatting\n    @@ Documentation/git-format-rev.adoc (new)\n     +\tHow to interpret standard input data:\n     ++\n     +--\n    -+`revs`:: Each line or record (see the <<io,INPUT AND OUTPUT FORMATS>>\n    ++`revs`;; Each line or record (see the <<io,INPUT AND OUTPUT FORMATS>>\n     +\tsection) is interpreted as a commit. Any kind of revision\n     +\texpression can be used (see linkgit:gitrevisions[7]). Annotated\n     +\ttags are peeled (see linkgit:gitglossary[7]).\n     ++\n     +The argument `rev` is also accepted.\n    -+`text`:: Formats all commit object names found in freeform text. These\n    ++\n    ++`text`;; Formats all commit object names found in freeform text. These\n     +\tmust the full object names, i.e. abbreviated hexidecimal object\n     +\tnames will not be interpreted.\n     ++\n    @@ Documentation/git-format-rev.adoc (new)\n     +\tof newline. This option cannot be negated.\n     ++\n     +This is useful if both the input and output could contain newlines or if\n    -+the input could contain _NUL_ characters; see the <<io,INPUT AND OUTPUT\n    -+FORMATS>> section.\n    ++the input to this command also uses _NUL_ character termination; see the\n    ++<<io,INPUT AND OUTPUT FORMATS>> section below.\n    +++\n    ++The mode `--stdin-mode=text` can have use for this option when it needs\n    ++to process input like for example `git last-modified -z`; see the\n    ++<<examples,EXAMPLES>> section below.\n     +\n     +`--null-output`::\n     +`--no-null-output`::\n    @@ Documentation/git-format-rev.adoc (new)\n     +\tdefault is `--no-null-input`.\n     ++\n     +This is useful if the input revision expressions could contain newlines.\n    -+It is also useful if the input could contain _NUL_ characters; see the\n    -+<<io,INPUT AND OUTPUT FORMATS>> section.\n     +\n     +[[io]]\n     +INPUT AND OUTPUT FORMAT\n    @@ Documentation/git-format-rev.adoc (new)\n     +line or record is also terminated by the terminator character.\n     +\n     +The mode `--stdin-mode=text` replaces each object name with the\n    -+formatted commit, i.e. the format `%s` would transform the object name\n    -+`abcdef012...` to `<subject>` without any termination. Like this:\n    ++formatted commit, i.e. the format `%s` would transform some commit\n    ++object name to `<subject>` without any termination. Like this:\n     +\n     +----\n     +Did we not fix this in \"<subject>\"?\n     +----\n     +\n    -+Regarding input in this mode: using `-z` or `--null-input` makes sure\n    -+that _NUL_ characters in the input are passed through correctly.\n    -+\n     +It is safe to interactively read and write from this command since each\n     +record is immediately flushed.\n     +\n    @@ Documentation/git-format-rev.adoc (new)\n     +\n     +In short, it is straightforward to use these two commands if you use one\n     +process per line. It is much more work if you just want to use one\n    -+process, but still doable. In contrast, this problem is just another\n    -+shell pipeline with this command.\n    ++process, but still doable. In contrast, this problem is solved with just\n    ++another shell pipeline with this command.\n     +\n     +SEE ALSO\n     +--------\n\nbase-commit: 67006b9db8b772423ad0706029286096307d2567\n-- \n2.54.0.13.g9c7419e39f8\n\n"},{"id":"543062","messageId":"V5_name-rev_braces.6ca@msgid.xyz","threadId":"65237","inReplyTo":"V5_CV_format-rev.6c9@msgid.xyz","subject":"[PATCH v5 1/5] name-rev: wrap both blocks in braces","fromName":"","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-05-11T15:45:45Z","receivedAt":"2026-05-11T15:46:27Z","isPatch":true,"body":"From: Kristoffer Haugsbakk <code@khaugsbakk.name>\n\nSee `CodingGuidelines`:\n\n    - When there are multiple arms to a conditional and some of them\n      require braces, enclose even a single line block in braces for\n      consistency. [...]\n\nSigned-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>\n---\n\nNotes (series):\n    v2:\n    Fix stray formatting of `(p+1)`\n\n builtin/name-rev.c | 8 ++++----\n 1 file changed, 4 insertions(+), 4 deletions(-)\n\ndiff --git a/builtin/name-rev.c b/builtin/name-rev.c\nindex 6188cf98ce0..171e7bd0e98 100644\n--- a/builtin/name-rev.c\n+++ b/builtin/name-rev.c\n@@ -466,9 +466,9 @@ static const char *get_rev_name(const struct object *o, struct strbuf *buf)\n \tif (!n)\n \t\treturn NULL;\n \n-\tif (!n->generation)\n+\tif (!n->generation) {\n \t\treturn n->tip_name;\n-\telse {\n+\t} else {\n \t\tstrbuf_reset(buf);\n \t\tstrbuf_addstr(buf, n->tip_name);\n \t\tstrbuf_strip_suffix(buf, \"^0\");\n@@ -516,9 +516,9 @@ static void name_rev_line(char *p, struct name_ref_data *data)\n \n \tfor (p_start = p; *p; p++) {\n #define ishex(x) (isdigit((x)) || ((x) >= 'a' && (x) <= 'f'))\n-\t\tif (!ishex(*p))\n+\t\tif (!ishex(*p)) {\n \t\t\tcounter = 0;\n-\t\telse if (++counter == hexsz &&\n+\t\t} else if (++counter == hexsz &&\n \t\t\t !ishex(*(p+1))) {\n \t\t\tstruct object_id oid;\n \t\t\tconst char *name = NULL;\n-- \n2.54.0.13.g9c7419e39f8\n\n"},{"id":"543063","messageId":"V5_name-rev_clang-format.6cb@msgid.xyz","threadId":"65237","inReplyTo":"V5_CV_format-rev.6c9@msgid.xyz","subject":"[PATCH v5 2/5] name-rev: run clang-format before factoring code","fromName":"","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-05-11T15:45:46Z","receivedAt":"2026-05-11T15:46:46Z","isPatch":true,"body":"From: Kristoffer Haugsbakk <code@khaugsbakk.name>\n\nWe are about to move code around to prepare for adding a new\ncommand. Let’s deal with clang-format changes first in the affected\nareas.\n\nSigned-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>\n---\n builtin/name-rev.c | 18 +++++++++---------\n 1 file changed, 9 insertions(+), 9 deletions(-)\n\ndiff --git a/builtin/name-rev.c b/builtin/name-rev.c\nindex 171e7bd0e98..6357eaa76d0 100644\n--- a/builtin/name-rev.c\n+++ b/builtin/name-rev.c\n@@ -519,22 +519,22 @@ static void name_rev_line(char *p, struct name_ref_data *data)\n \t\tif (!ishex(*p)) {\n \t\t\tcounter = 0;\n \t\t} else if (++counter == hexsz &&\n-\t\t\t !ishex(*(p+1))) {\n+\t\t\t   !ishex(*(p + 1))) {\n \t\t\tstruct object_id oid;\n \t\t\tconst char *name = NULL;\n-\t\t\tchar c = *(p+1);\n+\t\t\tchar c = *(p + 1);\n \t\t\tint p_len = p - p_start + 1;\n \n \t\t\tcounter = 0;\n \n-\t\t\t*(p+1) = 0;\n+\t\t\t*(p + 1) = 0;\n \t\t\tif (!repo_get_oid(the_repository, p - (hexsz - 1), &oid)) {\n \t\t\t\tstruct object *o =\n \t\t\t\t\tlookup_object(the_repository, &oid);\n \t\t\t\tif (o)\n \t\t\t\t\tname = get_rev_name(o, &buf);\n \t\t\t}\n-\t\t\t*(p+1) = c;\n+\t\t\t*(p + 1) = c;\n \n \t\t\tif (!name)\n \t\t\t\tcontinue;\n@@ -571,9 +571,9 @@ int cmd_name_rev(int argc,\n \t\tOPT_BOOL(0, \"name-only\", &data.name_only, N_(\"print only ref-based names (no object names)\")),\n \t\tOPT_BOOL(0, \"tags\", &data.tags_only, N_(\"only use tags to name the commits\")),\n \t\tOPT_STRING_LIST(0, \"refs\", &data.ref_filters, N_(\"pattern\"),\n-\t\t\t\t   N_(\"only use refs matching <pattern>\")),\n+\t\t\t\tN_(\"only use refs matching <pattern>\")),\n \t\tOPT_STRING_LIST(0, \"exclude\", &data.exclude_filters, N_(\"pattern\"),\n-\t\t\t\t   N_(\"ignore refs matching <pattern>\")),\n+\t\t\t\tN_(\"ignore refs matching <pattern>\")),\n \t\tOPT_GROUP(\"\"),\n \t\tOPT_BOOL(0, \"all\", &all, N_(\"list all commits reachable from all refs\")),\n #ifndef WITH_BREAKING_CHANGES\n@@ -585,10 +585,10 @@ int cmd_name_rev(int argc,\n #endif /* WITH_BREAKING_CHANGES */\n \t\tOPT_BOOL(0, \"annotate-stdin\", &annotate_stdin, N_(\"annotate text from stdin\")),\n \t\tOPT_BOOL(0, \"undefined\", &allow_undefined, N_(\"allow to print `undefined` names (default)\")),\n-\t\tOPT_BOOL(0, \"always\",     &always,\n-\t\t\t   N_(\"show abbreviated commit object as fallback\")),\n+\t\tOPT_BOOL(0, \"always\", &always,\n+\t\t\t N_(\"show abbreviated commit object as fallback\")),\n \t\tOPT_HIDDEN_BOOL(0, \"peel-tag\", &peel_tag,\n-\t\t\t   N_(\"dereference tags in the input (internal use)\")),\n+\t\t\t\tN_(\"dereference tags in the input (internal use)\")),\n \t\tOPT_END(),\n \t};\n \n-- \n2.54.0.13.g9c7419e39f8\n\n"},{"id":"543064","messageId":"V5_name-rev_factor.6cc@msgid.xyz","threadId":"65237","inReplyTo":"V5_CV_format-rev.6c9@msgid.xyz","subject":"[PATCH v5 3/5] name-rev: factor code for sharing with a new command","fromName":"","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-05-11T15:45:47Z","receivedAt":"2026-05-11T15:47:06Z","isPatch":true,"body":"From: Kristoffer Haugsbakk <code@khaugsbakk.name>\n\nWe are about to introduce a new command git-format-rev(1) to this\nfile. Let’s factor some code so that we can share it with the new\ncommand.\n\nWe want to be able to format commits found in freeform text, and\ngit-name-rev(1) already has a function for that but for symbolic\nnames. Let’s use a tagged union for the command-specific payload.\n\nNo functional changes.\n\nSigned-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>\n---\n\nNotes (series):\n    v4:\n    • Pull out `*(p + 1)` instead of doing it in every `case` (Phillip)\n    • Go back to using `continue` instead of `goto` (Phillip)\n\n builtin/name-rev.c | 53 ++++++++++++++++++++++++++++++++++------------\n 1 file changed, 40 insertions(+), 13 deletions(-)\n\ndiff --git a/builtin/name-rev.c b/builtin/name-rev.c\nindex 6357eaa76d0..475efb0b82b 100644\n--- a/builtin/name-rev.c\n+++ b/builtin/name-rev.c\n@@ -272,6 +272,24 @@ struct name_ref_data {\n \tstruct string_list exclude_filters;\n };\n \n+enum command_type {\n+\tNAME_REV = 1,\n+};\n+\n+struct command {\n+\tenum command_type type;\n+\tunion {\n+\t\tint name_only;\n+\t} u;\n+};\n+\n+static void init_name_rev_command(struct command *cmd,\n+\t\t\t\t  int name_only)\n+{\n+\tcmd->type = NAME_REV;\n+\tcmd->u.name_only = name_only;\n+}\n+\n static struct tip_table {\n \tstruct tip_table_entry {\n \t\tstruct object_id oid;\n@@ -507,7 +525,7 @@ static char const * const name_rev_usage[] = {\n \tNULL\n };\n \n-static void name_rev_line(char *p, struct name_ref_data *data)\n+static void name_rev_line(char *p, struct command *cmd)\n {\n \tstruct strbuf buf = STRBUF_INIT;\n \tint counter = 0;\n@@ -524,25 +542,32 @@ static void name_rev_line(char *p, struct name_ref_data *data)\n \t\t\tconst char *name = NULL;\n \t\t\tchar c = *(p + 1);\n \t\t\tint p_len = p - p_start + 1;\n+\t\t\tstruct object *o = NULL;\n+\t\t\tint oid_ret = 1;\n \n \t\t\tcounter = 0;\n \n \t\t\t*(p + 1) = 0;\n-\t\t\tif (!repo_get_oid(the_repository, p - (hexsz - 1), &oid)) {\n-\t\t\t\tstruct object *o =\n-\t\t\t\t\tlookup_object(the_repository, &oid);\n+\t\t\toid_ret = repo_get_oid(the_repository, p - (hexsz - 1), &oid);\n+\t\t\t*(p + 1) = c;\n+\n+\t\t\tswitch (cmd->type) {\n+\t\t\tcase NAME_REV:\n+\t\t\t\tif (!oid_ret)\n+\t\t\t\t\to = lookup_object(the_repository, &oid);\n \t\t\t\tif (o)\n \t\t\t\t\tname = get_rev_name(o, &buf);\n+\t\t\t\tif (!name)\n+\t\t\t\t\tcontinue;\n+\t\t\t\tif (cmd->u.name_only)\n+\t\t\t\t\tprintf(\"%.*s%s\", p_len - hexsz, p_start, name);\n+\t\t\t\telse\n+\t\t\t\t\tprintf(\"%.*s (%s)\", p_len, p_start, name);\n+\t\t\t\tbreak;\n+\t\t\tdefault:\n+\t\t\t\tBUG(\"uncovered case: %d\", cmd->type);\n \t\t\t}\n-\t\t\t*(p + 1) = c;\n-\n-\t\t\tif (!name)\n-\t\t\t\tcontinue;\n \n-\t\t\tif (data->name_only)\n-\t\t\t\tprintf(\"%.*s%s\", p_len - hexsz, p_start, name);\n-\t\t\telse\n-\t\t\t\tprintf(\"%.*s (%s)\", p_len, p_start, name);\n \t\t\tp_start = p + 1;\n \t\t}\n \t}\n@@ -567,6 +592,7 @@ int cmd_name_rev(int argc,\n #endif\n \tint all = 0, annotate_stdin = 0, allow_undefined = 1, always = 0, peel_tag = 0;\n \tstruct name_ref_data data = { 0, 0, STRING_LIST_INIT_NODUP, STRING_LIST_INIT_NODUP };\n+\tstruct command cmd;\n \tstruct option opts[] = {\n \t\tOPT_BOOL(0, \"name-only\", &data.name_only, N_(\"print only ref-based names (no object names)\")),\n \t\tOPT_BOOL(0, \"tags\", &data.tags_only, N_(\"only use tags to name the commits\")),\n@@ -596,6 +622,7 @@ int cmd_name_rev(int argc,\n \tinit_commit_rev_name(&rev_names);\n \trepo_config(the_repository, git_default_config, NULL);\n \targc = parse_options(argc, argv, prefix, opts, name_rev_usage, 0);\n+\tinit_name_rev_command(&cmd, data.name_only);\n \n #ifndef WITH_BREAKING_CHANGES\n \tif (transform_stdin) {\n@@ -663,7 +690,7 @@ int cmd_name_rev(int argc,\n \n \t\twhile (strbuf_getline(&sb, stdin) != EOF) {\n \t\t\tstrbuf_addch(&sb, '\\n');\n-\t\t\tname_rev_line(sb.buf, &data);\n+\t\t\tname_rev_line(sb.buf, &cmd);\n \t\t}\n \t\tstrbuf_release(&sb);\n \t} else if (all) {\n-- \n2.54.0.13.g9c7419e39f8\n\n"},{"id":"543065","messageId":"V5_name-rev_dedicated_test.6cd@msgid.xyz","threadId":"65237","inReplyTo":"V5_CV_format-rev.6c9@msgid.xyz","subject":"[PATCH v5 4/5] name-rev: make dedicated --annotate-stdin --name-only test","fromName":"","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-05-11T15:45:48Z","receivedAt":"2026-05-11T15:47:26Z","isPatch":true,"body":"From: Kristoffer Haugsbakk <code@khaugsbakk.name>\n\nThe previous commit split the `--name-only` handling:\n\n1. `--annotate-stdin`: uses the new `struct command`\n2. The rest: uses `struct name_ref_data`\n\nBut there is no dedicated test for the option combination in (1). That\nmeans that the following tests will fail if you neglect to set\n`command.u.name_only` properly:\n\n    name-rev --annotate-stdin works with commitGraph\n    name-rev --annotate-stdin works with non-monotonic timestamps\n\neven though it has nothing to do with what these tests are supposed\nto test.\n\nLet’s add another regression test now that it is relevant.\n\nSigned-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>\n---\n t/t6120-describe.sh | 14 ++++++++++++++\n 1 file changed, 14 insertions(+)\n\ndiff --git a/t/t6120-describe.sh b/t/t6120-describe.sh\nindex 2c70cc561ad..62789f76381 100755\n--- a/t/t6120-describe.sh\n+++ b/t/t6120-describe.sh\n@@ -298,6 +298,20 @@ test_expect_success 'name-rev --annotate-stdin' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'name-rev --annotate-stdin --name-only' '\n+\t>expect.unsorted &&\n+\tfor rev in $(git rev-list --all)\n+\tdo\n+\t\tname=$(git name-rev --name-only $rev) &&\n+\t\techo \"$name\" >>expect.unsorted || return 1\n+\tdone &&\n+\tsort <expect.unsorted >expect &&\n+\tgit name-rev --annotate-stdin --name-only \\\n+\t\t<list >actual.unsorted &&\n+\tsort <actual.unsorted >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success 'name-rev --stdin deprecated' '\n \tgit rev-list --all >list &&\n \tif ! test_have_prereq WITH_BREAKING_CHANGES\n-- \n2.54.0.13.g9c7419e39f8\n\n"},{"id":"543066","messageId":"V5_format-rev_new_builtin.6ce@msgid.xyz","threadId":"65237","inReplyTo":"V5_CV_format-rev.6c9@msgid.xyz","subject":"[PATCH v5 5/5] format-rev: introduce builtin for on-demand pretty formatting","fromName":"","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-05-11T15:45:49Z","receivedAt":"2026-05-11T15:47:43Z","isPatch":true,"body":"From: Kristoffer Haugsbakk <code@khaugsbakk.name>\n\nIntroduce a new builtin for pretty formatting one revision expression\nper line or commit object names found in running text.\n\nSometimes you want to format commits. Most of the time you’re\nwalking the graph, e.g. getting a range of commits like\n`master..topic`. That’s a job for git-log(1).\n\nBut there are times when you want to format commits that you encounter\non demand:\n\n• Full hashes in running text that you might want to pretty-print\n• git-last-modified(1) outputs full hashes that you can do the same\n  with\n• git-cherry(1) has `-v` for commit subject, but maybe you want\n  something else?\n\nBut now you can’t use git-log(1), git-show(1), or git-rev-list(1):\n\n• You can’t feed commits piecemeal to these commands, one input\n  for one output; they block until standard in is closed\n• You can’t feed a list of possibly duplicate commits, like the output\n  of git-last-modified(1); they effectively deduplicate the output\n\nBeyond these two points there’s also the input massage problem: you\ncannot feed mixed input (revisions mixed with arbitrary text).\n\nOne might hope that git-cat-file(1) can save us. But it doesn’t\nsupport pretty formats.\n\nBut there is one command that already both handles revisions as\narguments, revisions on standard input, and even revisions mixed in\nwith arbitrary text. Namely git-name-rev(1): the command for outputting\nsymbolic names for commits.\n\nWe made some room in `builtin/name-rev.c` two commits ago. Let’s\nnow add this new git-format-rev(1) command. Taking inspiration from\ngit-name-rev(1), there are two modes:\n\n• revs: like git-name-rev(1) in argv mode, but one revision per line\n  on standard in\n• text: like git-name-rev(1) with `--annotate-stdin`\n\n***\n\nWe need to add this command to the exception list in\n`t/t1517-outside-repo.sh` because it uses “EXPERIMENTAL!”\nin the usage line.\n\nHelped-by: Phillip Wood <phillip.wood@dunelm.org.uk>\nHelped-by: Ramsay Jones <ramsay@ramsayjones.plus.com>\nHelped-by: Junio C Hamano <gitster@pobox.com>\nSigned-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>\n---\n\nNotes (series):\n    v5:\n    • (All doc changes)\n    • Fix definition list mistake\n      • No blank line between the two modes\n      • Also replace `::` delimiters with `;;`. This is not strictly\n        needed since the definition list is inside an open block. But\n        it is consistent with all other definition list in definition\n        lists I’ve seen. (The command option section is a long\n        definition list.)\n    • Rewrite part that I was unhappy about regarding\n      `--stdin-mode=text` and `-z`:\n    \n        https://lore.kernel.org/git/c04d9cf9-e6a9-4e12-8025-9baededfdafc@app.fastmail.com/\n    • Replace `abcdef012...` with just “some object name”. The object\n      name is arbitrary and my AsciiDoc output seems to do some weird\n      things for `...` inside backticks for HTML.\n    • Replace “this problem is just another” with “this problem\n      *is solved* with...”\n    \n    ***\n    \n    v4:\n    • Squash in “SQUASH???” commits from Junio’s branch:\n      • Fix -Werror=maybe-uninitialized at builtin/name-rev.c:893:25\n      • Add this new builtin to Meson build file as well\n    • Flush every output line/record with `maybe_flush_or_die(...)` (Phillip)\n    • Introduce `-z` and other NUL output options (Phillip)\n    • Tests:\n      • More tests: “name lookup failures” (This would have caught my\n        uninitialized mistake from v3)\n      • More tests: for NUL/LF termination\n      • Fix stale setup command which uses “name-rev” in the name\n      • Use `setup:` prefix for both (now two) setup tests\n      • Drop some `:/fifth` expressions in favor just `fifth` (the tag from\n      `test_commit`)\n    • Flesh out documentation\n    • --stdin-mode=revs: Replace “Could not get [sha1][object name]” since the\n      user could be using SHA256\n      • This error string was originally stolen from name-rev\n    • Remember to add this command to `.gitignore` this time\n    • Correct doc to `git [show][log] --no-walk -1` (next to `git show`)\n    • Plus some minor things!\n    v3:\n    • And don’t forget to document --notes this time\n    \n          https://lore.kernel.org/git/CALnO6CB5WOTp_e7Kv3CrEbQ+3XE-gDxNVHf7qATBEbyKWfxpLg@mail.gmail.com/\n\n .gitignore                        |   1 +\n Documentation/git-format-rev.adoc | 215 ++++++++++++++++++++++++++++\n Documentation/meson.build         |   1 +\n Makefile                          |   1 +\n builtin.h                         |   1 +\n builtin/name-rev.c                | 223 ++++++++++++++++++++++++++++++\n command-list.txt                  |   1 +\n git.c                             |   1 +\n t/t1517-outside-repo.sh           |   3 +-\n t/t6120-describe.sh               | 194 ++++++++++++++++++++++++++\n 10 files changed, 640 insertions(+), 1 deletion(-)\n create mode 100644 Documentation/git-format-rev.adoc\n\ndiff --git a/.gitignore b/.gitignore\nindex 24635cf2d6f..e406d3741cd 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -71,6 +71,7 @@\n /git-for-each-ref\n /git-for-each-repo\n /git-format-patch\n+/git-format-rev\n /git-fsck\n /git-fsck-objects\n /git-fsmonitor--daemon\ndiff --git a/Documentation/git-format-rev.adoc b/Documentation/git-format-rev.adoc\nnew file mode 100644\nindex 00000000000..c40d52e9f6d\n--- /dev/null\n+++ b/Documentation/git-format-rev.adoc\n@@ -0,0 +1,215 @@\n+git-format-rev(1)\n+=================\n+\n+NAME\n+----\n+git-format-rev - EXPERIMENTAL: Pretty format revisions on demand\n+\n+\n+SYNOPSIS\n+--------\n+[synopsis]\n+(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> --format=<pretty> [--[no-]notes=<ref>] [-z] [--[no-]null-output] [--[no-]null-input]\n+\n+DESCRIPTION\n+-----------\n+\n+Pretty format revisions from standard input.\n+\n+THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.\n+\n+OPTIONS\n+-------\n+\n+`--stdin-mode=<mode>`::\n+\tHow to interpret standard input data:\n++\n+--\n+`revs`;; Each line or record (see the <<io,INPUT AND OUTPUT FORMATS>>\n+\tsection) is interpreted as a commit. Any kind of revision\n+\texpression can be used (see linkgit:gitrevisions[7]). Annotated\n+\ttags are peeled (see linkgit:gitglossary[7]).\n++\n+The argument `rev` is also accepted.\n+\n+`text`;; Formats all commit object names found in freeform text. These\n+\tmust the full object names, i.e. abbreviated hexidecimal object\n+\tnames will not be interpreted.\n++\n+Anything that is parsed as an object name but that is not found to be a\n+commit object name is left alone (echoed).\n+--\n+\n+`--format=<pretty>`::\n+\tPretty format string.\n+\n+`--notes=<ref>`::\n+`--no-notes`::\n+\tCustom notes ref. Notes are displayed when using the `%N`\n+\tatom. See linkgit:git-notes[1].\n+\n+`-z`::\n+`--null`::\n+\tUse _NUL_ character to terminate both input and output instead\n+\tof newline. This option cannot be negated.\n++\n+This is useful if both the input and output could contain newlines or if\n+the input to this command also uses _NUL_ character termination; see the\n+<<io,INPUT AND OUTPUT FORMATS>> section below.\n++\n+The mode `--stdin-mode=text` can have use for this option when it needs\n+to process input like for example `git last-modified -z`; see the\n+<<examples,EXAMPLES>> section below.\n+\n+`--null-output`::\n+`--no-null-output`::\n+\tUse _NUL_ character to terminate output instead of newline. The\n+\tdefault is `--no-null-output`.\n++\n+This is useful if the output could contain newlines, for example if the\n+`%n` (newline) atom is used.\n+\n+`--null-input`::\n+`--no-null-input`::\n+\tUse _NUL_ character to terminate input instead of newline. The\n+\tdefault is `--no-null-input`.\n++\n+This is useful if the input revision expressions could contain newlines.\n+\n+[[io]]\n+INPUT AND OUTPUT FORMAT\n+-----------------------\n+\n+The command uses newlines for both input and output termination by\n+default. See the `-z`, `--null-output`, and `--null-input` options for\n+using _NUL_ character as the terminator.\n+\n+The mode `--stdin-mode=revs` outputs one formatted commit followed by\n+the terminator. This could either be called a _line_ or a _record_ in\n+case \"line\" is too suggestive of newline termination.\n+\n+Note that this means that the terminator character (newline or _NUL_)\n+acts as a _terminator_, not a _separator_. In other words, the final\n+line or record is also terminated by the terminator character.\n+\n+The mode `--stdin-mode=text` replaces each object name with the\n+formatted commit, i.e. the format `%s` would transform some commit\n+object name to `<subject>` without any termination. Like this:\n+\n+----\n+Did we not fix this in \"<subject>\"?\n+----\n+\n+It is safe to interactively read and write from this command since each\n+record is immediately flushed.\n+\n+[[examples]]\n+EXAMPLES\n+--------\n+\n+The command linkgit:git-last-modified[1] shows the commit that each file\n+was last modified in.\n+\n+----\n+$ git last-modified -- README.md Makefile\n+7798034171030be0909c56377a4e0e10e6d2df93\tMakefile\n+c50fbb2dd225e7e82abba4380423ae105089f4d7\tREADME.md\n+----\n+\n+We can pipe the result to this command in order to replace the object\n+name with the commit author.\n+\n+----\n+$ git last-modified -- README.md Makefile |\n+    git format-rev --stdin-mode=text --format=%an\n+Junio C Hamano\tMakefile\n+Todd Zullinger\tREADME.md\n+----\n+\n+Another example is _formatting commits in commit messages_. Given this commit message:\n+\n+----\n+Fix off-by-one error\n+\n+Fix off-by-one error introduced in\n+e83c5163316f89bfbde7d9ab23ca2e25604af290.\n+\n+We thought we fixed this in 5569bf9bbedd63a00780fc5c110e0cfab3aa97b9 but\n+that only covered 1/3 of the faulty cases.\n+----\n+\n+We can format the commits and use par(1) to reflow the text, say in a\n+`commit-msg` hook:\n+\n+----\n+$ git config set hook.reference-commits.event commit-msg\n+$ git config set hook.reference-commits.command reference-commits\n+$ cat $(which reference-commits)\n+#/bin/sh\n+\n+msg=\"$1\"\n+rewritten=$(mktemp)\n+git format-rev --stdin-mode=text --format=reference <\"$msg\" |\n+    par >\"$rewritten\"\n+mv \"$rewritten\" \"$msg\"\n+----\n+\n+Which will produce something like this:\n+\n+----\n+Fix off-by-one error\n+\n+Fix off-by-one error introduced in e83c5163316 (Implement better memory\n+allocator, 2005-04-07).\n+\n+We thought we fixed this in 5569bf9bbed (Fix memory allocator,\n+2005-06-22) but that only covered 1/3 of the faulty cases.\n+----\n+\n+DISCUSSION\n+----------\n+\n+This command lets you format any number of revisions in any order\n+through one command invocation. Consider the\n+linkgit:git-last-modified[1] case from the <<examples,EXAMPLES>> section\n+above:\n+\n+1. There might be hundreds of files\n+2. Commits can be repeated, i.e. two or more files were last modified in\n+   the same commit\n+\n+Two widely-used commands which pretty formats commits are\n+linkgit:git-log[1] and linkgit:git-show[1]. It turns out that they are\n+not a good fit for the above use case.\n+\n+- The output of linkgit:git-last-modified[1] would have to be processed\n+  in stages since you need to transform the first column separately and\n+  then link the author to the filename. But this is surmountable.\n+- You can feed each commit to `git show` or `git log --no-walk -1`. But\n+  that means that you need to create a process for each line.\n+- Let’s say that you want to use one process, not one per line. So you\n+  want to feed all the commits to the command. Now you face the problem\n+  that you have to feed all the commits to the commands before you get\n+  any output (this is also the case for the `--stdin` modes). In other\n+  words, you cannot loop through each line, get the author for the\n+  commit, and output the author and the filename. You need to feed all\n+  the commits, get back all the output, and match the output with the\n+  filename.\n+- But the next problem is that commands will deduplicate the input and\n+  only output one commit one single time only. Thus you cannot make the\n+  output order match the input order, since a commit could have been\n+  repeated in the original input.\n+\n+In short, it is straightforward to use these two commands if you use one\n+process per line. It is much more work if you just want to use one\n+process, but still doable. In contrast, this problem is solved with just\n+another shell pipeline with this command.\n+\n+SEE ALSO\n+--------\n+linkgit:git-name-rev[1],\n+linkgit:git-log[1].\n+\n+GIT\n+---\n+Part of the linkgit:git[1] suite\ndiff --git a/Documentation/meson.build b/Documentation/meson.build\nindex d6365b888bb..58e7c6a0b8a 100644\n--- a/Documentation/meson.build\n+++ b/Documentation/meson.build\n@@ -55,6 +55,7 @@ manpages = {\n   'git-for-each-ref.adoc' : 1,\n   'git-for-each-repo.adoc' : 1,\n   'git-format-patch.adoc' : 1,\n+  'git-format-rev.adoc' : 1,\n   'git-fsck-objects.adoc' : 1,\n   'git-fsck.adoc' : 1,\n   'git-fsmonitor--daemon.adoc' : 1,\ndiff --git a/Makefile b/Makefile\nindex 15b1ded1a0b..cbaf91fd846 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -895,6 +895,7 @@ BUILT_INS += $(patsubst builtin/%.o,git-%$X,$(BUILTIN_OBJS))\n BUILT_INS += git-cherry$X\n BUILT_INS += git-cherry-pick$X\n BUILT_INS += git-format-patch$X\n+BUILT_INS += git-format-rev$X\n BUILT_INS += git-fsck-objects$X\n BUILT_INS += git-init$X\n BUILT_INS += git-maintenance$X\ndiff --git a/builtin.h b/builtin.h\nindex 235c51f30e5..63813c90125 100644\n--- a/builtin.h\n+++ b/builtin.h\n@@ -189,6 +189,7 @@ int cmd_fmt_merge_msg(int argc, const char **argv, const char *prefix, struct re\n int cmd_for_each_ref(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_for_each_repo(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_format_patch(int argc, const char **argv, const char *prefix, struct repository *repo);\n+int cmd_format_rev(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_fsck(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_fsmonitor__daemon(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_gc(int argc, const char **argv, const char *prefix, struct repository *repo);\ndiff --git a/builtin/name-rev.c b/builtin/name-rev.c\nindex 475efb0b82b..5494b0424b3 100644\n--- a/builtin/name-rev.c\n+++ b/builtin/name-rev.c\n@@ -18,6 +18,10 @@\n #include \"commit-graph.h\"\n #include \"wildmatch.h\"\n #include \"mem-pool.h\"\n+#include \"pretty.h\"\n+#include \"revision.h\"\n+#include \"notes.h\"\n+#include \"write-or-die.h\"\n \n /*\n  * One day.  See the 'name a rev shortly after epoch' test in t6120 when\n@@ -272,14 +276,26 @@ struct name_ref_data {\n \tstruct string_list exclude_filters;\n };\n \n+struct pretty_format {\n+\tstruct pretty_print_context ctx;\n+\tstruct userformat_want want;\n+};\n+\n enum command_type {\n \tNAME_REV = 1,\n+\tFORMAT_REV = 2,\n+};\n+\n+enum stdin_mode {\n+    TEXT = 1,\n+    REVS = 2,\n };\n \n struct command {\n \tenum command_type type;\n \tunion {\n \t\tint name_only;\n+\t\tstruct pretty_format *pretty_format;\n \t} u;\n };\n \n@@ -290,6 +306,13 @@ static void init_name_rev_command(struct command *cmd,\n \tcmd->u.name_only = name_only;\n }\n \n+static void init_format_rev_command(struct command *cmd,\n+\t\t\t\t    struct pretty_format *pretty_format)\n+{\n+\tcmd->type = FORMAT_REV;\n+\tcmd->u.pretty_format = pretty_format;\n+}\n+\n static struct tip_table {\n \tstruct tip_table_entry {\n \t\tstruct object_id oid;\n@@ -495,6 +518,27 @@ static const char *get_rev_name(const struct object *o, struct strbuf *buf)\n \t}\n }\n \n+static const char *get_format_rev(const struct commit *c,\n+\t\t\t\t  struct pretty_format *format_ctx,\n+\t\t\t\t  struct strbuf *buf)\n+{\n+\tstrbuf_reset(buf);\n+\n+\tif (format_ctx->want.notes) {\n+\t\tstruct strbuf notebuf = STRBUF_INIT;\n+\n+\t\tformat_display_notes(&c->object.oid, &notebuf,\n+\t\t\t\t     get_log_output_encoding(),\n+\t\t\t\t     format_ctx->ctx.fmt == CMIT_FMT_USERFORMAT);\n+\t\tformat_ctx->ctx.notes_message = strbuf_detach(&notebuf, NULL);\n+\t}\n+\n+\tpretty_print_commit(&format_ctx->ctx, c, buf);\n+\tFREE_AND_NULL(format_ctx->ctx.notes_message);\n+\n+\treturn buf->buf;\n+}\n+\n static void show_name(const struct object *obj,\n \t\t      const char *caller_name,\n \t\t      int always, int allow_undefined, int name_only)\n@@ -564,6 +608,18 @@ static void name_rev_line(char *p, struct command *cmd)\n \t\t\t\telse\n \t\t\t\t\tprintf(\"%.*s (%s)\", p_len, p_start, name);\n \t\t\t\tbreak;\n+\t\t\tcase FORMAT_REV:\n+\t\t\t\tif (!oid_ret)\n+\t\t\t\t\to = parse_object(the_repository, &oid);\n+\t\t\t\tif (o && o->type == OBJ_COMMIT)\n+\t\t\t\t\tname = get_format_rev((const struct commit *)o,\n+\t\t\t\t\t\t\t      cmd->u.pretty_format,\n+\t\t\t\t\t\t\t      &buf);\n+\t\t\t\tif (name)\n+\t\t\t\t\tprintf(\"%.*s%s\", p_len - hexsz, p_start, name);\n+\t\t\t\telse\n+\t\t\t\t\tprintf(\"%.*s\", p_len, p_start);\n+\t\t\t\tbreak;\n \t\t\tdefault:\n \t\t\t\tBUG(\"uncovered case: %d\", cmd->type);\n \t\t\t}\n@@ -717,3 +773,170 @@ int cmd_name_rev(int argc,\n \tobject_array_clear(&revs);\n \treturn 0;\n }\n+\n+struct format_nul_data {\n+\tbool nul_input;\n+\tbool nul_output;\n+};\n+\n+static int format_nul_cb(const struct option *option,\n+\t\t\t const char *arg,\n+\t\t\t int unset)\n+{\n+\tstruct format_nul_data *data = option->value;\n+\tdata->nul_input = 1;\n+\tdata->nul_output = 1;\n+\tBUG_ON_OPT_NEG(unset);\n+\tBUG_ON_OPT_ARG(arg);\n+\treturn 0;\n+}\n+\n+static enum stdin_mode parse_stdin_mode(const char *stdin_mode)\n+{\n+\tif (!strcmp(stdin_mode, \"text\"))\n+\t\treturn TEXT;\n+\telse if (!strcmp(stdin_mode, \"revs\") ||\n+\t\t !strcmp(stdin_mode, \"rev\"))\n+\t\treturn REVS;\n+\telse\n+\t\tdie(_(\"'%s' needs to be either text, revs, or rev\"),\n+\t\t    \"--stdin-mode\");\n+}\n+\n+static char const *const format_rev_usage[] = {\n+\tN_(\"(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> \"\n+\t   \"--format=<pretty> [--[no-]notes=<ref>] \"\n+\t   \"[-z] [--[no-]null-output] [--[no-]null-input]\"),\n+\tNULL\n+};\n+\n+int cmd_format_rev(int argc,\n+\t\t   const char **argv,\n+\t\t   const char *prefix,\n+\t\t   struct repository *repo UNUSED)\n+{\n+\tconst char *format = NULL;\n+\tenum stdin_mode stdin_mode;\n+\tconst char *stdin_mode_arg = NULL;\n+\tstruct format_nul_data nul_data = { 0, 0 };\n+\tchar output_terminator;\n+\tstrbuf_getline_fn getline_fn;\n+\tstruct display_notes_opt format_notes_opt;\n+\tstruct rev_info format_rev = REV_INFO_INIT;\n+\tstruct pretty_format format_pp = { 0 };\n+\tstruct string_list notes = STRING_LIST_INIT_NODUP;\n+\tstruct strbuf scratch_buf = STRBUF_INIT;\n+\tstruct command cmd;\n+\tstruct option opts[] = {\n+\t\tOPT_STRING(0, \"format\", &format, N_(\"format\"),\n+\t\t\t   N_(\"pretty format to use\")),\n+\t\tOPT_STRING(0, \"stdin-mode\", &stdin_mode_arg, N_(\"stdin-mode\"),\n+\t\t\t   N_(\"how revs are processed\")),\n+\t\tOPT_STRING_LIST(0, \"notes\", &notes, N_(\"notes\"),\n+\t\t\t\tN_(\"display notes for pretty format\")),\n+\t\tOPT_CALLBACK_F('z', \"null\", &nul_data, N_(\"z\"),\n+\t\t\t       N_(\"Use NUL for input and output termination\"),\n+\t\t\t       PARSE_OPT_NOARG | PARSE_OPT_NONEG, format_nul_cb),\n+\t\tOPT_BOOL(0, \"null-input\", &nul_data.nul_input,\n+\t\t\t N_(\"Use NUL for input termination\")),\n+\t\tOPT_BOOL(0, \"null-output\", &nul_data.nul_output,\n+\t\t\t N_(\"Use NUL for output termination\")),\n+\t\tOPT_END(),\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, opts, format_rev_usage, 0);\n+\n+\tif (argc > 0) {\n+\t\terror(_(\"too many arguments\"));\n+\t\tusage_with_options(format_rev_usage, opts);\n+\t}\n+\n+\tif (!format)\n+\t\tdie(_(\"'%s' is required\"), \"--format\");\n+\tif (!stdin_mode_arg)\n+\t\tdie(_(\"'%s' is required\"), \"--stdin-mode\");\n+\n+\tgetline_fn = nul_data.nul_input ? strbuf_getline_nul : strbuf_getline_lf;\n+\toutput_terminator = nul_data.nul_output ? '\\0' : '\\n';\n+\n+\tinit_display_notes(&format_notes_opt);\n+\tstdin_mode = parse_stdin_mode(stdin_mode_arg);\n+\n+\tget_commit_format(format, &format_rev);\n+\tformat_pp.ctx.rev = &format_rev;\n+\tformat_pp.ctx.fmt = format_rev.commit_format;\n+\tformat_pp.ctx.abbrev = format_rev.abbrev;\n+\tformat_pp.ctx.date_mode_explicit = format_rev.date_mode_explicit;\n+\tformat_pp.ctx.date_mode = format_rev.date_mode;\n+\tformat_pp.ctx.color = GIT_COLOR_AUTO;\n+\n+\tuserformat_find_requirements(format,\n+\t\t\t\t     &format_pp.want);\n+\tif (format_pp.want.notes) {\n+\t\tint ignore_show_notes = 0;\n+\t\tstruct string_list_item *n;\n+\n+\t\tfor_each_string_list_item(n, &notes)\n+\t\t\tenable_ref_display_notes(&format_notes_opt,\n+\t\t\t\t\t\t &ignore_show_notes,\n+\t\t\t\t\t\t n->string);\n+\t\tload_display_notes(&format_notes_opt);\n+\t}\n+\n+\tinit_format_rev_command(&cmd, &format_pp);\n+\n+\tswitch (stdin_mode) {\n+\tcase TEXT:\n+\t\twhile (getline_fn(&scratch_buf, stdin) != EOF) {\n+\t\t\tname_rev_line(scratch_buf.buf, &cmd);\n+\t\t\t/*\n+\t\t\t * We do not pass on the terminator to name_rev_line,\n+\t\t\t * unlike name-rev.\n+\t\t\t */\n+\t\t\tprintf(\"%c\", output_terminator);\n+\t\t\tmaybe_flush_or_die(stdout, \"stdout\");\n+\t\t}\n+\t\tbreak;\n+\tcase REVS:\n+\t\twhile (getline_fn(&scratch_buf, stdin) != EOF) {\n+\t\t\tstruct object_id oid;\n+\t\t\tstruct object *object;\n+\t\t\tstruct object *peeled;\n+\n+\t\t\tif (repo_get_oid(the_repository, scratch_buf.buf, &oid)) {\n+\t\t\t\tfprintf(stderr, \"Could not get object name for %s. Skipping.\\n\",\n+\t\t\t\t\tscratch_buf.buf);\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\tobject = parse_object(the_repository, &oid);\n+\t\t\tif (!object) {\n+\t\t\t\tfprintf(stderr, \"Could not get object for %s. Skipping.\\n\",\n+\t\t\t\t\tscratch_buf.buf);\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\tpeeled = deref_tag(the_repository, object, scratch_buf.buf, 0);\n+\t\t\tif (!peeled || peeled->type != OBJ_COMMIT) {\n+\t\t\t\tfprintf(stderr,\n+\t\t\t\t\t\"Could not get commit for %s. Skipping.\\n\",\n+\t\t\t\t\tscratch_buf.buf);\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\tget_format_rev((struct commit *)peeled,\n+\t\t\t\t       &format_pp, &scratch_buf);\n+\t\t\tprintf(\"%s%c\", scratch_buf.buf, output_terminator);\n+\t\t\tmaybe_flush_or_die(stdout, \"stdout\");\n+\t\t\tstrbuf_release(&scratch_buf);\n+\t\t}\n+\t\tbreak;\n+\tdefault:\n+\t\tBUG(\"uncovered case: %d\", stdin_mode);\n+\t}\n+\n+\tstrbuf_release(&scratch_buf);\n+\tstring_list_clear(&notes, 0);\n+\trelease_display_notes(&format_notes_opt);\n+\treturn 0;\n+}\ndiff --git a/command-list.txt b/command-list.txt\nindex f9005cf4597..df729872dca 100644\n--- a/command-list.txt\n+++ b/command-list.txt\n@@ -108,6 +108,7 @@ git-fmt-merge-msg                       purehelpers\n git-for-each-ref                        plumbinginterrogators\n git-for-each-repo                       plumbinginterrogators\n git-format-patch                        mainporcelain\n+git-format-rev                          plumbinginterrogators\n git-fsck                                ancillaryinterrogators          complete\n git-gc                                  mainporcelain\n git-get-tar-commit-id                   plumbinginterrogators\ndiff --git a/git.c b/git.c\nindex 2b212e6675d..af5b0422b00 100644\n--- a/git.c\n+++ b/git.c\n@@ -578,6 +578,7 @@ static struct cmd_struct commands[] = {\n \t{ \"for-each-ref\", cmd_for_each_ref, RUN_SETUP },\n \t{ \"for-each-repo\", cmd_for_each_repo, RUN_SETUP_GENTLY },\n \t{ \"format-patch\", cmd_format_patch, RUN_SETUP },\n+\t{ \"format-rev\", cmd_format_rev, RUN_SETUP },\n \t{ \"fsck\", cmd_fsck, RUN_SETUP },\n \t{ \"fsck-objects\", cmd_fsck, RUN_SETUP },\n \t{ \"fsmonitor--daemon\", cmd_fsmonitor__daemon, RUN_SETUP },\ndiff --git a/t/t1517-outside-repo.sh b/t/t1517-outside-repo.sh\nindex c824c1a25cf..360a9323343 100755\n--- a/t/t1517-outside-repo.sh\n+++ b/t/t1517-outside-repo.sh\n@@ -114,7 +114,8 @@ do\n \tarchimport | citool | credential-netrc | credential-libsecret | \\\n \tcredential-osxkeychain | cvsexportcommit | cvsimport | cvsserver | \\\n \tdaemon | \\\n-\tdifftool--helper | filter-branch | fsck-objects | get-tar-commit-id | \\\n+\tdifftool--helper | filter-branch | format-rev | fsck-objects | \\\n+\tget-tar-commit-id | \\\n \tgui | gui--askpass | \\\n \thttp-backend | http-fetch | http-push | init-db | \\\n \tmerge-octopus | merge-one-file | merge-resolve | mergetool | \\\ndiff --git a/t/t6120-describe.sh b/t/t6120-describe.sh\nindex 62789f76381..8ee3d2c37d0 100755\n--- a/t/t6120-describe.sh\n+++ b/t/t6120-describe.sh\n@@ -801,4 +801,198 @@ test_expect_success 'do not be fooled by invalid describe format ' '\n \ttest_must_fail git cat-file -t \"refs/tags/super-invalid/./../...../ ~^:/?*[////\\\\\\\\\\\\&}/busted.lock-42-g\"$(cat out)\n '\n \n+test_expect_success 'setup: format-rev' '\n+\tmkdir repo-format &&\n+\tgit -C repo-format init &&\n+\ttest_commit -C repo-format first &&\n+\ttest_commit -C repo-format second &&\n+\ttest_commit -C repo-format third &&\n+\ttest_commit -C repo-format fourth &&\n+\ttest_commit -C repo-format fifth &&\n+\ttest_commit -C repo-format sixth &&\n+\ttest_commit -C repo-format seventh &&\n+\ttest_commit -C repo-format eighth\n+'\n+\n+test_expect_success 'format-rev --stdin-mode=revs' '\n+\tcat >expect <<-\\EOF &&\n+\teighth\n+\tseventh\n+\tfifth\n+\tEOF\n+\tgit -C repo-format format-rev --stdin-mode=revs \\\n+\t\t--format=%s >actual <<-\\EOF &&\n+\tHEAD\n+\tHEAD~\n+\tHEAD~3\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'format-rev --stdin-mode=text from rev-list same as log' '\n+\tgit -C repo-format log --format=reference >expect &&\n+\ttest_file_not_empty expect &&\n+\tgit -C repo-format rev-list HEAD >list &&\n+\tgit -C repo-format format-rev --stdin-mode=text \\\n+\t\t--format=reference <list >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'format-rev --stdin-mode=text with running text and tree oid' '\n+\tcmit_oid=$(git -C repo-format rev-parse fifth) &&\n+\treference=$(git -C repo-format log -n1 --format=reference fifth) &&\n+\ttree=$(git -C repo-format rev-parse HEAD^{tree}) &&\n+\tcat >expect <<-EOF &&\n+\tWe thought we fixed this in ${reference}.\n+\tBut look at this tree: ${tree}.\n+\tEOF\n+\tgit -C repo-format format-rev --stdin-mode=text --format=reference \\\n+\t\t>actual <<-EOF &&\n+\tWe thought we fixed this in ${cmit_oid}.\n+\tBut look at this tree: ${tree}.\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'format-rev with %N (note)' '\n+\ttest_when_finished \"git -C repo-format notes remove\" &&\n+\tgit -C repo-format notes add -m\"Make a note\" &&\n+\tprintf \"Make a note\\n\\n\\n\" >expect &&\n+\tgit -C repo-format format-rev --stdin-mode=revs \\\n+\t\t--format=\"tformat:%N\" \\\n+\t\t>actual <<-\\EOF &&\n+\tHEAD\n+\tHEAD~\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'format-rev --notes<ref> (custom notes ref)' '\n+\t# One custom notes ref\n+\ttest_when_finished \"git -C repo-format notes remove\" &&\n+\ttest_when_finished \"git -C repo-format notes --ref=word remove\" &&\n+\tgit -C repo-format notes add -m\"default\" &&\n+\tgit -C repo-format notes --ref=word add -m\"custom\" &&\n+\tprintf \"custom\\n\\n\" >expect &&\n+\tgit -C repo-format format-rev --stdin-mode=revs \\\n+\t\t--format=\"tformat:%N\" \\\n+\t\t--notes=word \\\n+\t\t>actual <<-\\EOF &&\n+\tHEAD\n+\tEOF\n+\ttest_cmp expect actual &&\n+\t# Glob all\n+\tprintf \"default\\ncustom\\n\\n\" >expect &&\n+\tgit -C repo-format format-rev --stdin-mode=revs \\\n+\t\t--format=\"tformat:%N\" \\\n+\t\t--notes=* >actual <<-\\EOF &&\n+\tHEAD\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'format-rev --stdin-mode=revs on annotated tag peels to commit' '\n+\ttest_when_finished \"git -C repo-format tag -d version\" &&\n+\tgit -C repo-format tag -a -m\"new version\" version &&\n+\tcat >expect <<-\\EOF &&\n+\teighth\n+\tEOF\n+\tgit -C repo-format format-rev --stdin-mode=revs \\\n+\t\t--format=%s \\\n+\t\t>actual <<-\\EOF &&\n+\tversion\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'format-rev --stdin-mode=revs lookup failures' '\n+\ttest_when_finished \"git -C repo-format tag -d tag-to-tree\" &&\n+\tinvalid_syntax=not-valid &&\n+\tnon_existing_oid=${EMPTY_BLOB} &&\n+\ttree=$(git -C repo-format rev-parse eighth^{tree}) &&\n+\tgit -C repo-format tag -a -mmessage tag-to-tree \"$tree\" &&\n+\ttag_to_tree=$(git -C repo-format rev-parse tag-to-tree) &&\n+\tcat >expect <<-EOF &&\n+\tCould not get object name for ${invalid_syntax}. Skipping.\n+\tCould not get object for ${non_existing_oid}. Skipping.\n+\tCould not get commit for ${tree}. Skipping.\n+\tCould not get commit for ${tag_to_tree}. Skipping.\n+\tEOF\n+\tgit -C repo-format format-rev --stdin-mode=revs \\\n+\t\t--format=%s \\\n+\t\t2>actual >out <<-EOF &&\n+\t${invalid_syntax}\n+\t${non_existing_oid}\n+\t${tree}\n+\t${tag_to_tree}\n+\tEOF\n+\ttest_line_count = 0 out &&\n+\ttest_cmp expect actual\n+'\n+\n+\n+test_expect_success 'format-rev -z --stdin-mode=text with object name lookup failures' '\n+\tprintf \"%s\\0\" \"$(git -C repo-format rev-parse HEAD)\" >input &&\n+\tprintf \"%s\\0\" \"$(git -C repo-format rev-parse HEAD^{tree})\" >>input &&\n+\tprintf \"%s\\0\" \"$EMPTY_BLOB\" >>input &&\n+\tprintf \"%s\\0\" \"$(git -C repo-format log --format=%s -1)\" >expect &&\n+\tprintf \"%s\\0\" \"$(git -C repo-format rev-parse HEAD^{tree})\" >>expect &&\n+\tprintf \"%s\\0\" \"$EMPTY_BLOB\" >>expect &&\n+\tgit -C repo-format format-rev --stdin-mode=text \\\n+\t\t--format=%s -z <input >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup: format-rev input and output separators' '\n+\tgit -C repo-format rev-list HEAD >input-lf &&\n+\tgit -C repo-format rev-list -z HEAD >input-nul &&\n+\tgit -C repo-format log --format=%s >output-lf &&\n+\tgit -C repo-format log -z --format=%s >output-nul &&\n+\techo revs >stdin-modes &&\n+\techo text >>stdin-modes\n+'\n+\n+while read mode\n+do\n+\ttest_expect_success \"format-rev -z --stdin-mode=$mode\" '\n+\t\tcat output-nul >expect &&\n+\t\tgit -C repo-format format-rev --stdin-mode=\"$mode\" \\\n+\t\t\t--format=%s -z <input-nul >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"format-rev -z --no-null-input --no-null-output --stdin-mode=$mode\" '\n+\t\tcat output-lf >expect &&\n+\t\tgit -C repo-format format-rev --stdin-mode=\"$mode\" \\\n+\t\t\t--format=%s -z --no-null-input --no-null-output \\\n+\t\t\t<input-lf >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"format-rev ---null-input --stdin-mode=$mode\" '\n+\t\tcat output-lf >expect &&\n+\t\tgit -C repo-format format-rev --stdin-mode=\"$mode\" \\\n+\t\t\t--format=%s --null-input \\\n+\t\t\t<input-nul >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"format-rev --null-output --stdin-mode=$mode\" '\n+\t\tcat output-nul >expect &&\n+\t\tgit -C repo-format format-rev --stdin-mode=\"$mode\" \\\n+\t\t\t--format=%s --null-output \\\n+\t\t\t<input-lf >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+\n+\ttest_expect_success \"format-rev -z --stdin-mode=$mode with multi-line output\" '\n+\t\tformat=\"%s%n%aI\" &&\n+\t\tgit -C repo-format log -z --format=\"$format\" \\\n+\t\t\t>expect &&\n+\t\tgit -C repo-format format-rev --stdin-mode=\"$mode\" \\\n+\t\t\t--format=\"$format\" -z <input-nul >actual &&\n+\t\ttest_cmp expect actual\n+\t'\n+done <stdin-modes\n+\n test_done\n-- \n2.54.0.13.g9c7419e39f8\n\n"}]}