* [PATCH 0/2] name-rev: learn --format=<pretty>
@ 2026-03-13 16:03 kristofferhaugsbakk
2026-03-13 16:03 ` [PATCH 1/2] name-rev: wrap both blocks in braces kristofferhaugsbakk
` (2 more replies)
0 siblings, 3 replies; 45+ messages in thread
From: kristofferhaugsbakk @ 2026-03-13 16:03 UTC (permalink / raw)
To: git; +Cc: Kristoffer Haugsbakk
From: Kristoffer Haugsbakk <code@khaugsbakk.name>
Topic name: kh/name-rev-pretty-format
Topic summary: Teach git-name-rev(1) a mode to pretty format revisions
instead of outputting symbolic names.
(See the second patch for details.)
The first patch is just for `CodingGuidelines`. Unrelated.
I have gone through three approaches: pretty print, `log-tree:
log_tree_commit`, and pretty print again. At first I was confused
when using `pretty_print_commit` because I couldn’t seem to get the same
output as git-log(1):
# for git-log(1)
git log --format=<pretty>
# for git-name-rev(1)
git rev-list HEAD |
git name-rev --format=<pretty> --annotate-stdin
Because there were some minor differences for a few things I tried:
• fuller: log has the `commit <commit>` header; pretty does not
• oneline: log has the oid; pretty does not
Then I tried `log_tree_commit`. But then I got some things that I didn’t
want. This function also didn’t fit in with the git-name-rev(1) processing
since it just dumps straight to standard out instead of allowing you to
accumulate things in a scratch buffer (strbuf). So then I went back to the
`pretty_print_commit` approach.
Notes are handled by reading the display refs. I can imagine that it would
be better for this command to use `--[no-]notes` arguments like the ones
that git-log(1) has for explicit control (without using env. variables).
[1/2] name-rev: wrap both blocks in braces
[2/2] name-rev: learn --format=<pretty>
Documentation/git-name-rev.adoc | 9 +++-
builtin/name-rev.c | 85 ++++++++++++++++++++++++++++-----
t/t6120-describe.sh | 58 ++++++++++++++++++++++
3 files changed, 139 insertions(+), 13 deletions(-)
base-commit: 67006b9db8b772423ad0706029286096307d2567
--
2.53.0.32.gf6228eaf9cc
^ permalink raw reply [flat|nested] 45+ messages in thread
* [PATCH 1/2] name-rev: wrap both blocks in braces
2026-03-13 16:03 [PATCH 0/2] name-rev: learn --format=<pretty> kristofferhaugsbakk
@ 2026-03-13 16:03 ` kristofferhaugsbakk
2026-03-14 0:22 ` Junio C Hamano
2026-03-13 16:03 ` [PATCH 2/2] name-rev: learn --format=<pretty> kristofferhaugsbakk
2026-03-20 13:09 ` [PATCH v2 0/2] " kristofferhaugsbakk
2 siblings, 1 reply; 45+ messages in thread
From: kristofferhaugsbakk @ 2026-03-13 16:03 UTC (permalink / raw)
To: git; +Cc: Kristoffer Haugsbakk
From: Kristoffer Haugsbakk <code@khaugsbakk.name>
See `CodingGuidelines`:
- When there are multiple arms to a conditional and some of them
require braces, enclose even a single line block in braces for
consistency. [...]
Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
---
builtin/name-rev.c | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/builtin/name-rev.c b/builtin/name-rev.c
index 6188cf98ce0..9d2774f3723 100644
--- a/builtin/name-rev.c
+++ b/builtin/name-rev.c
@@ -466,9 +466,9 @@ static const char *get_rev_name(const struct object *o, struct strbuf *buf)
if (!n)
return NULL;
- if (!n->generation)
+ if (!n->generation) {
return n->tip_name;
- else {
+ } else {
strbuf_reset(buf);
strbuf_addstr(buf, n->tip_name);
strbuf_strip_suffix(buf, "^0");
@@ -516,10 +516,10 @@ static void name_rev_line(char *p, struct name_ref_data *data)
for (p_start = p; *p; p++) {
#define ishex(x) (isdigit((x)) || ((x) >= 'a' && (x) <= 'f'))
- if (!ishex(*p))
+ if (!ishex(*p)) {
counter = 0;
- else if (++counter == hexsz &&
- !ishex(*(p+1))) {
+ } else if (++counter == hexsz &&
+ !ishex(*(p + 1))) {
struct object_id oid;
const char *name = NULL;
char c = *(p+1);
--
2.53.0.32.gf6228eaf9cc
^ permalink raw reply related [flat|nested] 45+ messages in thread
* [PATCH 2/2] name-rev: learn --format=<pretty>
2026-03-13 16:03 [PATCH 0/2] name-rev: learn --format=<pretty> kristofferhaugsbakk
2026-03-13 16:03 ` [PATCH 1/2] name-rev: wrap both blocks in braces kristofferhaugsbakk
@ 2026-03-13 16:03 ` kristofferhaugsbakk
2026-03-14 0:22 ` Junio C Hamano
2026-03-20 13:09 ` [PATCH v2 0/2] " kristofferhaugsbakk
2 siblings, 1 reply; 45+ messages in thread
From: kristofferhaugsbakk @ 2026-03-13 16:03 UTC (permalink / raw)
To: git; +Cc: Kristoffer Haugsbakk
From: Kristoffer Haugsbakk <code@khaugsbakk.name>
Teach git-name-rev(1) to format the given revisions instead of creating
symbolic names.
Sometimes you want to format commits. Most of the time you’re walking
the graph, e.g. getting a range of commits like `master..topic`. That’s
a job for git-log(1).
But sometimes you might want to format commits that you encounter
on demand:
• Full hashes in running text that you might want to pretty-print
• git-last-modified(1) outputs full hashes that you can do the same with
• git-cherry(1) has `-v` for commit subject, but maybe you want
something else?
But now you can’t use git-log(1), git-show(1), or git-rev-list(1):
• You can’t feed commits piecemeal to these commands, one input for one
output; they block until standard in is closed
• You can’t feed a list of possibly duplicate commits, like the output
of git-last-modified(1); they effectively deduplicate the output
Beyond these two points there’s also the input massage problem: you
cannot feed mixed input (revisions mixed with arbitrary text).
One might hope that git-cat-file(1) can save us. But it doesn’t support
pretty formats.
But there is one command that already both handles revisions as
arguments, revisions on standard input, and even revisions mixed
in with arbitrary text. Namely git-name-rev(1).
Teach it to work in a format mode where the output for each revision is
the pretty output (implies `--name-only`). This can be used to format
any revision expression when given as arguments, and all full commit
hashes in running text on stdin.
Just bring the hashes (to the pipeline). We will pretty print them.
Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
---
Documentation/git-name-rev.adoc | 9 +++-
builtin/name-rev.c | 75 ++++++++++++++++++++++++++++++---
t/t6120-describe.sh | 58 +++++++++++++++++++++++++
3 files changed, 134 insertions(+), 8 deletions(-)
diff --git a/Documentation/git-name-rev.adoc b/Documentation/git-name-rev.adoc
index d4f1c4d5945..8f050cd4763 100644
--- a/Documentation/git-name-rev.adoc
+++ b/Documentation/git-name-rev.adoc
@@ -9,7 +9,7 @@ git-name-rev - Find symbolic names for given revs
SYNOPSIS
--------
[verse]
-'git name-rev' [--tags] [--refs=<pattern>]
+'git name-rev' [--tags] [--refs=<pattern>] [--format=<pretty>]
( --all | --annotate-stdin | <commit-ish>... )
DESCRIPTION
@@ -21,6 +21,13 @@ format parsable by 'git rev-parse'.
OPTIONS
-------
+--format=<pretty>::
+--no-format::
+ Format revisions instead of outputting symbolic names. The
+ default is `--no-format`.
++
+Implies `--name-only`.
+
--tags::
Do not use branch names, but only tags to name the commits
diff --git a/builtin/name-rev.c b/builtin/name-rev.c
index 9d2774f3723..30d981104c6 100644
--- a/builtin/name-rev.c
+++ b/builtin/name-rev.c
@@ -18,6 +18,9 @@
#include "commit-graph.h"
#include "wildmatch.h"
#include "mem-pool.h"
+#include "pretty.h"
+#include "revision.h"
+#include "notes.h"
/*
* One day. See the 'name a rev shortly after epoch' test in t6120 when
@@ -33,6 +36,11 @@ struct rev_name {
int from_tag;
};
+struct pretty_format {
+ struct pretty_print_context ctx;
+ struct userformat_want want;
+};
+
define_commit_slab(commit_rev_name, struct rev_name);
static timestamp_t generation_cutoff = GENERATION_NUMBER_INFINITY;
@@ -454,7 +462,9 @@ static const char *get_exact_ref_match(const struct object *o)
}
/* may return a constant string or use "buf" as scratch space */
-static const char *get_rev_name(const struct object *o, struct strbuf *buf)
+static const char *get_rev_name(const struct object *o,
+ struct pretty_format *format_ctx,
+ struct strbuf *buf)
{
struct rev_name *n;
const struct commit *c;
@@ -462,6 +472,25 @@ static const char *get_rev_name(const struct object *o, struct strbuf *buf)
if (o->type != OBJ_COMMIT)
return get_exact_ref_match(o);
c = (const struct commit *) o;
+
+ if (format_ctx) {
+ strbuf_reset(buf);
+
+ if (format_ctx->want.notes) {
+ struct strbuf notebuf = STRBUF_INIT;
+
+ format_display_notes(&c->object.oid, ¬ebuf,
+ get_log_output_encoding(),
+ format_ctx->ctx.fmt == CMIT_FMT_USERFORMAT);
+ format_ctx->ctx.notes_message = strbuf_detach(¬ebuf, NULL);
+ }
+
+ pretty_print_commit(&format_ctx->ctx, c, buf);
+ free(format_ctx->ctx.notes_message);
+
+ return buf->buf;
+ }
+
n = get_commit_rev_name(c);
if (!n)
return NULL;
@@ -479,6 +508,7 @@ static const char *get_rev_name(const struct object *o, struct strbuf *buf)
static void show_name(const struct object *obj,
const char *caller_name,
+ struct pretty_format *format_ctx,
int always, int allow_undefined, int name_only)
{
const char *name;
@@ -487,7 +517,7 @@ static void show_name(const struct object *obj,
if (!name_only)
printf("%s ", caller_name ? caller_name : oid_to_hex(oid));
- name = get_rev_name(obj, &buf);
+ name = get_rev_name(obj, format_ctx, &buf);
if (name)
printf("%s\n", name);
else if (allow_undefined)
@@ -507,7 +537,9 @@ static char const * const name_rev_usage[] = {
NULL
};
-static void name_rev_line(char *p, struct name_ref_data *data)
+static void name_rev_line(char *p,
+ struct name_ref_data *data,
+ struct pretty_format *format_ctx)
{
struct strbuf buf = STRBUF_INIT;
int counter = 0;
@@ -532,7 +564,7 @@ static void name_rev_line(char *p, struct name_ref_data *data)
struct object *o =
lookup_object(the_repository, &oid);
if (o)
- name = get_rev_name(o, &buf);
+ name = get_rev_name(o, format_ctx, &buf);
}
*(p+1) = c;
@@ -567,6 +599,10 @@ int cmd_name_rev(int argc,
#endif
int all = 0, annotate_stdin = 0, allow_undefined = 1, always = 0, peel_tag = 0;
struct name_ref_data data = { 0, 0, STRING_LIST_INIT_NODUP, STRING_LIST_INIT_NODUP };
+ const char *format = NULL;
+ struct rev_info format_rev = REV_INFO_INIT;
+ struct pretty_format *format_ctx = NULL;
+ struct pretty_format format_pp = {0};
struct option opts[] = {
OPT_BOOL(0, "name-only", &data.name_only, N_("print only ref-based names (no object names)")),
OPT_BOOL(0, "tags", &data.tags_only, N_("only use tags to name the commits")),
@@ -584,6 +620,8 @@ int cmd_name_rev(int argc,
PARSE_OPT_HIDDEN),
#endif /* WITH_BREAKING_CHANGES */
OPT_BOOL(0, "annotate-stdin", &annotate_stdin, N_("annotate text from stdin")),
+ OPT_STRING(0, "format", &format, N_("format"),
+ "pretty-print output instead"),
OPT_BOOL(0, "undefined", &allow_undefined, N_("allow to print `undefined` names (default)")),
OPT_BOOL(0, "always", &always,
N_("show abbreviated commit object as fallback")),
@@ -606,6 +644,29 @@ int cmd_name_rev(int argc,
}
#endif
+ if (format) {
+ struct pretty_print_context ctx = {0};
+ struct userformat_want want = {0};
+
+ get_commit_format(format, &format_rev);
+ ctx.rev = &format_rev;
+ ctx.fmt = format_rev.commit_format;
+ ctx.abbrev = format_rev.abbrev;
+ ctx.date_mode_explicit = format_rev.date_mode_explicit;
+ ctx.date_mode = format_rev.date_mode;
+ ctx.color = GIT_COLOR_AUTO;
+ format_pp.ctx = ctx;
+
+ userformat_find_requirements(format, &want);
+ if (want.notes)
+ load_display_notes(NULL);
+
+ format_pp.want = want;
+ format_ctx = &format_pp;
+
+ data.name_only = true;
+ }
+
if (all + annotate_stdin + !!argc > 1) {
error("Specify either a list, or --all, not both!");
usage_with_options(name_rev_usage, opts);
@@ -663,7 +724,7 @@ int cmd_name_rev(int argc,
while (strbuf_getline(&sb, stdin) != EOF) {
strbuf_addch(&sb, '\n');
- name_rev_line(sb.buf, &data);
+ name_rev_line(sb.buf, &data, format_ctx);
}
strbuf_release(&sb);
} else if (all) {
@@ -674,13 +735,13 @@ int cmd_name_rev(int argc,
struct object *obj = get_indexed_object(the_repository, i);
if (!obj || obj->type != OBJ_COMMIT)
continue;
- show_name(obj, NULL,
+ show_name(obj, NULL, format_ctx,
always, allow_undefined, data.name_only);
}
} else {
int i;
for (i = 0; i < revs.nr; i++)
- show_name(revs.objects[i].item, revs.objects[i].name,
+ show_name(revs.objects[i].item, revs.objects[i].name, format_ctx,
always, allow_undefined, data.name_only);
}
diff --git a/t/t6120-describe.sh b/t/t6120-describe.sh
index 2c70cc561ad..6dba392d343 100755
--- a/t/t6120-describe.sh
+++ b/t/t6120-describe.sh
@@ -658,6 +658,64 @@ test_expect_success 'name-rev --annotate-stdin works with commitGraph' '
)
'
+test_expect_success 'name-rev --format setup' '
+ mkdir repo-format &&
+ git -C repo-format init &&
+ test_commit -C repo-format first &&
+ test_commit -C repo-format second &&
+ test_commit -C repo-format third &&
+ test_commit -C repo-format fourth &&
+ test_commit -C repo-format fifth &&
+ test_commit -C repo-format sixth &&
+ test_commit -C repo-format seventh &&
+ test_commit -C repo-format eighth
+'
+
+test_expect_success 'name-rev --format=%s for argument revs' '
+ cat >expect <<-\EOF &&
+ eighth
+ seventh
+ fifth
+ EOF
+ git -C repo-format name-rev --format=%s \
+ HEAD HEAD~ HEAD~3 >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success '--name-rev --format=<pretty> --annotate-stdin from rev-list same as log' '
+ git -C repo-format log --format=reference >expect &&
+ test_file_not_empty expect &&
+ git -C repo-format rev-list HEAD >list &&
+ git -C repo-format name-rev --format=reference \
+ --annotate-stdin <list >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success '--name-rev --format=<pretty> --annotate-stdin with running text and tree oid' '
+ cmit_oid=$(git -C repo-format rev-parse :/fifth) &&
+ reference=$(git -C repo-format log -n1 --format=reference :/fifth) &&
+ tree=$(git -C repo-format rev-parse HEAD^{tree}) &&
+ cat >expect <<-EOF &&
+ We thought we fixed this in ${reference}.
+ But look at this tree: ${tree}.
+ EOF
+ git -C repo-format name-rev --format=reference --annotate-stdin \
+ >actual <<-EOF &&
+ We thought we fixed this in ${cmit_oid}.
+ But look at this tree: ${tree}.
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success '--name-rev --format=<pretty> with a note' '
+ test_when_finished "git -C repo-format notes remove" &&
+ git -C repo-format notes add -m"Make a note" &&
+ printf "Make a note\n\n\n" >expect &&
+ git -C repo-format name-rev --format="tformat:%N" \
+ HEAD HEAD~ >actual &&
+ test_cmp expect actual
+'
+
# B
# o
# H \
--
2.53.0.32.gf6228eaf9cc
^ permalink raw reply related [flat|nested] 45+ messages in thread
* Re: [PATCH 1/2] name-rev: wrap both blocks in braces
2026-03-13 16:03 ` [PATCH 1/2] name-rev: wrap both blocks in braces kristofferhaugsbakk
@ 2026-03-14 0:22 ` Junio C Hamano
2026-03-17 22:10 ` Kristoffer Haugsbakk
0 siblings, 1 reply; 45+ messages in thread
From: Junio C Hamano @ 2026-03-14 0:22 UTC (permalink / raw)
To: kristofferhaugsbakk; +Cc: git, Kristoffer Haugsbakk
kristofferhaugsbakk@fastmail.com writes:
> - else if (++counter == hexsz &&
> - !ishex(*(p+1))) {
> + } else if (++counter == hexsz &&
> + !ishex(*(p + 1))) {
> struct object_id oid;
> const char *name = NULL;
> char c = *(p+1);
You are correcting "p+1" to "p + 1" to honor our coding style in a
few lines above "while at it", but there are three others in the
same block (we can see one of them in the post-context), which means
these are now inconsistent. Fixing all of them would make it a far
larger change than qualifies as a "while at it" change. Either make
it another step that is an unrelated clean up, or leave it as-is.
The primary thrust of this patch does make sense and is executed
well.
Thanks.
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [PATCH 2/2] name-rev: learn --format=<pretty>
2026-03-13 16:03 ` [PATCH 2/2] name-rev: learn --format=<pretty> kristofferhaugsbakk
@ 2026-03-14 0:22 ` Junio C Hamano
2026-03-17 22:07 ` Kristoffer Haugsbakk
0 siblings, 1 reply; 45+ messages in thread
From: Junio C Hamano @ 2026-03-14 0:22 UTC (permalink / raw)
To: kristofferhaugsbakk; +Cc: git, Kristoffer Haugsbakk
kristofferhaugsbakk@fastmail.com writes:
> diff --git a/Documentation/git-name-rev.adoc b/Documentation/git-name-rev.adoc
> index d4f1c4d5945..8f050cd4763 100644
> --- a/Documentation/git-name-rev.adoc
> +++ b/Documentation/git-name-rev.adoc
> @@ -9,7 +9,7 @@ git-name-rev - Find symbolic names for given revs
> SYNOPSIS
> --------
> [verse]
> -'git name-rev' [--tags] [--refs=<pattern>]
> +'git name-rev' [--tags] [--refs=<pattern>] [--format=<pretty>]
> ( --all | --annotate-stdin | <commit-ish>... )
We acquired a new option. Do we need a matching change to
the contents of name_rev_usage[] array?
> +--format=<pretty>::
> +--no-format::
> + Format revisions instead of outputting symbolic names. The
> + default is `--no-format`.
> ++
> +Implies `--name-only`.
If it is implication, would
git name-rev --format=reference --no-name-only
do what is naturally expected?
> @@ -462,6 +472,25 @@ static const char *get_rev_name(const struct object *o, struct strbuf *buf)
> if (o->type != OBJ_COMMIT)
> return get_exact_ref_match(o);
> c = (const struct commit *) o;
> +
> + if (format_ctx) {
> + strbuf_reset(buf);
> +
> + if (format_ctx->want.notes) {
> + struct strbuf notebuf = STRBUF_INIT;
> +
> + format_display_notes(&c->object.oid, ¬ebuf,
> + get_log_output_encoding(),
> + format_ctx->ctx.fmt == CMIT_FMT_USERFORMAT);
> + format_ctx->ctx.notes_message = strbuf_detach(¬ebuf, NULL);
> + }
> +
> + pretty_print_commit(&format_ctx->ctx, c, buf);
> + free(format_ctx->ctx.notes_message);
Is free() the expected thing to do here, or FREE_AND_NULL()? Unlike
callers like log-tree.c:show_log() where a context is prepared, used
once, and then discarded, format_pp is initialized in cmd_name_rev()
once and then repeatedly used by show_name() potentially multiple
times, so there may be a risk of getting confused by this leftover
non-NULL pointer that points at an already free'd piece of memory.
Or there may not be---I did not check, but you as the author must
have already checked, hence this question.
> + return buf->buf;
> + }
> +
> n = get_commit_rev_name(c);
> if (!n)
> return NULL;
> @@ -479,6 +508,7 @@ static const char *get_rev_name(const struct object *o, struct strbuf *buf)
>
> static void show_name(const struct object *obj,
> const char *caller_name,
> + struct pretty_format *format_ctx,
> int always, int allow_undefined, int name_only)
> {
> const char *name;
> @@ -487,7 +517,7 @@ static void show_name(const struct object *obj,
>
> if (!name_only)
> printf("%s ", caller_name ? caller_name : oid_to_hex(oid));
> - name = get_rev_name(obj, &buf);
> + name = get_rev_name(obj, format_ctx, &buf);
> if (name)
> printf("%s\n", name);
> else if (allow_undefined)
> @@ -507,7 +537,9 @@ static char const * const name_rev_usage[] = {
> NULL
> };
>
> -static void name_rev_line(char *p, struct name_ref_data *data)
> +static void name_rev_line(char *p,
> + struct name_ref_data *data,
> + struct pretty_format *format_ctx)
> {
> struct strbuf buf = STRBUF_INIT;
> int counter = 0;
> @@ -532,7 +564,7 @@ static void name_rev_line(char *p, struct name_ref_data *data)
> struct object *o =
> lookup_object(the_repository, &oid);
> if (o)
> - name = get_rev_name(o, &buf);
> + name = get_rev_name(o, format_ctx, &buf);
> }
> *(p+1) = c;
>
> @@ -567,6 +599,10 @@ int cmd_name_rev(int argc,
> #endif
> int all = 0, annotate_stdin = 0, allow_undefined = 1, always = 0, peel_tag = 0;
> struct name_ref_data data = { 0, 0, STRING_LIST_INIT_NODUP, STRING_LIST_INIT_NODUP };
> + const char *format = NULL;
> + struct rev_info format_rev = REV_INFO_INIT;
> + struct pretty_format *format_ctx = NULL;
> + struct pretty_format format_pp = {0};
Hmph, would we want to use the full init_revisions() instead of
static REV_INFO_INIT that initialises a lot more members of the
struct properly, most importantly the "repo" member that points at
the repostiory to be used?
> + if (format) {
> + struct pretty_print_context ctx = {0};
> + struct userformat_want want = {0};
> +
> + get_commit_format(format, &format_rev);
> + ctx.rev = &format_rev;
> + ctx.fmt = format_rev.commit_format;
> + ctx.abbrev = format_rev.abbrev;
> + ctx.date_mode_explicit = format_rev.date_mode_explicit;
> + ctx.date_mode = format_rev.date_mode;
> + ctx.color = GIT_COLOR_AUTO;
> + format_pp.ctx = ctx;
Why does this code initialize and assign to a on-stack ctx first and
then assign it to format_pp.ctx, instead of working on format_pp.ctx
directly?
> + userformat_find_requirements(format, &want);
> + if (want.notes)
> + load_display_notes(NULL);
> +
> + format_pp.want = want;
> + format_ctx = &format_pp;
> +
> + data.name_only = true;
> + }
> +
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [PATCH 2/2] name-rev: learn --format=<pretty>
2026-03-14 0:22 ` Junio C Hamano
@ 2026-03-17 22:07 ` Kristoffer Haugsbakk
2026-03-18 15:36 ` Kristoffer Haugsbakk
0 siblings, 1 reply; 45+ messages in thread
From: Kristoffer Haugsbakk @ 2026-03-17 22:07 UTC (permalink / raw)
To: Junio C Hamano; +Cc: git, Kristoffer Haugsbakk
On Sat, Mar 14, 2026, at 01:22, Junio C Hamano wrote:
> kristofferhaugsbakk@fastmail.com writes:
>
>> diff --git a/Documentation/git-name-rev.adoc b/Documentation/git-name-rev.adoc
>> index d4f1c4d5945..8f050cd4763 100644
>> --- a/Documentation/git-name-rev.adoc
>> +++ b/Documentation/git-name-rev.adoc
>> @@ -9,7 +9,7 @@ git-name-rev - Find symbolic names for given revs
>> SYNOPSIS
>> --------
>> [verse]
>> -'git name-rev' [--tags] [--refs=<pattern>]
>> +'git name-rev' [--tags] [--refs=<pattern>] [--format=<pretty>]
>> ( --all | --annotate-stdin | <commit-ish>... )
>
> We acquired a new option. Do we need a matching change to
> the contents of name_rev_usage[] array?
I looked at it and it seemed that `[<options>]` were supposed to stay
inside that placeholder. But since this is a new mode, maybe:
diff --git builtin/name-rev.c builtin/name-rev.c
index 6188cf98ce0..13e67a7723c 100644
--- builtin/name-rev.c
+++ builtin/name-rev.c
@@ -504,6 +504,7 @@ static char const * const name_rev_usage[] = {
N_("git name-rev [<options>] <commit>..."),
N_("git name-rev [<options>] --all"),
N_("git name-rev [<options>] --annotate-stdin"),
+ N_("git name-rev --format=<pretty> ..."),
NULL
};
>
>> +--format=<pretty>::
>> +--no-format::
>> + Format revisions instead of outputting symbolic names. The
>> + default is `--no-format`.
>> ++
>> +Implies `--name-only`.
>
> If it is implication, would
>
> git name-rev --format=reference --no-name-only
>
> do what is naturally expected?
No it wouldn’t. You’re right, it really locks in that option with no
escape hatch.
But in my next version I have switched to a parse-options callback so
that `... --format=... --no-name-only` really does turn off `--name-only`.
>
>> @@ -462,6 +472,25 @@ static const char *get_rev_name(const struct object *o, struct strbuf *buf)
>> if (o->type != OBJ_COMMIT)
>> return get_exact_ref_match(o);
>> c = (const struct commit *) o;
>> +
>> + if (format_ctx) {
>> + strbuf_reset(buf);
>> +
>> + if (format_ctx->want.notes) {
>> + struct strbuf notebuf = STRBUF_INIT;
>> +
>> + format_display_notes(&c->object.oid, ¬ebuf,
>> + get_log_output_encoding(),
>> + format_ctx->ctx.fmt == CMIT_FMT_USERFORMAT);
>> + format_ctx->ctx.notes_message = strbuf_detach(¬ebuf, NULL);
>> + }
>> +
>> + pretty_print_commit(&format_ctx->ctx, c, buf);
>> + free(format_ctx->ctx.notes_message);
>
> Is free() the expected thing to do here, or FREE_AND_NULL()? Unlike
> callers like log-tree.c:show_log() where a context is prepared, used
> once, and then discarded, format_pp is initialized in cmd_name_rev()
> once and then repeatedly used by show_name() potentially multiple
> times, so there may be a risk of getting confused by this leftover
> non-NULL pointer that points at an already free'd piece of memory.
>
> Or there may not be---I did not check, but you as the author must
> have already checked, hence this question.
This is supposed to be tested by `--name-rev --format=<pretty> with a
note`; it has a note on the first revision but not the second.
Here we never use this pointer again and we get a fresh pointer from the
strbuf before freeing again (whether it gets populated with a pointer or
not).
But it does sound better to just null it. There’s no need to have it
laying around.
>
>> + return buf->buf;
>> + }
>> +
>>[snip]
>> @@ -567,6 +599,10 @@ int cmd_name_rev(int argc,
>> #endif
>> int all = 0, annotate_stdin = 0, allow_undefined = 1, always = 0, peel_tag = 0;
>> struct name_ref_data data = { 0, 0, STRING_LIST_INIT_NODUP, STRING_LIST_INIT_NODUP };
>> + const char *format = NULL;
>> + struct rev_info format_rev = REV_INFO_INIT;
>> + struct pretty_format *format_ctx = NULL;
>> + struct pretty_format format_pp = {0};
>
> Hmph, would we want to use the full init_revisions() instead of
> static REV_INFO_INIT that initialises a lot more members of the
> struct properly, most importantly the "repo" member that points at
> the repostiory to be used?
Here I looked at the doc for `pretty.h:get_commit_format` in order to
learn what I needed to set up. Since it doesn’t say much I did the least
work that I could get away with when it comes to struct
initializing. Since it does seem like a struct for a lot of different
situations while this is just a formatting situation.
I might have done less research than I ought to.
>
>> + if (format) {
>> + struct pretty_print_context ctx = {0};
>> + struct userformat_want want = {0};
>> +
>> + get_commit_format(format, &format_rev);
>> + ctx.rev = &format_rev;
>> + ctx.fmt = format_rev.commit_format;
>> + ctx.abbrev = format_rev.abbrev;
>> + ctx.date_mode_explicit = format_rev.date_mode_explicit;
>> + ctx.date_mode = format_rev.date_mode;
>> + ctx.color = GIT_COLOR_AUTO;
>> + format_pp.ctx = ctx;
>
> Why does this code initialize and assign to a on-stack ctx first and
> then assign it to format_pp.ctx, instead of working on format_pp.ctx
> directly?
You’re right. I’ll just assign directly.
>
>> + userformat_find_requirements(format, &want);
>> + if (want.notes)
>> + load_display_notes(NULL);
>> +
>> + format_pp.want = want;
>> + format_ctx = &format_pp;
>> +
>> + data.name_only = true;
>> + }
>> +
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [PATCH 1/2] name-rev: wrap both blocks in braces
2026-03-14 0:22 ` Junio C Hamano
@ 2026-03-17 22:10 ` Kristoffer Haugsbakk
0 siblings, 0 replies; 45+ messages in thread
From: Kristoffer Haugsbakk @ 2026-03-17 22:10 UTC (permalink / raw)
To: Junio C Hamano, Kristoffer Haugsbakk; +Cc: git
On Sat, Mar 14, 2026, at 01:22, Junio C Hamano wrote:
> kristofferhaugsbakk@fastmail.com writes:
>
>> - else if (++counter == hexsz &&
>> - !ishex(*(p+1))) {
>> + } else if (++counter == hexsz &&
>> + !ishex(*(p + 1))) {
>> struct object_id oid;
>> const char *name = NULL;
>> char c = *(p+1);
>
> You are correcting "p+1" to "p + 1" to honor our coding style in a
> few lines above "while at it", but there are three others in the
> same block (we can see one of them in the post-context), which means
> these are now inconsistent. Fixing all of them would make it a far
> larger change than qualifies as a "while at it" change. Either make
> it another step that is an unrelated clean up, or leave it as-is.
Correct observation. clang-format got ahold of a few stray lines. But I
thought it only corrected the indentation, not that it also caught this
arithmetic formatting. I’ll remove it in the next version.
Thanks
>
> The primary thrust of this patch does make sense and is executed
> well.
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [PATCH 2/2] name-rev: learn --format=<pretty>
2026-03-17 22:07 ` Kristoffer Haugsbakk
@ 2026-03-18 15:36 ` Kristoffer Haugsbakk
0 siblings, 0 replies; 45+ messages in thread
From: Kristoffer Haugsbakk @ 2026-03-18 15:36 UTC (permalink / raw)
To: Junio C Hamano; +Cc: git
On Tue, Mar 17, 2026, at 23:07, Kristoffer Haugsbakk wrote:
>>[snip]
>>
>> Or there may not be---I did not check, but you as the author must
>> have already checked, hence this question.
>
> This is supposed to be tested by `--name-rev --format=<pretty> with a
> note`; it has a note on the first revision but not the second.
>
> Here we never use this pointer again and we get a fresh pointer from the
> strbuf before freeing again (whether it gets populated with a pointer or
> not).
Here I meant to write: “whether it gets populated with any note data
or not”.
>
> But it does sound better to just null it. There’s no need to have it
> laying around.
>[snip]
^ permalink raw reply [flat|nested] 45+ messages in thread
* [PATCH v2 0/2] name-rev: learn --format=<pretty>
2026-03-13 16:03 [PATCH 0/2] name-rev: learn --format=<pretty> kristofferhaugsbakk
2026-03-13 16:03 ` [PATCH 1/2] name-rev: wrap both blocks in braces kristofferhaugsbakk
2026-03-13 16:03 ` [PATCH 2/2] name-rev: learn --format=<pretty> kristofferhaugsbakk
@ 2026-03-20 13:09 ` kristofferhaugsbakk
2026-03-20 13:09 ` [PATCH v2 1/2] name-rev: wrap both blocks in braces kristofferhaugsbakk
` (2 more replies)
2 siblings, 3 replies; 45+ messages in thread
From: kristofferhaugsbakk @ 2026-03-20 13:09 UTC (permalink / raw)
To: git; +Cc: Kristoffer Haugsbakk
From: Kristoffer Haugsbakk <code@khaugsbakk.name>
Topic name (applied): kh/name-rev-custom-format
Topic summary: Teach git-name-rev(1) a mode to pretty format revisions
instead of outputting symbolic names.
(See the second patch for details.)
The first patch is just for `CodingGuidelines`. Unrelated.
(See the previous cover letter for a slightly different introduction.)
This started because I had a faint wish in the back of my head (that I was
probably not going to act on) to implement a third-party git-format-rev(1)
command which does what `git name-rev --format=<pretty>` does with this
series applied. I thought I could just use one invocation of some git(1)
command to feed to stdin and read from stdout. Like git-log(1), or
git-rev-list(1). But due to apparent limitations (see patch [2/2] message)
there didn’t seem to be anything better than invoking a git(1) command per
revision/hash that was encountered in the text. I wanted something more
efficient. (I had no need for something more efficient. I just wanted it.)
I then picked git-name-rev(1) since that was the easiest thing to implement
for me. Add an option to an existing command.
Right now this is just version 2 and I don’t know if this is a good
approach or not. If it ever is accepted (in whatever form) I don’t expect
it to be soon.
git(1) already has a lot of commands. This doesn’t expand the footprint. On
the other hand, I might be naive about what a proper formatter needs in
terms of command options. Right now there is the helper `--notes`. But is
more needed?
• --abbrev ?
• --date ?
I don’t know. Maybe at some point this would be too much, too crowded, for
git-name-rev(1). Then it might make sense to split it out to a separate
builtin?
These are some thoughts and context.
§ Changes in v2
Implement `--notes` and respond to reviewer feedback. Details on the second
patch.
The first patch now just formats what it intends to (there was stray
clang-formatting).
[1/2] name-rev: wrap both blocks in braces
[2/2] name-rev: learn --format=<pretty>
Documentation/git-name-rev.adoc | 10 ++-
builtin/name-rev.c | 108 ++++++++++++++++++++++++++++----
t/t6120-describe.sh | 96 ++++++++++++++++++++++++++++
3 files changed, 202 insertions(+), 12 deletions(-)
Interdiff against v1:
diff --git a/Documentation/git-name-rev.adoc b/Documentation/git-name-rev.adoc
index 8f050cd4763..65348690c8c 100644
--- a/Documentation/git-name-rev.adoc
+++ b/Documentation/git-name-rev.adoc
@@ -26,7 +26,8 @@ OPTIONS
Format revisions instead of outputting symbolic names. The
default is `--no-format`.
+
-Implies `--name-only`.
+Implies `--name-only`. The negation `--no-format` implies
+`--no-name-only` (the default for the command).
--tags::
Do not use branch names, but only tags to name the commits
diff --git a/builtin/name-rev.c b/builtin/name-rev.c
index 30d981104c6..9a008d8b7a8 100644
--- a/builtin/name-rev.c
+++ b/builtin/name-rev.c
@@ -41,6 +41,11 @@ struct pretty_format {
struct userformat_want want;
};
+struct format_cb_data {
+ const char *format;
+ int *name_only;
+};
+
define_commit_slab(commit_rev_name, struct rev_name);
static timestamp_t generation_cutoff = GENERATION_NUMBER_INFINITY;
@@ -486,7 +491,7 @@ static const char *get_rev_name(const struct object *o,
}
pretty_print_commit(&format_ctx->ctx, c, buf);
- free(format_ctx->ctx.notes_message);
+ FREE_AND_NULL(format_ctx->ctx.notes_message);
return buf->buf;
}
@@ -551,7 +556,7 @@ static void name_rev_line(char *p,
if (!ishex(*p)) {
counter = 0;
} else if (++counter == hexsz &&
- !ishex(*(p + 1))) {
+ !ishex(*(p+1))) {
struct object_id oid;
const char *name = NULL;
char c = *(p+1);
@@ -586,6 +591,16 @@ static void name_rev_line(char *p,
strbuf_release(&buf);
}
+static int format_cb(const struct option *option,
+ const char *arg,
+ int unset)
+{
+ struct format_cb_data *data = option->value;
+ data->format = arg;
+ *data->name_only = !unset;
+ return 0;
+}
+
int cmd_name_rev(int argc,
const char **argv,
const char *prefix,
@@ -599,10 +614,12 @@ int cmd_name_rev(int argc,
#endif
int all = 0, annotate_stdin = 0, allow_undefined = 1, always = 0, peel_tag = 0;
struct name_ref_data data = { 0, 0, STRING_LIST_INIT_NODUP, STRING_LIST_INIT_NODUP };
- const char *format = NULL;
+ static struct format_cb_data format_cb_data = { 0 };
+ struct display_notes_opt format_notes_opt;
struct rev_info format_rev = REV_INFO_INIT;
struct pretty_format *format_ctx = NULL;
- struct pretty_format format_pp = {0};
+ struct pretty_format format_pp = { 0 };
+ struct string_list notes = STRING_LIST_INIT_NODUP;
struct option opts[] = {
OPT_BOOL(0, "name-only", &data.name_only, N_("print only ref-based names (no object names)")),
OPT_BOOL(0, "tags", &data.tags_only, N_("only use tags to name the commits")),
@@ -620,8 +637,10 @@ int cmd_name_rev(int argc,
PARSE_OPT_HIDDEN),
#endif /* WITH_BREAKING_CHANGES */
OPT_BOOL(0, "annotate-stdin", &annotate_stdin, N_("annotate text from stdin")),
- OPT_STRING(0, "format", &format, N_("format"),
- "pretty-print output instead"),
+ OPT_CALLBACK(0, "format", &format_cb_data, N_("format"),
+ N_("pretty-print output instead"), format_cb),
+ OPT_STRING_LIST(0, "notes", ¬es, N_("notes"),
+ N_("display notes for --format")),
OPT_BOOL(0, "undefined", &allow_undefined, N_("allow to print `undefined` names (default)")),
OPT_BOOL(0, "always", &always,
N_("show abbreviated commit object as fallback")),
@@ -630,6 +649,8 @@ int cmd_name_rev(int argc,
OPT_END(),
};
+ init_display_notes(&format_notes_opt);
+ format_cb_data.name_only = &data.name_only;
mem_pool_init(&string_pool, 0);
init_commit_rev_name(&rev_names);
repo_config(the_repository, git_default_config, NULL);
@@ -644,27 +665,29 @@ int cmd_name_rev(int argc,
}
#endif
- if (format) {
- struct pretty_print_context ctx = {0};
- struct userformat_want want = {0};
-
- get_commit_format(format, &format_rev);
- ctx.rev = &format_rev;
- ctx.fmt = format_rev.commit_format;
- ctx.abbrev = format_rev.abbrev;
- ctx.date_mode_explicit = format_rev.date_mode_explicit;
- ctx.date_mode = format_rev.date_mode;
- ctx.color = GIT_COLOR_AUTO;
- format_pp.ctx = ctx;
-
- userformat_find_requirements(format, &want);
- if (want.notes)
- load_display_notes(NULL);
+ if (format_cb_data.format) {
+ get_commit_format(format_cb_data.format, &format_rev);
+ format_pp.ctx.rev = &format_rev;
+ format_pp.ctx.fmt = format_rev.commit_format;
+ format_pp.ctx.abbrev = format_rev.abbrev;
+ format_pp.ctx.date_mode_explicit = format_rev.date_mode_explicit;
+ format_pp.ctx.date_mode = format_rev.date_mode;
+ format_pp.ctx.color = GIT_COLOR_AUTO;
+
+ userformat_find_requirements(format_cb_data.format,
+ &format_pp.want);
+ if (format_pp.want.notes) {
+ int ignore_show_notes = 0;
+ struct string_list_item *n;
+
+ for_each_string_list_item(n, ¬es)
+ enable_ref_display_notes(&format_notes_opt,
+ &ignore_show_notes,
+ n->string);
+ load_display_notes(&format_notes_opt);
+ }
- format_pp.want = want;
format_ctx = &format_pp;
-
- data.name_only = true;
}
if (all + annotate_stdin + !!argc > 1) {
@@ -747,6 +770,8 @@ int cmd_name_rev(int argc,
string_list_clear(&data.ref_filters, 0);
string_list_clear(&data.exclude_filters, 0);
+ string_list_clear(¬es, 0);
+ release_display_notes(&format_notes_opt);
mem_pool_discard(&string_pool, 0);
object_array_clear(&revs);
return 0;
diff --git a/t/t6120-describe.sh b/t/t6120-describe.sh
index 6dba392d343..0b7e9fe396d 100755
--- a/t/t6120-describe.sh
+++ b/t/t6120-describe.sh
@@ -671,6 +671,25 @@ test_expect_success 'name-rev --format setup' '
test_commit -C repo-format eighth
'
+test_expect_success 'name-rev --format --no-name-only' '
+ cat >expect <<-\EOF &&
+ HEAD~3 [fifth]
+ HEAD [eighth]
+ HEAD~5 [third]
+ EOF
+ git -C repo-format name-rev --format="[%s]" \
+ --no-name-only HEAD~3 HEAD HEAD~5 >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'name-rev --format --no-format is the same as regular name-rev' '
+ git -C repo-format name-rev HEAD~2 HEAD~3 >expect &&
+ test_file_not_empty expect &&
+ git -C repo-format name-rev --format="huh?" \
+ --no-format HEAD~2 HEAD~3 >actual &&
+ test_cmp expect actual
+'
+
test_expect_success 'name-rev --format=%s for argument revs' '
cat >expect <<-\EOF &&
eighth
@@ -682,7 +701,7 @@ test_expect_success 'name-rev --format=%s for argument revs' '
test_cmp expect actual
'
-test_expect_success '--name-rev --format=<pretty> --annotate-stdin from rev-list same as log' '
+test_expect_success '--name-rev --format=reference --annotate-stdin from rev-list same as log' '
git -C repo-format log --format=reference >expect &&
test_file_not_empty expect &&
git -C repo-format rev-list HEAD >list &&
@@ -707,7 +726,7 @@ test_expect_success '--name-rev --format=<pretty> --annotate-stdin with running
test_cmp expect actual
'
-test_expect_success '--name-rev --format=<pretty> with a note' '
+test_expect_success 'name-rev --format=<pretty> with %N (note)' '
test_when_finished "git -C repo-format notes remove" &&
git -C repo-format notes add -m"Make a note" &&
printf "Make a note\n\n\n" >expect &&
@@ -716,6 +735,25 @@ test_expect_success '--name-rev --format=<pretty> with a note' '
test_cmp expect actual
'
+test_expect_success 'name-rev --format=<pretty> --notes<ref>' '
+ # One custom notes ref
+ test_when_finished "git -C repo-format notes remove" &&
+ test_when_finished "git -C repo-format notes --ref=word remove" &&
+ git -C repo-format notes add -m"default" &&
+ git -C repo-format notes --ref=word add -m"custom" &&
+ printf "custom\n\n" >expect &&
+ git -C repo-format name-rev --format="tformat:%N" \
+ --notes=word \
+ HEAD >actual &&
+ test_cmp expect actual &&
+ # Glob all
+ printf "default\ncustom\n\n" >expect &&
+ git -C repo-format name-rev --format="tformat:%N" \
+ --notes=* \
+ HEAD >actual &&
+ test_cmp expect actual
+'
+
# B
# o
# H \
Range-diff against v1:
1: 6f88b4c96a9 ! 1: 9cb5cfd1ec3 name-rev: wrap both blocks in braces
@@ builtin/name-rev.c: static void name_rev_line(char *p, struct name_ref_data *dat
+ if (!ishex(*p)) {
counter = 0;
- else if (++counter == hexsz &&
-- !ishex(*(p+1))) {
+ } else if (++counter == hexsz &&
-+ !ishex(*(p + 1))) {
+ !ishex(*(p+1))) {
struct object_id oid;
const char *name = NULL;
- char c = *(p+1);
2: 6430627e611 ! 2: 52a52060776 name-rev: learn --format=<pretty>
@@ Documentation/git-name-rev.adoc: format parsable by 'git rev-parse'.
+ Format revisions instead of outputting symbolic names. The
+ default is `--no-format`.
++
-+Implies `--name-only`.
++Implies `--name-only`. The negation `--no-format` implies
++`--no-name-only` (the default for the command).
+
--tags::
Do not use branch names, but only tags to name the commits
@@ builtin/name-rev.c: struct rev_name {
+ struct pretty_print_context ctx;
+ struct userformat_want want;
+};
++
++struct format_cb_data {
++ const char *format;
++ int *name_only;
++};
+
define_commit_slab(commit_rev_name, struct rev_name);
@@ builtin/name-rev.c: static const char *get_rev_name(const struct object *o, stru
+ }
+
+ pretty_print_commit(&format_ctx->ctx, c, buf);
-+ free(format_ctx->ctx.notes_message);
++ FREE_AND_NULL(format_ctx->ctx.notes_message);
+
+ return buf->buf;
+ }
@@ builtin/name-rev.c: static void name_rev_line(char *p, struct name_ref_data *dat
}
*(p+1) = c;
+@@ builtin/name-rev.c: static void name_rev_line(char *p, struct name_ref_data *data)
+ strbuf_release(&buf);
+ }
+
++static int format_cb(const struct option *option,
++ const char *arg,
++ int unset)
++{
++ struct format_cb_data *data = option->value;
++ data->format = arg;
++ *data->name_only = !unset;
++ return 0;
++}
++
+ int cmd_name_rev(int argc,
+ const char **argv,
+ const char *prefix,
@@ builtin/name-rev.c: int cmd_name_rev(int argc,
#endif
int all = 0, annotate_stdin = 0, allow_undefined = 1, always = 0, peel_tag = 0;
struct name_ref_data data = { 0, 0, STRING_LIST_INIT_NODUP, STRING_LIST_INIT_NODUP };
-+ const char *format = NULL;
++ static struct format_cb_data format_cb_data = { 0 };
++ struct display_notes_opt format_notes_opt;
+ struct rev_info format_rev = REV_INFO_INIT;
+ struct pretty_format *format_ctx = NULL;
-+ struct pretty_format format_pp = {0};
++ struct pretty_format format_pp = { 0 };
++ struct string_list notes = STRING_LIST_INIT_NODUP;
struct option opts[] = {
OPT_BOOL(0, "name-only", &data.name_only, N_("print only ref-based names (no object names)")),
OPT_BOOL(0, "tags", &data.tags_only, N_("only use tags to name the commits")),
@@ builtin/name-rev.c: int cmd_name_rev(int argc,
PARSE_OPT_HIDDEN),
#endif /* WITH_BREAKING_CHANGES */
OPT_BOOL(0, "annotate-stdin", &annotate_stdin, N_("annotate text from stdin")),
-+ OPT_STRING(0, "format", &format, N_("format"),
-+ "pretty-print output instead"),
++ OPT_CALLBACK(0, "format", &format_cb_data, N_("format"),
++ N_("pretty-print output instead"), format_cb),
++ OPT_STRING_LIST(0, "notes", ¬es, N_("notes"),
++ N_("display notes for --format")),
OPT_BOOL(0, "undefined", &allow_undefined, N_("allow to print `undefined` names (default)")),
OPT_BOOL(0, "always", &always,
N_("show abbreviated commit object as fallback")),
+@@ builtin/name-rev.c: int cmd_name_rev(int argc,
+ OPT_END(),
+ };
+
++ init_display_notes(&format_notes_opt);
++ format_cb_data.name_only = &data.name_only;
+ mem_pool_init(&string_pool, 0);
+ init_commit_rev_name(&rev_names);
+ repo_config(the_repository, git_default_config, NULL);
@@ builtin/name-rev.c: int cmd_name_rev(int argc,
}
#endif
-+ if (format) {
-+ struct pretty_print_context ctx = {0};
-+ struct userformat_want want = {0};
++ if (format_cb_data.format) {
++ get_commit_format(format_cb_data.format, &format_rev);
++ format_pp.ctx.rev = &format_rev;
++ format_pp.ctx.fmt = format_rev.commit_format;
++ format_pp.ctx.abbrev = format_rev.abbrev;
++ format_pp.ctx.date_mode_explicit = format_rev.date_mode_explicit;
++ format_pp.ctx.date_mode = format_rev.date_mode;
++ format_pp.ctx.color = GIT_COLOR_AUTO;
+
-+ get_commit_format(format, &format_rev);
-+ ctx.rev = &format_rev;
-+ ctx.fmt = format_rev.commit_format;
-+ ctx.abbrev = format_rev.abbrev;
-+ ctx.date_mode_explicit = format_rev.date_mode_explicit;
-+ ctx.date_mode = format_rev.date_mode;
-+ ctx.color = GIT_COLOR_AUTO;
-+ format_pp.ctx = ctx;
++ userformat_find_requirements(format_cb_data.format,
++ &format_pp.want);
++ if (format_pp.want.notes) {
++ int ignore_show_notes = 0;
++ struct string_list_item *n;
+
-+ userformat_find_requirements(format, &want);
-+ if (want.notes)
-+ load_display_notes(NULL);
++ for_each_string_list_item(n, ¬es)
++ enable_ref_display_notes(&format_notes_opt,
++ &ignore_show_notes,
++ n->string);
++ load_display_notes(&format_notes_opt);
++ }
+
-+ format_pp.want = want;
+ format_ctx = &format_pp;
-+
-+ data.name_only = true;
+ }
+
if (all + annotate_stdin + !!argc > 1) {
@@ builtin/name-rev.c: int cmd_name_rev(int argc,
always, allow_undefined, data.name_only);
}
+ string_list_clear(&data.ref_filters, 0);
+ string_list_clear(&data.exclude_filters, 0);
++ string_list_clear(¬es, 0);
++ release_display_notes(&format_notes_opt);
+ mem_pool_discard(&string_pool, 0);
+ object_array_clear(&revs);
+ return 0;
## t/t6120-describe.sh ##
@@ t/t6120-describe.sh: test_expect_success 'name-rev --annotate-stdin works with commitGraph' '
@@ t/t6120-describe.sh: test_expect_success 'name-rev --annotate-stdin works with c
+ test_commit -C repo-format eighth
+'
+
++test_expect_success 'name-rev --format --no-name-only' '
++ cat >expect <<-\EOF &&
++ HEAD~3 [fifth]
++ HEAD [eighth]
++ HEAD~5 [third]
++ EOF
++ git -C repo-format name-rev --format="[%s]" \
++ --no-name-only HEAD~3 HEAD HEAD~5 >actual &&
++ test_cmp expect actual
++'
++
++test_expect_success 'name-rev --format --no-format is the same as regular name-rev' '
++ git -C repo-format name-rev HEAD~2 HEAD~3 >expect &&
++ test_file_not_empty expect &&
++ git -C repo-format name-rev --format="huh?" \
++ --no-format HEAD~2 HEAD~3 >actual &&
++ test_cmp expect actual
++'
++
+test_expect_success 'name-rev --format=%s for argument revs' '
+ cat >expect <<-\EOF &&
+ eighth
@@ t/t6120-describe.sh: test_expect_success 'name-rev --annotate-stdin works with c
+ test_cmp expect actual
+'
+
-+test_expect_success '--name-rev --format=<pretty> --annotate-stdin from rev-list same as log' '
++test_expect_success '--name-rev --format=reference --annotate-stdin from rev-list same as log' '
+ git -C repo-format log --format=reference >expect &&
+ test_file_not_empty expect &&
+ git -C repo-format rev-list HEAD >list &&
@@ t/t6120-describe.sh: test_expect_success 'name-rev --annotate-stdin works with c
+ test_cmp expect actual
+'
+
-+test_expect_success '--name-rev --format=<pretty> with a note' '
++test_expect_success 'name-rev --format=<pretty> with %N (note)' '
+ test_when_finished "git -C repo-format notes remove" &&
+ git -C repo-format notes add -m"Make a note" &&
+ printf "Make a note\n\n\n" >expect &&
@@ t/t6120-describe.sh: test_expect_success 'name-rev --annotate-stdin works with c
+ HEAD HEAD~ >actual &&
+ test_cmp expect actual
+'
++
++test_expect_success 'name-rev --format=<pretty> --notes<ref>' '
++ # One custom notes ref
++ test_when_finished "git -C repo-format notes remove" &&
++ test_when_finished "git -C repo-format notes --ref=word remove" &&
++ git -C repo-format notes add -m"default" &&
++ git -C repo-format notes --ref=word add -m"custom" &&
++ printf "custom\n\n" >expect &&
++ git -C repo-format name-rev --format="tformat:%N" \
++ --notes=word \
++ HEAD >actual &&
++ test_cmp expect actual &&
++ # Glob all
++ printf "default\ncustom\n\n" >expect &&
++ git -C repo-format name-rev --format="tformat:%N" \
++ --notes=* \
++ HEAD >actual &&
++ test_cmp expect actual
++'
+
# B
# o
base-commit: 67006b9db8b772423ad0706029286096307d2567
--
2.53.0.32.gf6228eaf9cc
^ permalink raw reply related [flat|nested] 45+ messages in thread
* [PATCH v2 1/2] name-rev: wrap both blocks in braces
2026-03-20 13:09 ` [PATCH v2 0/2] " kristofferhaugsbakk
@ 2026-03-20 13:09 ` kristofferhaugsbakk
2026-03-20 13:09 ` [PATCH v2 2/2] name-rev: learn --format=<pretty> kristofferhaugsbakk
2026-04-28 22:25 ` [PATCH v3 0/5] format-rev: introduce builtin for on-demand pretty formatting kristofferhaugsbakk
2 siblings, 0 replies; 45+ messages in thread
From: kristofferhaugsbakk @ 2026-03-20 13:09 UTC (permalink / raw)
To: git; +Cc: Kristoffer Haugsbakk
From: Kristoffer Haugsbakk <code@khaugsbakk.name>
See `CodingGuidelines`:
- When there are multiple arms to a conditional and some of them
require braces, enclose even a single line block in braces for
consistency. [...]
Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
---
Notes (series):
v2:
Fix stray formatting of `(p+1)`
builtin/name-rev.c | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/builtin/name-rev.c b/builtin/name-rev.c
index 6188cf98ce0..171e7bd0e98 100644
--- a/builtin/name-rev.c
+++ b/builtin/name-rev.c
@@ -466,9 +466,9 @@ static const char *get_rev_name(const struct object *o, struct strbuf *buf)
if (!n)
return NULL;
- if (!n->generation)
+ if (!n->generation) {
return n->tip_name;
- else {
+ } else {
strbuf_reset(buf);
strbuf_addstr(buf, n->tip_name);
strbuf_strip_suffix(buf, "^0");
@@ -516,9 +516,9 @@ static void name_rev_line(char *p, struct name_ref_data *data)
for (p_start = p; *p; p++) {
#define ishex(x) (isdigit((x)) || ((x) >= 'a' && (x) <= 'f'))
- if (!ishex(*p))
+ if (!ishex(*p)) {
counter = 0;
- else if (++counter == hexsz &&
+ } else if (++counter == hexsz &&
!ishex(*(p+1))) {
struct object_id oid;
const char *name = NULL;
--
2.53.0.32.gf6228eaf9cc
^ permalink raw reply related [flat|nested] 45+ messages in thread
* [PATCH v2 2/2] name-rev: learn --format=<pretty>
2026-03-20 13:09 ` [PATCH v2 0/2] " kristofferhaugsbakk
2026-03-20 13:09 ` [PATCH v2 1/2] name-rev: wrap both blocks in braces kristofferhaugsbakk
@ 2026-03-20 13:09 ` kristofferhaugsbakk
2026-03-20 15:25 ` D. Ben Knoble
2026-04-28 22:25 ` [PATCH v3 0/5] format-rev: introduce builtin for on-demand pretty formatting kristofferhaugsbakk
2 siblings, 1 reply; 45+ messages in thread
From: kristofferhaugsbakk @ 2026-03-20 13:09 UTC (permalink / raw)
To: git; +Cc: Kristoffer Haugsbakk
From: Kristoffer Haugsbakk <code@khaugsbakk.name>
Teach git-name-rev(1) to format the given revisions instead of creating
symbolic names.
Sometimes you want to format commits. Most of the time you’re walking
the graph, e.g. getting a range of commits like `master..topic`. That’s
a job for git-log(1).
But sometimes you might want to format commits that you encounter
on demand:
• Full hashes in running text that you might want to pretty-print
• git-last-modified(1) outputs full hashes that you can do the same with
• git-cherry(1) has `-v` for commit subject, but maybe you want
something else?
But now you can’t use git-log(1), git-show(1), or git-rev-list(1):
• You can’t feed commits piecemeal to these commands, one input for one
output; they block until standard in is closed
• You can’t feed a list of possibly duplicate commits, like the output
of git-last-modified(1); they effectively deduplicate the output
Beyond these two points there’s also the input massage problem: you
cannot feed mixed input (revisions mixed with arbitrary text).
One might hope that git-cat-file(1) can save us. But it doesn’t support
pretty formats.
But there is one command that already both handles revisions as
arguments, revisions on standard input, and even revisions mixed
in with arbitrary text. Namely git-name-rev(1).
Teach it to work in a format mode where the output for each revision is
the pretty output (implies `--name-only`). This can be used to format
any revision expression when given as arguments, and all full commit
hashes in running text on stdin.
Just bring the hashes (to the pipeline). We will pretty print them.
Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
---
Notes (series):
v2:
• Propely implement “--format implies --name-only”
• Don’t use a needless intermediary struct
• Add a new member to `name_rev_usage[]`
• FREE_AND_NULL notes string. There is no use-after-free but we have no
reason to leave a freed pointer just laying there
• Implement `--notes` for `%N` atom use (not just restrict to the default
notes ref)
• Previous review pointed out `init_revisions()` but this still just uses
`REV_INFO_INIT` since it seemed enough. But I have no problem with
changing it whatsoever. (This series is still very work-in-progress in
any case.)
• Tweak test name to mention the specific pretty format (reference); it
doesn’t generalize to e.g. `oneline` because you get different output in
that case
Mostly from: https://lore.kernel.org/git/xmqq8qbvz2dm.fsf@gitster.g/
Documentation/git-name-rev.adoc | 10 +++-
builtin/name-rev.c | 100 +++++++++++++++++++++++++++++---
t/t6120-describe.sh | 96 ++++++++++++++++++++++++++++++
3 files changed, 198 insertions(+), 8 deletions(-)
diff --git a/Documentation/git-name-rev.adoc b/Documentation/git-name-rev.adoc
index d4f1c4d5945..65348690c8c 100644
--- a/Documentation/git-name-rev.adoc
+++ b/Documentation/git-name-rev.adoc
@@ -9,7 +9,7 @@ git-name-rev - Find symbolic names for given revs
SYNOPSIS
--------
[verse]
-'git name-rev' [--tags] [--refs=<pattern>]
+'git name-rev' [--tags] [--refs=<pattern>] [--format=<pretty>]
( --all | --annotate-stdin | <commit-ish>... )
DESCRIPTION
@@ -21,6 +21,14 @@ format parsable by 'git rev-parse'.
OPTIONS
-------
+--format=<pretty>::
+--no-format::
+ Format revisions instead of outputting symbolic names. The
+ default is `--no-format`.
++
+Implies `--name-only`. The negation `--no-format` implies
+`--no-name-only` (the default for the command).
+
--tags::
Do not use branch names, but only tags to name the commits
diff --git a/builtin/name-rev.c b/builtin/name-rev.c
index 171e7bd0e98..9a008d8b7a8 100644
--- a/builtin/name-rev.c
+++ b/builtin/name-rev.c
@@ -18,6 +18,9 @@
#include "commit-graph.h"
#include "wildmatch.h"
#include "mem-pool.h"
+#include "pretty.h"
+#include "revision.h"
+#include "notes.h"
/*
* One day. See the 'name a rev shortly after epoch' test in t6120 when
@@ -33,6 +36,16 @@ struct rev_name {
int from_tag;
};
+struct pretty_format {
+ struct pretty_print_context ctx;
+ struct userformat_want want;
+};
+
+struct format_cb_data {
+ const char *format;
+ int *name_only;
+};
+
define_commit_slab(commit_rev_name, struct rev_name);
static timestamp_t generation_cutoff = GENERATION_NUMBER_INFINITY;
@@ -454,7 +467,9 @@ static const char *get_exact_ref_match(const struct object *o)
}
/* may return a constant string or use "buf" as scratch space */
-static const char *get_rev_name(const struct object *o, struct strbuf *buf)
+static const char *get_rev_name(const struct object *o,
+ struct pretty_format *format_ctx,
+ struct strbuf *buf)
{
struct rev_name *n;
const struct commit *c;
@@ -462,6 +477,25 @@ static const char *get_rev_name(const struct object *o, struct strbuf *buf)
if (o->type != OBJ_COMMIT)
return get_exact_ref_match(o);
c = (const struct commit *) o;
+
+ if (format_ctx) {
+ strbuf_reset(buf);
+
+ if (format_ctx->want.notes) {
+ struct strbuf notebuf = STRBUF_INIT;
+
+ format_display_notes(&c->object.oid, ¬ebuf,
+ get_log_output_encoding(),
+ format_ctx->ctx.fmt == CMIT_FMT_USERFORMAT);
+ format_ctx->ctx.notes_message = strbuf_detach(¬ebuf, NULL);
+ }
+
+ pretty_print_commit(&format_ctx->ctx, c, buf);
+ FREE_AND_NULL(format_ctx->ctx.notes_message);
+
+ return buf->buf;
+ }
+
n = get_commit_rev_name(c);
if (!n)
return NULL;
@@ -479,6 +513,7 @@ static const char *get_rev_name(const struct object *o, struct strbuf *buf)
static void show_name(const struct object *obj,
const char *caller_name,
+ struct pretty_format *format_ctx,
int always, int allow_undefined, int name_only)
{
const char *name;
@@ -487,7 +522,7 @@ static void show_name(const struct object *obj,
if (!name_only)
printf("%s ", caller_name ? caller_name : oid_to_hex(oid));
- name = get_rev_name(obj, &buf);
+ name = get_rev_name(obj, format_ctx, &buf);
if (name)
printf("%s\n", name);
else if (allow_undefined)
@@ -507,7 +542,9 @@ static char const * const name_rev_usage[] = {
NULL
};
-static void name_rev_line(char *p, struct name_ref_data *data)
+static void name_rev_line(char *p,
+ struct name_ref_data *data,
+ struct pretty_format *format_ctx)
{
struct strbuf buf = STRBUF_INIT;
int counter = 0;
@@ -532,7 +569,7 @@ static void name_rev_line(char *p, struct name_ref_data *data)
struct object *o =
lookup_object(the_repository, &oid);
if (o)
- name = get_rev_name(o, &buf);
+ name = get_rev_name(o, format_ctx, &buf);
}
*(p+1) = c;
@@ -554,6 +591,16 @@ static void name_rev_line(char *p, struct name_ref_data *data)
strbuf_release(&buf);
}
+static int format_cb(const struct option *option,
+ const char *arg,
+ int unset)
+{
+ struct format_cb_data *data = option->value;
+ data->format = arg;
+ *data->name_only = !unset;
+ return 0;
+}
+
int cmd_name_rev(int argc,
const char **argv,
const char *prefix,
@@ -567,6 +614,12 @@ int cmd_name_rev(int argc,
#endif
int all = 0, annotate_stdin = 0, allow_undefined = 1, always = 0, peel_tag = 0;
struct name_ref_data data = { 0, 0, STRING_LIST_INIT_NODUP, STRING_LIST_INIT_NODUP };
+ static struct format_cb_data format_cb_data = { 0 };
+ struct display_notes_opt format_notes_opt;
+ struct rev_info format_rev = REV_INFO_INIT;
+ struct pretty_format *format_ctx = NULL;
+ struct pretty_format format_pp = { 0 };
+ struct string_list notes = STRING_LIST_INIT_NODUP;
struct option opts[] = {
OPT_BOOL(0, "name-only", &data.name_only, N_("print only ref-based names (no object names)")),
OPT_BOOL(0, "tags", &data.tags_only, N_("only use tags to name the commits")),
@@ -584,6 +637,10 @@ int cmd_name_rev(int argc,
PARSE_OPT_HIDDEN),
#endif /* WITH_BREAKING_CHANGES */
OPT_BOOL(0, "annotate-stdin", &annotate_stdin, N_("annotate text from stdin")),
+ OPT_CALLBACK(0, "format", &format_cb_data, N_("format"),
+ N_("pretty-print output instead"), format_cb),
+ OPT_STRING_LIST(0, "notes", ¬es, N_("notes"),
+ N_("display notes for --format")),
OPT_BOOL(0, "undefined", &allow_undefined, N_("allow to print `undefined` names (default)")),
OPT_BOOL(0, "always", &always,
N_("show abbreviated commit object as fallback")),
@@ -592,6 +649,8 @@ int cmd_name_rev(int argc,
OPT_END(),
};
+ init_display_notes(&format_notes_opt);
+ format_cb_data.name_only = &data.name_only;
mem_pool_init(&string_pool, 0);
init_commit_rev_name(&rev_names);
repo_config(the_repository, git_default_config, NULL);
@@ -606,6 +665,31 @@ int cmd_name_rev(int argc,
}
#endif
+ if (format_cb_data.format) {
+ get_commit_format(format_cb_data.format, &format_rev);
+ format_pp.ctx.rev = &format_rev;
+ format_pp.ctx.fmt = format_rev.commit_format;
+ format_pp.ctx.abbrev = format_rev.abbrev;
+ format_pp.ctx.date_mode_explicit = format_rev.date_mode_explicit;
+ format_pp.ctx.date_mode = format_rev.date_mode;
+ format_pp.ctx.color = GIT_COLOR_AUTO;
+
+ userformat_find_requirements(format_cb_data.format,
+ &format_pp.want);
+ if (format_pp.want.notes) {
+ int ignore_show_notes = 0;
+ struct string_list_item *n;
+
+ for_each_string_list_item(n, ¬es)
+ enable_ref_display_notes(&format_notes_opt,
+ &ignore_show_notes,
+ n->string);
+ load_display_notes(&format_notes_opt);
+ }
+
+ format_ctx = &format_pp;
+ }
+
if (all + annotate_stdin + !!argc > 1) {
error("Specify either a list, or --all, not both!");
usage_with_options(name_rev_usage, opts);
@@ -663,7 +747,7 @@ int cmd_name_rev(int argc,
while (strbuf_getline(&sb, stdin) != EOF) {
strbuf_addch(&sb, '\n');
- name_rev_line(sb.buf, &data);
+ name_rev_line(sb.buf, &data, format_ctx);
}
strbuf_release(&sb);
} else if (all) {
@@ -674,18 +758,20 @@ int cmd_name_rev(int argc,
struct object *obj = get_indexed_object(the_repository, i);
if (!obj || obj->type != OBJ_COMMIT)
continue;
- show_name(obj, NULL,
+ show_name(obj, NULL, format_ctx,
always, allow_undefined, data.name_only);
}
} else {
int i;
for (i = 0; i < revs.nr; i++)
- show_name(revs.objects[i].item, revs.objects[i].name,
+ show_name(revs.objects[i].item, revs.objects[i].name, format_ctx,
always, allow_undefined, data.name_only);
}
string_list_clear(&data.ref_filters, 0);
string_list_clear(&data.exclude_filters, 0);
+ string_list_clear(¬es, 0);
+ release_display_notes(&format_notes_opt);
mem_pool_discard(&string_pool, 0);
object_array_clear(&revs);
return 0;
diff --git a/t/t6120-describe.sh b/t/t6120-describe.sh
index 2c70cc561ad..0b7e9fe396d 100755
--- a/t/t6120-describe.sh
+++ b/t/t6120-describe.sh
@@ -658,6 +658,102 @@ test_expect_success 'name-rev --annotate-stdin works with commitGraph' '
)
'
+test_expect_success 'name-rev --format setup' '
+ mkdir repo-format &&
+ git -C repo-format init &&
+ test_commit -C repo-format first &&
+ test_commit -C repo-format second &&
+ test_commit -C repo-format third &&
+ test_commit -C repo-format fourth &&
+ test_commit -C repo-format fifth &&
+ test_commit -C repo-format sixth &&
+ test_commit -C repo-format seventh &&
+ test_commit -C repo-format eighth
+'
+
+test_expect_success 'name-rev --format --no-name-only' '
+ cat >expect <<-\EOF &&
+ HEAD~3 [fifth]
+ HEAD [eighth]
+ HEAD~5 [third]
+ EOF
+ git -C repo-format name-rev --format="[%s]" \
+ --no-name-only HEAD~3 HEAD HEAD~5 >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'name-rev --format --no-format is the same as regular name-rev' '
+ git -C repo-format name-rev HEAD~2 HEAD~3 >expect &&
+ test_file_not_empty expect &&
+ git -C repo-format name-rev --format="huh?" \
+ --no-format HEAD~2 HEAD~3 >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'name-rev --format=%s for argument revs' '
+ cat >expect <<-\EOF &&
+ eighth
+ seventh
+ fifth
+ EOF
+ git -C repo-format name-rev --format=%s \
+ HEAD HEAD~ HEAD~3 >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success '--name-rev --format=reference --annotate-stdin from rev-list same as log' '
+ git -C repo-format log --format=reference >expect &&
+ test_file_not_empty expect &&
+ git -C repo-format rev-list HEAD >list &&
+ git -C repo-format name-rev --format=reference \
+ --annotate-stdin <list >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success '--name-rev --format=<pretty> --annotate-stdin with running text and tree oid' '
+ cmit_oid=$(git -C repo-format rev-parse :/fifth) &&
+ reference=$(git -C repo-format log -n1 --format=reference :/fifth) &&
+ tree=$(git -C repo-format rev-parse HEAD^{tree}) &&
+ cat >expect <<-EOF &&
+ We thought we fixed this in ${reference}.
+ But look at this tree: ${tree}.
+ EOF
+ git -C repo-format name-rev --format=reference --annotate-stdin \
+ >actual <<-EOF &&
+ We thought we fixed this in ${cmit_oid}.
+ But look at this tree: ${tree}.
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success 'name-rev --format=<pretty> with %N (note)' '
+ test_when_finished "git -C repo-format notes remove" &&
+ git -C repo-format notes add -m"Make a note" &&
+ printf "Make a note\n\n\n" >expect &&
+ git -C repo-format name-rev --format="tformat:%N" \
+ HEAD HEAD~ >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'name-rev --format=<pretty> --notes<ref>' '
+ # One custom notes ref
+ test_when_finished "git -C repo-format notes remove" &&
+ test_when_finished "git -C repo-format notes --ref=word remove" &&
+ git -C repo-format notes add -m"default" &&
+ git -C repo-format notes --ref=word add -m"custom" &&
+ printf "custom\n\n" >expect &&
+ git -C repo-format name-rev --format="tformat:%N" \
+ --notes=word \
+ HEAD >actual &&
+ test_cmp expect actual &&
+ # Glob all
+ printf "default\ncustom\n\n" >expect &&
+ git -C repo-format name-rev --format="tformat:%N" \
+ --notes=* \
+ HEAD >actual &&
+ test_cmp expect actual
+'
+
# B
# o
# H \
--
2.53.0.32.gf6228eaf9cc
^ permalink raw reply related [flat|nested] 45+ messages in thread
* Re: [PATCH v2 2/2] name-rev: learn --format=<pretty>
2026-03-20 13:09 ` [PATCH v2 2/2] name-rev: learn --format=<pretty> kristofferhaugsbakk
@ 2026-03-20 15:25 ` D. Ben Knoble
2026-03-23 17:34 ` Kristoffer Haugsbakk
0 siblings, 1 reply; 45+ messages in thread
From: D. Ben Knoble @ 2026-03-20 15:25 UTC (permalink / raw)
To: kristofferhaugsbakk; +Cc: git, Kristoffer Haugsbakk
On Fri, Mar 20, 2026 at 9:13 AM <kristofferhaugsbakk@fastmail.com> wrote:
>
> From: Kristoffer Haugsbakk <code@khaugsbakk.name>
>
> Teach git-name-rev(1) to format the given revisions instead of creating
> symbolic names.
[snip]
> ---
>
> Notes (series):
> v2:
> • Propely implement “--format implies --name-only”
> • Don’t use a needless intermediary struct
> • Add a new member to `name_rev_usage[]`
> • FREE_AND_NULL notes string. There is no use-after-free but we have no
> reason to leave a freed pointer just laying there
> • Implement `--notes` for `%N` atom use (not just restrict to the default
> notes ref)
[snip]
> diff --git a/Documentation/git-name-rev.adoc b/Documentation/git-name-rev.adoc
> index d4f1c4d5945..65348690c8c 100644
> --- a/Documentation/git-name-rev.adoc
> +++ b/Documentation/git-name-rev.adoc
> @@ -9,7 +9,7 @@ git-name-rev - Find symbolic names for given revs
> SYNOPSIS
> --------
> [verse]
> -'git name-rev' [--tags] [--refs=<pattern>]
> +'git name-rev' [--tags] [--refs=<pattern>] [--format=<pretty>]
> ( --all | --annotate-stdin | <commit-ish>... )
>
> DESCRIPTION
> @@ -21,6 +21,14 @@ format parsable by 'git rev-parse'.
> OPTIONS
> -------
>
> +--format=<pretty>::
> +--no-format::
> + Format revisions instead of outputting symbolic names. The
> + default is `--no-format`.
> ++
> +Implies `--name-only`. The negation `--no-format` implies
> +`--no-name-only` (the default for the command).
> +
> --tags::
> Do not use branch names, but only tags to name the commits
IIUC that this patch also adds --notes, should it be documented here?
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [PATCH v2 2/2] name-rev: learn --format=<pretty>
2026-03-20 15:25 ` D. Ben Knoble
@ 2026-03-23 17:34 ` Kristoffer Haugsbakk
0 siblings, 0 replies; 45+ messages in thread
From: Kristoffer Haugsbakk @ 2026-03-23 17:34 UTC (permalink / raw)
To: D. Ben Knoble; +Cc: git
On Fri, Mar 20, 2026, at 16:25, D. Ben Knoble wrote:
>>[snip]
>> +--format=<pretty>::
>> +--no-format::
>> + Format revisions instead of outputting symbolic names. The
>> + default is `--no-format`.
>> ++
>> +Implies `--name-only`. The negation `--no-format` implies
>> +`--no-name-only` (the default for the command).
>> +
>> --tags::
>> Do not use branch names, but only tags to name the commits
>
> IIUC that this patch also adds --notes, should it be documented here?
Yes certainly. It dropped from my mind when doing the last session for
this version.
Thanks for spotting.
^ permalink raw reply [flat|nested] 45+ messages in thread
* [PATCH v3 0/5] format-rev: introduce builtin for on-demand pretty formatting
2026-03-20 13:09 ` [PATCH v2 0/2] " kristofferhaugsbakk
2026-03-20 13:09 ` [PATCH v2 1/2] name-rev: wrap both blocks in braces kristofferhaugsbakk
2026-03-20 13:09 ` [PATCH v2 2/2] name-rev: learn --format=<pretty> kristofferhaugsbakk
@ 2026-04-28 22:25 ` kristofferhaugsbakk
2026-04-28 22:25 ` [PATCH v3 1/5] name-rev: wrap both blocks in braces kristofferhaugsbakk
` (5 more replies)
2 siblings, 6 replies; 45+ messages in thread
From: kristofferhaugsbakk @ 2026-04-28 22:25 UTC (permalink / raw)
To: git; +Cc: Kristoffer Haugsbakk, ben.knoble
From: Kristoffer Haugsbakk <code@khaugsbakk.name>
(Previous subject: name-rev: learn --format=<pretty>)
Topic name (applied): kh/name-rev-custom-format
Topic summary: Introduce a new builtin for pretty formatting one revision
expression per line or commit object names found in running text.
See the last patch for the motivation. In short there isn’t anything that I
have found that lets you format however many commits you want through one
process (so looping over `git show` is excepted).
The other patches prepare for this change.
§ Changes in v3
Rewrite from a git-name-rev(1) option add-on to a new builtin. I make room
in `builtin/name-rev.c` for it.
This was based on this part from the previous cover letter:
git(1) already has a lot of commands. This doesn’t expand the footprint. On
the other hand, I might be naive about what a proper formatter needs in
terms of command options. Right now there is the helper `--notes`. But is
more needed?
• --abbrev ?
• --date ?
I don’t know. Maybe at some point this would be too much, too crowded, for
git-name-rev(1). Then it might make sense to split it out to a separate
builtin?
I did not want to expand this git-name-rev(1) command with more options
specific to pretty formatting.
§ Why experimental command?
This command is marked Experimental three times (like git-replay(1)).[1]
Not so much because the UI design seems difficult. It’s more so that it can
be thrown out if it doesn’t end up being worth having around.
(*Experimental* also implies wholesale trashing. Right? That’s one possible
change in behavior.)
Or the behavior here could be moved somewhere else.
† 1: I vaguely recall reading a good argument for emphasizing this on the
mailing list. I wasn’t able to find back to it right now.
§ Outstanding work
I could look more into how to do the translation strings for “X is
require” (arg), I’m probably missing something that already exists. I could
probably also link to other commands on the git-format-rev(1) doc. But
right now I wanted to get this out there. There might not be any appetite
for builtin number 148 for this particular niche anyway.
§ Link to v2
https://lore.kernel.org/git/V2_CV_name-rev_--format.51b@msgid.xyz/
[1/5] name-rev: wrap both blocks in braces
[2/5] name-rev: run clang-format before factoring code
[3/5] name-rev: factor code for sharing with a new command
[4/5] name-rev: make dedicated --annotate-stdin --name-only test
[5/5] format-rev: introduce builtin for on-demand pretty formatting
Documentation/git-format-rev.adoc | 148 +++++++++++++++++
Makefile | 1 +
builtin.h | 1 +
builtin/name-rev.c | 264 +++++++++++++++++++++++++++---
command-list.txt | 1 +
git.c | 1 +
t/t1517-outside-repo.sh | 3 +-
t/t6120-describe.sh | 118 +++++++++++++
8 files changed, 511 insertions(+), 26 deletions(-)
create mode 100644 Documentation/git-format-rev.adoc
Interdiff against v2:
diff --git a/Documentation/git-format-rev.adoc b/Documentation/git-format-rev.adoc
new file mode 100644
index 00000000000..d960001d750
--- /dev/null
+++ b/Documentation/git-format-rev.adoc
@@ -0,0 +1,148 @@
+git-format-rev(1)
+=================
+
+NAME
+----
+git-format-rev - EXPERIMENTAL: Pretty format revisions on demand
+
+
+SYNOPSIS
+--------
+[synopsis]
+(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> --format=<pretty> [--notes=<ref>]
+
+DESCRIPTION
+-----------
+
+Pretty format revisions from standard input.
+
+THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
+
+OPTIONS
+-------
+
+`--stdin-mode=<mode>`::
+ How to interpret standard input data:
++
+--
+`revs`:: Each line is interpreted as a commit. Any kind of revision
+ expression can be used (see linkgit:gitrevisions[7]). Annotated
+ tags are peeled (see linkgit:gitglossary[7]).
++
+The argument `rev` is also accepted.
+`text`:: Formats all commit object names found in freeform text. These
+ must the full object names, i.e. abbreviated hexidecimal object
+ names will not be interpreted.
+--
+
+`--format=<pretty>`::
+ Pretty format string.
+
+`--notes=<ref>`::
+`--no-notes`::
+ Custom notes ref. Notes are displayed when using the `%N`
+ atom. See linkgit:git-notes[1].
+
+EXAMPLES
+--------
+
+The command linkgit:git-last-modified[1] shows the commit that each file
+was last modified in.
+
+----
+$ git last-modified -- README.md Makefile
+7798034171030be0909c56377a4e0e10e6d2df93 Makefile
+c50fbb2dd225e7e82abba4380423ae105089f4d7 README.md
+----
+
+We can pipe the result to this command in order to replace the object
+name with the commit author.
+
+----
+$ git last-modified -- README.md Makefile |
+ git format-rev --stdin-mode=text --format=%an
+Junio C Hamano Makefile
+Todd Zullinger README.md
+----
+
+Another example is _formatting commits in commit messages_. Given this commit message:
+
+----
+Fix off-by-one error
+
+Fix off-by-one error introduced in
+e83c5163316f89bfbde7d9ab23ca2e25604af290.
+
+We thought we fixed this in 5569bf9bbedd63a00780fc5c110e0cfab3aa97b9 but
+that only covered 1/3 of the faulty cases.
+----
+
+We can format the commits and use par(1) to reflow the text, say in a
+`commit-msg` hook:
+
+----
+$ git config set hook.reference-commits.event commit-msg
+$ git config set hook.reference-commits.command reference-commits
+$ cat $(which reference-commits)
+#/bin/sh
+
+msg="$1"
+rewritten=$(mktemp)
+git format-rev --stdin-mode=text --format=reference <"$msg" |
+ par >"$rewritten"
+mv "$rewritten" "$msg"
+----
+
+Which will produce something like this:
+
+----
+Fix off-by-one error
+
+Fix off-by-one error introduced in e83c5163316 (Implement better memory
+allocator, 2005-04-07).
+
+We thought we fixed this in 5569bf9bbed (Fix memory allocator,
+2005-06-22) but that only covered 1/3 of the faulty cases.
+----
+
+DISCUSSION
+----------
+
+This command lets you format any number of revisions in any order
+through one command invocation. Consider the
+linkgit:git-last-modified[1] case from the "EXAMPLES" section above:
+
+1. There might be hundreds of files
+2. Commits can be repeated, i.e. two or more files were last modified in
+ the same commit
+
+Two widely-used commands which pretty formats commits are
+linkgit:git-log[1] and linkgit:git-show[1]. It turns out that they are
+not a good fit for the above use case.
+
+- The output of linkgit:git-last-modified[1] would have to be processed
+ in stages since you need to transform the first column separately and
+ then link the author to the filename. But this is surmountable.
+- You can feed each commit to `git show` or `git show --no-walk -1`. But
+ that means that you need to create a process for each line.
+- Let’s say that you want to use one process, not one per line. So you
+ want to feed all the commits to the command. Now you face the problem
+ that you have to feed all the commits to the commands before you get
+ any output (this is also the case for the `--stdin` modes). In other
+ words, you cannot loop through each line, get the author for the
+ commit, and output the author and the filename. You need to feed all
+ the commits, get back all the output, and match the output with the
+ filename.
+- But the next problem is that commands will deduplicate the input and
+ only output one commit one single time only. Thus you cannot make the
+ output order match the input order, since a commit could have been
+ repeated in the original input.
+
+In short, it is straightforward to use these two commands if you use one
+process per line. It is much more work if you just want to use one
+process, but still doable. In contrast, this problem is just another
+shell pipeline with this command.
+
+GIT
+---
+Part of the linkgit:git[1] suite
diff --git a/Documentation/git-name-rev.adoc b/Documentation/git-name-rev.adoc
index 65348690c8c..d4f1c4d5945 100644
--- a/Documentation/git-name-rev.adoc
+++ b/Documentation/git-name-rev.adoc
@@ -9,7 +9,7 @@ git-name-rev - Find symbolic names for given revs
SYNOPSIS
--------
[verse]
-'git name-rev' [--tags] [--refs=<pattern>] [--format=<pretty>]
+'git name-rev' [--tags] [--refs=<pattern>]
( --all | --annotate-stdin | <commit-ish>... )
DESCRIPTION
@@ -21,14 +21,6 @@ format parsable by 'git rev-parse'.
OPTIONS
-------
---format=<pretty>::
---no-format::
- Format revisions instead of outputting symbolic names. The
- default is `--no-format`.
-+
-Implies `--name-only`. The negation `--no-format` implies
-`--no-name-only` (the default for the command).
-
--tags::
Do not use branch names, but only tags to name the commits
diff --git a/Makefile b/Makefile
index 15b1ded1a0b..cbaf91fd846 100644
--- a/Makefile
+++ b/Makefile
@@ -895,6 +895,7 @@ BUILT_INS += $(patsubst builtin/%.o,git-%$X,$(BUILTIN_OBJS))
BUILT_INS += git-cherry$X
BUILT_INS += git-cherry-pick$X
BUILT_INS += git-format-patch$X
+BUILT_INS += git-format-rev$X
BUILT_INS += git-fsck-objects$X
BUILT_INS += git-init$X
BUILT_INS += git-maintenance$X
diff --git a/builtin.h b/builtin.h
index 235c51f30e5..63813c90125 100644
--- a/builtin.h
+++ b/builtin.h
@@ -189,6 +189,7 @@ int cmd_fmt_merge_msg(int argc, const char **argv, const char *prefix, struct re
int cmd_for_each_ref(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_for_each_repo(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_format_patch(int argc, const char **argv, const char *prefix, struct repository *repo);
+int cmd_format_rev(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_fsck(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_fsmonitor__daemon(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_gc(int argc, const char **argv, const char *prefix, struct repository *repo);
diff --git a/builtin/name-rev.c b/builtin/name-rev.c
index 9a008d8b7a8..b60cc766279 100644
--- a/builtin/name-rev.c
+++ b/builtin/name-rev.c
@@ -36,16 +36,6 @@ struct rev_name {
int from_tag;
};
-struct pretty_format {
- struct pretty_print_context ctx;
- struct userformat_want want;
-};
-
-struct format_cb_data {
- const char *format;
- int *name_only;
-};
-
define_commit_slab(commit_rev_name, struct rev_name);
static timestamp_t generation_cutoff = GENERATION_NUMBER_INFINITY;
@@ -285,6 +275,43 @@ struct name_ref_data {
struct string_list exclude_filters;
};
+struct pretty_format {
+ struct pretty_print_context ctx;
+ struct userformat_want want;
+};
+
+enum command_type {
+ NAME_REV = 1,
+ FORMAT_REV = 2,
+};
+
+enum stdin_mode {
+ TEXT = 1,
+ REVS = 2,
+};
+
+struct command {
+ enum command_type type;
+ union {
+ int name_only;
+ struct pretty_format *pretty_format;
+ } u;
+};
+
+static void init_name_rev_command(struct command *cmd,
+ int name_only)
+{
+ cmd->type = NAME_REV;
+ cmd->u.name_only = name_only;
+}
+
+static void init_format_rev_command(struct command *cmd,
+ struct pretty_format *pretty_format)
+{
+ cmd->type = FORMAT_REV;
+ cmd->u.pretty_format = pretty_format;
+}
+
static struct tip_table {
struct tip_table_entry {
struct object_id oid;
@@ -467,9 +494,7 @@ static const char *get_exact_ref_match(const struct object *o)
}
/* may return a constant string or use "buf" as scratch space */
-static const char *get_rev_name(const struct object *o,
- struct pretty_format *format_ctx,
- struct strbuf *buf)
+static const char *get_rev_name(const struct object *o, struct strbuf *buf)
{
struct rev_name *n;
const struct commit *c;
@@ -477,25 +502,6 @@ static const char *get_rev_name(const struct object *o,
if (o->type != OBJ_COMMIT)
return get_exact_ref_match(o);
c = (const struct commit *) o;
-
- if (format_ctx) {
- strbuf_reset(buf);
-
- if (format_ctx->want.notes) {
- struct strbuf notebuf = STRBUF_INIT;
-
- format_display_notes(&c->object.oid, ¬ebuf,
- get_log_output_encoding(),
- format_ctx->ctx.fmt == CMIT_FMT_USERFORMAT);
- format_ctx->ctx.notes_message = strbuf_detach(¬ebuf, NULL);
- }
-
- pretty_print_commit(&format_ctx->ctx, c, buf);
- FREE_AND_NULL(format_ctx->ctx.notes_message);
-
- return buf->buf;
- }
-
n = get_commit_rev_name(c);
if (!n)
return NULL;
@@ -511,9 +517,29 @@ static const char *get_rev_name(const struct object *o,
}
}
+static const char *get_format_rev(const struct commit *c,
+ struct pretty_format *format_ctx,
+ struct strbuf *buf)
+{
+ strbuf_reset(buf);
+
+ if (format_ctx->want.notes) {
+ struct strbuf notebuf = STRBUF_INIT;
+
+ format_display_notes(&c->object.oid, ¬ebuf,
+ get_log_output_encoding(),
+ format_ctx->ctx.fmt == CMIT_FMT_USERFORMAT);
+ format_ctx->ctx.notes_message = strbuf_detach(¬ebuf, NULL);
+ }
+
+ pretty_print_commit(&format_ctx->ctx, c, buf);
+ FREE_AND_NULL(format_ctx->ctx.notes_message);
+
+ return buf->buf;
+}
+
static void show_name(const struct object *obj,
const char *caller_name,
- struct pretty_format *format_ctx,
int always, int allow_undefined, int name_only)
{
const char *name;
@@ -522,7 +548,7 @@ static void show_name(const struct object *obj,
if (!name_only)
printf("%s ", caller_name ? caller_name : oid_to_hex(oid));
- name = get_rev_name(obj, format_ctx, &buf);
+ name = get_rev_name(obj, &buf);
if (name)
printf("%s\n", name);
else if (allow_undefined)
@@ -542,9 +568,7 @@ static char const * const name_rev_usage[] = {
NULL
};
-static void name_rev_line(char *p,
- struct name_ref_data *data,
- struct pretty_format *format_ctx)
+static void name_rev_line(char *p, struct command *cmd)
{
struct strbuf buf = STRBUF_INIT;
int counter = 0;
@@ -553,33 +577,54 @@ static void name_rev_line(char *p,
for (p_start = p; *p; p++) {
#define ishex(x) (isdigit((x)) || ((x) >= 'a' && (x) <= 'f'))
+ start:
if (!ishex(*p)) {
counter = 0;
} else if (++counter == hexsz &&
- !ishex(*(p+1))) {
+ !ishex(*(p + 1))) {
struct object_id oid;
const char *name = NULL;
- char c = *(p+1);
+ char c = *(p + 1);
int p_len = p - p_start + 1;
+ struct object *o = NULL;
+ int oid_ret = 1;
counter = 0;
- *(p+1) = 0;
- if (!repo_get_oid(the_repository, p - (hexsz - 1), &oid)) {
- struct object *o =
- lookup_object(the_repository, &oid);
+ *(p + 1) = 0;
+ oid_ret = repo_get_oid(the_repository, p - (hexsz - 1), &oid);
+
+ switch (cmd->type) {
+ case NAME_REV:
+ if (!oid_ret)
+ o = lookup_object(the_repository, &oid);
if (o)
- name = get_rev_name(o, format_ctx, &buf);
+ name = get_rev_name(o, &buf);
+ *(p + 1) = c;
+ if (!name)
+ goto start;
+ if (cmd->u.name_only)
+ printf("%.*s%s", p_len - hexsz, p_start, name);
+ else
+ printf("%.*s (%s)", p_len, p_start, name);
+ break;
+ case FORMAT_REV:
+ if (!oid_ret)
+ o = parse_object(the_repository, &oid);
+ if (o && o->type == OBJ_COMMIT)
+ name = get_format_rev((const struct commit *)o,
+ cmd->u.pretty_format,
+ &buf);
+ *(p + 1) = c;
+ if (name)
+ printf("%.*s%s", p_len - hexsz, p_start, name);
+ else
+ printf("%.*s", p_len, p_start);
+ break;
+ default:
+ BUG("uncovered case: %d", cmd->type);
}
- *(p+1) = c;
- if (!name)
- continue;
-
- if (data->name_only)
- printf("%.*s%s", p_len - hexsz, p_start, name);
- else
- printf("%.*s (%s)", p_len, p_start, name);
p_start = p + 1;
}
}
@@ -591,16 +636,6 @@ static void name_rev_line(char *p,
strbuf_release(&buf);
}
-static int format_cb(const struct option *option,
- const char *arg,
- int unset)
-{
- struct format_cb_data *data = option->value;
- data->format = arg;
- *data->name_only = !unset;
- return 0;
-}
-
int cmd_name_rev(int argc,
const char **argv,
const char *prefix,
@@ -614,19 +649,14 @@ int cmd_name_rev(int argc,
#endif
int all = 0, annotate_stdin = 0, allow_undefined = 1, always = 0, peel_tag = 0;
struct name_ref_data data = { 0, 0, STRING_LIST_INIT_NODUP, STRING_LIST_INIT_NODUP };
- static struct format_cb_data format_cb_data = { 0 };
- struct display_notes_opt format_notes_opt;
- struct rev_info format_rev = REV_INFO_INIT;
- struct pretty_format *format_ctx = NULL;
- struct pretty_format format_pp = { 0 };
- struct string_list notes = STRING_LIST_INIT_NODUP;
+ struct command cmd;
struct option opts[] = {
OPT_BOOL(0, "name-only", &data.name_only, N_("print only ref-based names (no object names)")),
OPT_BOOL(0, "tags", &data.tags_only, N_("only use tags to name the commits")),
OPT_STRING_LIST(0, "refs", &data.ref_filters, N_("pattern"),
- N_("only use refs matching <pattern>")),
+ N_("only use refs matching <pattern>")),
OPT_STRING_LIST(0, "exclude", &data.exclude_filters, N_("pattern"),
- N_("ignore refs matching <pattern>")),
+ N_("ignore refs matching <pattern>")),
OPT_GROUP(""),
OPT_BOOL(0, "all", &all, N_("list all commits reachable from all refs")),
#ifndef WITH_BREAKING_CHANGES
@@ -637,24 +667,19 @@ int cmd_name_rev(int argc,
PARSE_OPT_HIDDEN),
#endif /* WITH_BREAKING_CHANGES */
OPT_BOOL(0, "annotate-stdin", &annotate_stdin, N_("annotate text from stdin")),
- OPT_CALLBACK(0, "format", &format_cb_data, N_("format"),
- N_("pretty-print output instead"), format_cb),
- OPT_STRING_LIST(0, "notes", ¬es, N_("notes"),
- N_("display notes for --format")),
OPT_BOOL(0, "undefined", &allow_undefined, N_("allow to print `undefined` names (default)")),
- OPT_BOOL(0, "always", &always,
- N_("show abbreviated commit object as fallback")),
+ OPT_BOOL(0, "always", &always,
+ N_("show abbreviated commit object as fallback")),
OPT_HIDDEN_BOOL(0, "peel-tag", &peel_tag,
- N_("dereference tags in the input (internal use)")),
+ N_("dereference tags in the input (internal use)")),
OPT_END(),
};
- init_display_notes(&format_notes_opt);
- format_cb_data.name_only = &data.name_only;
mem_pool_init(&string_pool, 0);
init_commit_rev_name(&rev_names);
repo_config(the_repository, git_default_config, NULL);
argc = parse_options(argc, argv, prefix, opts, name_rev_usage, 0);
+ init_name_rev_command(&cmd, data.name_only);
#ifndef WITH_BREAKING_CHANGES
if (transform_stdin) {
@@ -665,31 +690,6 @@ int cmd_name_rev(int argc,
}
#endif
- if (format_cb_data.format) {
- get_commit_format(format_cb_data.format, &format_rev);
- format_pp.ctx.rev = &format_rev;
- format_pp.ctx.fmt = format_rev.commit_format;
- format_pp.ctx.abbrev = format_rev.abbrev;
- format_pp.ctx.date_mode_explicit = format_rev.date_mode_explicit;
- format_pp.ctx.date_mode = format_rev.date_mode;
- format_pp.ctx.color = GIT_COLOR_AUTO;
-
- userformat_find_requirements(format_cb_data.format,
- &format_pp.want);
- if (format_pp.want.notes) {
- int ignore_show_notes = 0;
- struct string_list_item *n;
-
- for_each_string_list_item(n, ¬es)
- enable_ref_display_notes(&format_notes_opt,
- &ignore_show_notes,
- n->string);
- load_display_notes(&format_notes_opt);
- }
-
- format_ctx = &format_pp;
- }
-
if (all + annotate_stdin + !!argc > 1) {
error("Specify either a list, or --all, not both!");
usage_with_options(name_rev_usage, opts);
@@ -747,7 +747,7 @@ int cmd_name_rev(int argc,
while (strbuf_getline(&sb, stdin) != EOF) {
strbuf_addch(&sb, '\n');
- name_rev_line(sb.buf, &data, format_ctx);
+ name_rev_line(sb.buf, &cmd);
}
strbuf_release(&sb);
} else if (all) {
@@ -758,21 +758,149 @@ int cmd_name_rev(int argc,
struct object *obj = get_indexed_object(the_repository, i);
if (!obj || obj->type != OBJ_COMMIT)
continue;
- show_name(obj, NULL, format_ctx,
+ show_name(obj, NULL,
always, allow_undefined, data.name_only);
}
} else {
int i;
for (i = 0; i < revs.nr; i++)
- show_name(revs.objects[i].item, revs.objects[i].name, format_ctx,
+ show_name(revs.objects[i].item, revs.objects[i].name,
always, allow_undefined, data.name_only);
}
string_list_clear(&data.ref_filters, 0);
string_list_clear(&data.exclude_filters, 0);
- string_list_clear(¬es, 0);
- release_display_notes(&format_notes_opt);
mem_pool_discard(&string_pool, 0);
object_array_clear(&revs);
return 0;
}
+
+static enum stdin_mode parse_stdin_mode(const char *stdin_mode)
+{
+ if (!strcmp(stdin_mode, "text"))
+ return TEXT;
+ else if (!strcmp(stdin_mode, "revs") ||
+ !strcmp(stdin_mode, "rev"))
+ return REVS;
+ else
+ die(_("'%s' needs to be either text, revs, or rev"),
+ "--stdin-mode");
+}
+
+static char const *const format_rev_usage[] = {
+ N_("(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> --format=<pretty> [--notes=<ref>]"),
+ NULL
+};
+
+int cmd_format_rev(int argc,
+ const char **argv,
+ const char *prefix,
+ struct repository *repo UNUSED)
+{
+ const char *format = NULL;
+ enum stdin_mode stdin_mode;
+ const char *stdin_mode_arg = NULL;
+ struct display_notes_opt format_notes_opt;
+ struct rev_info format_rev = REV_INFO_INIT;
+ struct pretty_format format_pp = { 0 };
+ struct string_list notes = STRING_LIST_INIT_NODUP;
+ struct strbuf scratch_buf = STRBUF_INIT;
+ struct command cmd;
+ struct option opts[] = {
+ OPT_STRING(0, "format", &format, N_("format"),
+ N_("pretty format to use")),
+ OPT_STRING(0, "stdin-mode", &stdin_mode_arg, N_("stdin-mode"),
+ N_("how revs are processed")),
+ OPT_STRING_LIST(0, "notes", ¬es, N_("notes"),
+ N_("display notes for pretty format")),
+ OPT_END(),
+ };
+
+ argc = parse_options(argc, argv, prefix, opts, format_rev_usage, 0);
+
+ if (argc > 0) {
+ error(_("too many arguments"));
+ usage_with_options(format_rev_usage, opts);
+ }
+
+ if (!format)
+ die(_("'%s' is required"), "--format");
+ if (!stdin_mode_arg)
+ die(_("'%s' is required"), "--stdin-mode");
+
+ init_display_notes(&format_notes_opt);
+ stdin_mode = parse_stdin_mode(stdin_mode_arg);
+
+ get_commit_format(format, &format_rev);
+ format_pp.ctx.rev = &format_rev;
+ format_pp.ctx.fmt = format_rev.commit_format;
+ format_pp.ctx.abbrev = format_rev.abbrev;
+ format_pp.ctx.date_mode_explicit = format_rev.date_mode_explicit;
+ format_pp.ctx.date_mode = format_rev.date_mode;
+ format_pp.ctx.color = GIT_COLOR_AUTO;
+
+ userformat_find_requirements(format,
+ &format_pp.want);
+ if (format_pp.want.notes) {
+ int ignore_show_notes = 0;
+ struct string_list_item *n;
+
+ for_each_string_list_item(n, ¬es)
+ enable_ref_display_notes(&format_notes_opt,
+ &ignore_show_notes,
+ n->string);
+ load_display_notes(&format_notes_opt);
+ }
+
+ init_format_rev_command(&cmd, &format_pp);
+
+ switch (stdin_mode) {
+ case TEXT:
+ while (strbuf_getline(&scratch_buf, stdin) != EOF) {
+ strbuf_addch(&scratch_buf, '\n');
+ name_rev_line(scratch_buf.buf, &cmd);
+ }
+ break;
+ case REVS:
+ while (strbuf_getline(&scratch_buf, stdin) != EOF) {
+ struct object_id oid;
+ struct object *object;
+ struct object *peeled;
+ struct commit *commit;
+
+ if (repo_get_oid(the_repository, scratch_buf.buf, &oid)) {
+ fprintf(stderr, "Could not get sha1 for %s. Skipping.\n",
+ scratch_buf.buf);
+ continue;
+ }
+
+ object = parse_object(the_repository, &oid);
+ if (!object) {
+ fprintf(stderr, "Could not get object for %s. Skipping.\n",
+ scratch_buf.buf);
+ continue;
+ }
+
+ peeled = deref_tag(the_repository, object, scratch_buf.buf, 0);
+ if (peeled && peeled->type == OBJ_COMMIT)
+ commit = (struct commit *)peeled;
+ if (!commit) {
+ fprintf(stderr, "Could not get commit for %s. Skipping.\n",
+ *argv);
+ continue;
+ }
+
+ get_format_rev(commit, &format_pp, &scratch_buf);
+ printf("%s\n", scratch_buf.buf);
+ strbuf_release(&scratch_buf);
+ }
+ break;
+ default:
+ BUG("uncovered case: %d", stdin_mode);
+ }
+
+ strbuf_release(&scratch_buf);
+ string_list_clear(¬es, 0);
+ release_display_notes(&format_notes_opt);
+ return 0;
+}
diff --git a/command-list.txt b/command-list.txt
index f9005cf4597..df729872dca 100644
--- a/command-list.txt
+++ b/command-list.txt
@@ -108,6 +108,7 @@ git-fmt-merge-msg purehelpers
git-for-each-ref plumbinginterrogators
git-for-each-repo plumbinginterrogators
git-format-patch mainporcelain
+git-format-rev plumbinginterrogators
git-fsck ancillaryinterrogators complete
git-gc mainporcelain
git-get-tar-commit-id plumbinginterrogators
diff --git a/git.c b/git.c
index 2b212e6675d..af5b0422b00 100644
--- a/git.c
+++ b/git.c
@@ -578,6 +578,7 @@ static struct cmd_struct commands[] = {
{ "for-each-ref", cmd_for_each_ref, RUN_SETUP },
{ "for-each-repo", cmd_for_each_repo, RUN_SETUP_GENTLY },
{ "format-patch", cmd_format_patch, RUN_SETUP },
+ { "format-rev", cmd_format_rev, RUN_SETUP },
{ "fsck", cmd_fsck, RUN_SETUP },
{ "fsck-objects", cmd_fsck, RUN_SETUP },
{ "fsmonitor--daemon", cmd_fsmonitor__daemon, RUN_SETUP },
diff --git a/t/t1517-outside-repo.sh b/t/t1517-outside-repo.sh
index c824c1a25cf..360a9323343 100755
--- a/t/t1517-outside-repo.sh
+++ b/t/t1517-outside-repo.sh
@@ -114,7 +114,8 @@ do
archimport | citool | credential-netrc | credential-libsecret | \
credential-osxkeychain | cvsexportcommit | cvsimport | cvsserver | \
daemon | \
- difftool--helper | filter-branch | fsck-objects | get-tar-commit-id | \
+ difftool--helper | filter-branch | format-rev | fsck-objects | \
+ get-tar-commit-id | \
gui | gui--askpass | \
http-backend | http-fetch | http-push | init-db | \
merge-octopus | merge-one-file | merge-resolve | mergetool | \
diff --git a/t/t6120-describe.sh b/t/t6120-describe.sh
index 0b7e9fe396d..725f7d81b6b 100755
--- a/t/t6120-describe.sh
+++ b/t/t6120-describe.sh
@@ -298,6 +298,20 @@ test_expect_success 'name-rev --annotate-stdin' '
test_cmp expect actual
'
+test_expect_success 'name-rev --annotate-stdin --name-only' '
+ >expect.unsorted &&
+ for rev in $(git rev-list --all)
+ do
+ name=$(git name-rev --name-only $rev) &&
+ echo "$name" >>expect.unsorted || return 1
+ done &&
+ sort <expect.unsorted >expect &&
+ git name-rev --annotate-stdin --name-only \
+ <list >actual.unsorted &&
+ sort <actual.unsorted >actual &&
+ test_cmp expect actual
+'
+
test_expect_success 'name-rev --stdin deprecated' '
git rev-list --all >list &&
if ! test_have_prereq WITH_BREAKING_CHANGES
@@ -658,102 +672,6 @@ test_expect_success 'name-rev --annotate-stdin works with commitGraph' '
)
'
-test_expect_success 'name-rev --format setup' '
- mkdir repo-format &&
- git -C repo-format init &&
- test_commit -C repo-format first &&
- test_commit -C repo-format second &&
- test_commit -C repo-format third &&
- test_commit -C repo-format fourth &&
- test_commit -C repo-format fifth &&
- test_commit -C repo-format sixth &&
- test_commit -C repo-format seventh &&
- test_commit -C repo-format eighth
-'
-
-test_expect_success 'name-rev --format --no-name-only' '
- cat >expect <<-\EOF &&
- HEAD~3 [fifth]
- HEAD [eighth]
- HEAD~5 [third]
- EOF
- git -C repo-format name-rev --format="[%s]" \
- --no-name-only HEAD~3 HEAD HEAD~5 >actual &&
- test_cmp expect actual
-'
-
-test_expect_success 'name-rev --format --no-format is the same as regular name-rev' '
- git -C repo-format name-rev HEAD~2 HEAD~3 >expect &&
- test_file_not_empty expect &&
- git -C repo-format name-rev --format="huh?" \
- --no-format HEAD~2 HEAD~3 >actual &&
- test_cmp expect actual
-'
-
-test_expect_success 'name-rev --format=%s for argument revs' '
- cat >expect <<-\EOF &&
- eighth
- seventh
- fifth
- EOF
- git -C repo-format name-rev --format=%s \
- HEAD HEAD~ HEAD~3 >actual &&
- test_cmp expect actual
-'
-
-test_expect_success '--name-rev --format=reference --annotate-stdin from rev-list same as log' '
- git -C repo-format log --format=reference >expect &&
- test_file_not_empty expect &&
- git -C repo-format rev-list HEAD >list &&
- git -C repo-format name-rev --format=reference \
- --annotate-stdin <list >actual &&
- test_cmp expect actual
-'
-
-test_expect_success '--name-rev --format=<pretty> --annotate-stdin with running text and tree oid' '
- cmit_oid=$(git -C repo-format rev-parse :/fifth) &&
- reference=$(git -C repo-format log -n1 --format=reference :/fifth) &&
- tree=$(git -C repo-format rev-parse HEAD^{tree}) &&
- cat >expect <<-EOF &&
- We thought we fixed this in ${reference}.
- But look at this tree: ${tree}.
- EOF
- git -C repo-format name-rev --format=reference --annotate-stdin \
- >actual <<-EOF &&
- We thought we fixed this in ${cmit_oid}.
- But look at this tree: ${tree}.
- EOF
- test_cmp expect actual
-'
-
-test_expect_success 'name-rev --format=<pretty> with %N (note)' '
- test_when_finished "git -C repo-format notes remove" &&
- git -C repo-format notes add -m"Make a note" &&
- printf "Make a note\n\n\n" >expect &&
- git -C repo-format name-rev --format="tformat:%N" \
- HEAD HEAD~ >actual &&
- test_cmp expect actual
-'
-
-test_expect_success 'name-rev --format=<pretty> --notes<ref>' '
- # One custom notes ref
- test_when_finished "git -C repo-format notes remove" &&
- test_when_finished "git -C repo-format notes --ref=word remove" &&
- git -C repo-format notes add -m"default" &&
- git -C repo-format notes --ref=word add -m"custom" &&
- printf "custom\n\n" >expect &&
- git -C repo-format name-rev --format="tformat:%N" \
- --notes=word \
- HEAD >actual &&
- test_cmp expect actual &&
- # Glob all
- printf "default\ncustom\n\n" >expect &&
- git -C repo-format name-rev --format="tformat:%N" \
- --notes=* \
- HEAD >actual &&
- test_cmp expect actual
-'
-
# B
# o
# H \
@@ -883,4 +801,108 @@ test_expect_success 'do not be fooled by invalid describe format ' '
test_must_fail git cat-file -t "refs/tags/super-invalid/./../...../ ~^:/?*[////\\\\\\&}/busted.lock-42-g"$(cat out)
'
+test_expect_success 'name-rev --format setup' '
+ mkdir repo-format &&
+ git -C repo-format init &&
+ test_commit -C repo-format first &&
+ test_commit -C repo-format second &&
+ test_commit -C repo-format third &&
+ test_commit -C repo-format fourth &&
+ test_commit -C repo-format fifth &&
+ test_commit -C repo-format sixth &&
+ test_commit -C repo-format seventh &&
+ test_commit -C repo-format eighth
+'
+
+test_expect_success 'format-rev --stdin-mode=revs' '
+ cat >expect <<-\EOF &&
+ eighth
+ seventh
+ fifth
+ EOF
+ git -C repo-format format-rev --stdin-mode=revs \
+ --format=%s >actual <<-\EOF &&
+ HEAD
+ HEAD~
+ HEAD~3
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success 'format-rev --stdin-mode=text from rev-list same as log' '
+ git -C repo-format log --format=reference >expect &&
+ test_file_not_empty expect &&
+ git -C repo-format rev-list HEAD >list &&
+ git -C repo-format format-rev --stdin-mode=text \
+ --format=reference <list >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'format-rev --stdin-mode=text with running text and tree oid' '
+ cmit_oid=$(git -C repo-format rev-parse :/fifth) &&
+ reference=$(git -C repo-format log -n1 --format=reference :/fifth) &&
+ tree=$(git -C repo-format rev-parse HEAD^{tree}) &&
+ cat >expect <<-EOF &&
+ We thought we fixed this in ${reference}.
+ But look at this tree: ${tree}.
+ EOF
+ git -C repo-format format-rev --stdin-mode=text --format=reference \
+ >actual <<-EOF &&
+ We thought we fixed this in ${cmit_oid}.
+ But look at this tree: ${tree}.
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success 'format-rev with %N (note)' '
+ test_when_finished "git -C repo-format notes remove" &&
+ git -C repo-format notes add -m"Make a note" &&
+ printf "Make a note\n\n\n" >expect &&
+ git -C repo-format format-rev --stdin-mode=revs \
+ --format="tformat:%N" \
+ >actual <<-\EOF &&
+ HEAD
+ HEAD~
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success 'format-rev --notes<ref> (custom notes ref)' '
+ # One custom notes ref
+ test_when_finished "git -C repo-format notes remove" &&
+ test_when_finished "git -C repo-format notes --ref=word remove" &&
+ git -C repo-format notes add -m"default" &&
+ git -C repo-format notes --ref=word add -m"custom" &&
+ printf "custom\n\n" >expect &&
+ git -C repo-format format-rev --stdin-mode=revs \
+ --format="tformat:%N" \
+ --notes=word \
+ >actual <<-\EOF &&
+ HEAD
+ EOF
+ test_cmp expect actual &&
+ # Glob all
+ printf "default\ncustom\n\n" >expect &&
+ git -C repo-format format-rev --stdin-mode=revs \
+ --format="tformat:%N" \
+ --notes=* >actual <<-\EOF &&
+ HEAD
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success 'format-rev --stdin-mode=revs on annotated tag peels to commit' '
+ test_when_finished "git -C repo-format tag -d version" &&
+ git -C repo-format tag -a -m"new version" version &&
+ cat >expect <<-\EOF &&
+ eighth
+ EOF
+ git -C repo-format format-rev --stdin-mode=revs \
+ --format=%s \
+ --notes=* >actual <<-\EOF &&
+ version
+ EOF
+ test_cmp expect actual
+'
+
test_done
Range-diff against v2:
1: 9cb5cfd1ec3 = 1: 9cb5cfd1ec3 name-rev: wrap both blocks in braces
-: ----------- > 2: 14900271321 name-rev: run clang-format before factoring code
-: ----------- > 3: 1f17f0b0090 name-rev: factor code for sharing with a new command
-: ----------- > 4: 40989b3672a name-rev: make dedicated --annotate-stdin --name-only test
2: 52a52060776 ! 5: bb54e8f753e name-rev: learn --format=<pretty>
@@ Metadata
Author: Kristoffer Haugsbakk <code@khaugsbakk.name>
## Commit message ##
- name-rev: learn --format=<pretty>
+ format-rev: introduce builtin for on-demand pretty formatting
- Teach git-name-rev(1) to format the given revisions instead of creating
- symbolic names.
+ Introduce a new builtin for pretty formatting one revision expression
+ per line or commit object names found in running text.
- Sometimes you want to format commits. Most of the time you’re walking
- the graph, e.g. getting a range of commits like `master..topic`. That’s
- a job for git-log(1).
+ Sometimes you want to format commits. Most of the time you’re
+ walking the graph, e.g. getting a range of commits like
+ `master..topic`. That’s a job for git-log(1).
- But sometimes you might want to format commits that you encounter
+ But there are times when you want to format commits that you encounter
on demand:
• Full hashes in running text that you might want to pretty-print
- • git-last-modified(1) outputs full hashes that you can do the same with
+ • git-last-modified(1) outputs full hashes that you can do the same
+ with
• git-cherry(1) has `-v` for commit subject, but maybe you want
something else?
But now you can’t use git-log(1), git-show(1), or git-rev-list(1):
- • You can’t feed commits piecemeal to these commands, one input for one
- output; they block until standard in is closed
+ • You can’t feed commits piecemeal to these commands, one input
+ for one output; they block until standard in is closed
• You can’t feed a list of possibly duplicate commits, like the output
of git-last-modified(1); they effectively deduplicate the output
Beyond these two points there’s also the input massage problem: you
cannot feed mixed input (revisions mixed with arbitrary text).
- One might hope that git-cat-file(1) can save us. But it doesn’t support
- pretty formats.
+ One might hope that git-cat-file(1) can save us. But it doesn’t
+ support pretty formats.
But there is one command that already both handles revisions as
- arguments, revisions on standard input, and even revisions mixed
- in with arbitrary text. Namely git-name-rev(1).
+ arguments, revisions on standard input, and even revisions mixed in
+ with arbitrary text. Namely git-name-rev(1): the command for outputting
+ symbolic names for commits.
- Teach it to work in a format mode where the output for each revision is
- the pretty output (implies `--name-only`). This can be used to format
- any revision expression when given as arguments, and all full commit
- hashes in running text on stdin.
+ We made some room in `builtin/name-rev.c` two commits ago. Let’s
+ now add this new git-format-rev(1) command. Taking inspiration from
+ git-name-rev(1), there are two modes:
- Just bring the hashes (to the pipeline). We will pretty print them.
+ • revs: like git-name-rev(1) in argv mode, but one revision per line
+ on standard in
+ • text: like git-name-rev(1) with `--annotate-stdin`
+
+ ***
+
+ We need to add this command to the exception list in
+ `t/t1517-outside-repo.sh` because it uses “EXPERIMENTAL!”
+ in the usage line.
Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
- ## Documentation/git-name-rev.adoc ##
-@@ Documentation/git-name-rev.adoc: git-name-rev - Find symbolic names for given revs
- SYNOPSIS
- --------
- [verse]
--'git name-rev' [--tags] [--refs=<pattern>]
-+'git name-rev' [--tags] [--refs=<pattern>] [--format=<pretty>]
- ( --all | --annotate-stdin | <commit-ish>... )
-
- DESCRIPTION
-@@ Documentation/git-name-rev.adoc: format parsable by 'git rev-parse'.
- OPTIONS
- -------
-
-+--format=<pretty>::
-+--no-format::
-+ Format revisions instead of outputting symbolic names. The
-+ default is `--no-format`.
+ ## Documentation/git-format-rev.adoc (new) ##
+@@
++git-format-rev(1)
++=================
++
++NAME
++----
++git-format-rev - EXPERIMENTAL: Pretty format revisions on demand
++
++
++SYNOPSIS
++--------
++[synopsis]
++(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> --format=<pretty> [--notes=<ref>]
++
++DESCRIPTION
++-----------
++
++Pretty format revisions from standard input.
++
++THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
++
++OPTIONS
++-------
++
++`--stdin-mode=<mode>`::
++ How to interpret standard input data:
+++
++--
++`revs`:: Each line is interpreted as a commit. Any kind of revision
++ expression can be used (see linkgit:gitrevisions[7]). Annotated
++ tags are peeled (see linkgit:gitglossary[7]).
++
-+Implies `--name-only`. The negation `--no-format` implies
-+`--no-name-only` (the default for the command).
++The argument `rev` is also accepted.
++`text`:: Formats all commit object names found in freeform text. These
++ must the full object names, i.e. abbreviated hexidecimal object
++ names will not be interpreted.
++--
+
- --tags::
- Do not use branch names, but only tags to name the commits
-
++`--format=<pretty>`::
++ Pretty format string.
++
++`--notes=<ref>`::
++`--no-notes`::
++ Custom notes ref. Notes are displayed when using the `%N`
++ atom. See linkgit:git-notes[1].
++
++EXAMPLES
++--------
++
++The command linkgit:git-last-modified[1] shows the commit that each file
++was last modified in.
++
++----
++$ git last-modified -- README.md Makefile
++7798034171030be0909c56377a4e0e10e6d2df93 Makefile
++c50fbb2dd225e7e82abba4380423ae105089f4d7 README.md
++----
++
++We can pipe the result to this command in order to replace the object
++name with the commit author.
++
++----
++$ git last-modified -- README.md Makefile |
++ git format-rev --stdin-mode=text --format=%an
++Junio C Hamano Makefile
++Todd Zullinger README.md
++----
++
++Another example is _formatting commits in commit messages_. Given this commit message:
++
++----
++Fix off-by-one error
++
++Fix off-by-one error introduced in
++e83c5163316f89bfbde7d9ab23ca2e25604af290.
++
++We thought we fixed this in 5569bf9bbedd63a00780fc5c110e0cfab3aa97b9 but
++that only covered 1/3 of the faulty cases.
++----
++
++We can format the commits and use par(1) to reflow the text, say in a
++`commit-msg` hook:
++
++----
++$ git config set hook.reference-commits.event commit-msg
++$ git config set hook.reference-commits.command reference-commits
++$ cat $(which reference-commits)
++#/bin/sh
++
++msg="$1"
++rewritten=$(mktemp)
++git format-rev --stdin-mode=text --format=reference <"$msg" |
++ par >"$rewritten"
++mv "$rewritten" "$msg"
++----
++
++Which will produce something like this:
++
++----
++Fix off-by-one error
++
++Fix off-by-one error introduced in e83c5163316 (Implement better memory
++allocator, 2005-04-07).
++
++We thought we fixed this in 5569bf9bbed (Fix memory allocator,
++2005-06-22) but that only covered 1/3 of the faulty cases.
++----
++
++DISCUSSION
++----------
++
++This command lets you format any number of revisions in any order
++through one command invocation. Consider the
++linkgit:git-last-modified[1] case from the "EXAMPLES" section above:
++
++1. There might be hundreds of files
++2. Commits can be repeated, i.e. two or more files were last modified in
++ the same commit
++
++Two widely-used commands which pretty formats commits are
++linkgit:git-log[1] and linkgit:git-show[1]. It turns out that they are
++not a good fit for the above use case.
++
++- The output of linkgit:git-last-modified[1] would have to be processed
++ in stages since you need to transform the first column separately and
++ then link the author to the filename. But this is surmountable.
++- You can feed each commit to `git show` or `git show --no-walk -1`. But
++ that means that you need to create a process for each line.
++- Let’s say that you want to use one process, not one per line. So you
++ want to feed all the commits to the command. Now you face the problem
++ that you have to feed all the commits to the commands before you get
++ any output (this is also the case for the `--stdin` modes). In other
++ words, you cannot loop through each line, get the author for the
++ commit, and output the author and the filename. You need to feed all
++ the commits, get back all the output, and match the output with the
++ filename.
++- But the next problem is that commands will deduplicate the input and
++ only output one commit one single time only. Thus you cannot make the
++ output order match the input order, since a commit could have been
++ repeated in the original input.
++
++In short, it is straightforward to use these two commands if you use one
++process per line. It is much more work if you just want to use one
++process, but still doable. In contrast, this problem is just another
++shell pipeline with this command.
++
++GIT
++---
++Part of the linkgit:git[1] suite
+
+ ## Makefile ##
+@@ Makefile: BUILT_INS += $(patsubst builtin/%.o,git-%$X,$(BUILTIN_OBJS))
+ BUILT_INS += git-cherry$X
+ BUILT_INS += git-cherry-pick$X
+ BUILT_INS += git-format-patch$X
++BUILT_INS += git-format-rev$X
+ BUILT_INS += git-fsck-objects$X
+ BUILT_INS += git-init$X
+ BUILT_INS += git-maintenance$X
+
+ ## builtin.h ##
+@@ builtin.h: int cmd_fmt_merge_msg(int argc, const char **argv, const char *prefix, struct re
+ int cmd_for_each_ref(int argc, const char **argv, const char *prefix, struct repository *repo);
+ int cmd_for_each_repo(int argc, const char **argv, const char *prefix, struct repository *repo);
+ int cmd_format_patch(int argc, const char **argv, const char *prefix, struct repository *repo);
++int cmd_format_rev(int argc, const char **argv, const char *prefix, struct repository *repo);
+ int cmd_fsck(int argc, const char **argv, const char *prefix, struct repository *repo);
+ int cmd_fsmonitor__daemon(int argc, const char **argv, const char *prefix, struct repository *repo);
+ int cmd_gc(int argc, const char **argv, const char *prefix, struct repository *repo);
## builtin/name-rev.c ##
@@
@@ builtin/name-rev.c
/*
* One day. See the 'name a rev shortly after epoch' test in t6120 when
-@@ builtin/name-rev.c: struct rev_name {
- int from_tag;
+@@ builtin/name-rev.c: struct name_ref_data {
+ struct string_list exclude_filters;
};
+struct pretty_format {
@@ builtin/name-rev.c: struct rev_name {
+ struct userformat_want want;
+};
+
-+struct format_cb_data {
-+ const char *format;
-+ int *name_only;
+ enum command_type {
+ NAME_REV = 1,
++ FORMAT_REV = 2,
+};
+
- define_commit_slab(commit_rev_name, struct rev_name);
++enum stdin_mode {
++ TEXT = 1,
++ REVS = 2,
+ };
+
+ struct command {
+ enum command_type type;
+ union {
+ int name_only;
++ struct pretty_format *pretty_format;
+ } u;
+ };
- static timestamp_t generation_cutoff = GENERATION_NUMBER_INFINITY;
-@@ builtin/name-rev.c: static const char *get_exact_ref_match(const struct object *o)
+@@ builtin/name-rev.c: static void init_name_rev_command(struct command *cmd,
+ cmd->u.name_only = name_only;
}
- /* may return a constant string or use "buf" as scratch space */
--static const char *get_rev_name(const struct object *o, struct strbuf *buf)
-+static const char *get_rev_name(const struct object *o,
-+ struct pretty_format *format_ctx,
-+ struct strbuf *buf)
- {
- struct rev_name *n;
- const struct commit *c;
-@@ builtin/name-rev.c: static const char *get_rev_name(const struct object *o, struct strbuf *buf)
- if (o->type != OBJ_COMMIT)
- return get_exact_ref_match(o);
- c = (const struct commit *) o;
++static void init_format_rev_command(struct command *cmd,
++ struct pretty_format *pretty_format)
++{
++ cmd->type = FORMAT_REV;
++ cmd->u.pretty_format = pretty_format;
++}
+
-+ if (format_ctx) {
-+ strbuf_reset(buf);
+ static struct tip_table {
+ struct tip_table_entry {
+ struct object_id oid;
+@@ builtin/name-rev.c: static const char *get_rev_name(const struct object *o, struct strbuf *buf)
+ }
+ }
+
++static const char *get_format_rev(const struct commit *c,
++ struct pretty_format *format_ctx,
++ struct strbuf *buf)
++{
++ strbuf_reset(buf);
+
-+ if (format_ctx->want.notes) {
-+ struct strbuf notebuf = STRBUF_INIT;
++ if (format_ctx->want.notes) {
++ struct strbuf notebuf = STRBUF_INIT;
+
-+ format_display_notes(&c->object.oid, ¬ebuf,
-+ get_log_output_encoding(),
-+ format_ctx->ctx.fmt == CMIT_FMT_USERFORMAT);
-+ format_ctx->ctx.notes_message = strbuf_detach(¬ebuf, NULL);
-+ }
++ format_display_notes(&c->object.oid, ¬ebuf,
++ get_log_output_encoding(),
++ format_ctx->ctx.fmt == CMIT_FMT_USERFORMAT);
++ format_ctx->ctx.notes_message = strbuf_detach(¬ebuf, NULL);
++ }
+
-+ pretty_print_commit(&format_ctx->ctx, c, buf);
-+ FREE_AND_NULL(format_ctx->ctx.notes_message);
++ pretty_print_commit(&format_ctx->ctx, c, buf);
++ FREE_AND_NULL(format_ctx->ctx.notes_message);
+
-+ return buf->buf;
-+ }
++ return buf->buf;
++}
+
- n = get_commit_rev_name(c);
- if (!n)
- return NULL;
-@@ builtin/name-rev.c: static const char *get_rev_name(const struct object *o, struct strbuf *buf)
-
static void show_name(const struct object *obj,
const char *caller_name,
-+ struct pretty_format *format_ctx,
int always, int allow_undefined, int name_only)
- {
- const char *name;
-@@ builtin/name-rev.c: static void show_name(const struct object *obj,
-
- if (!name_only)
- printf("%s ", caller_name ? caller_name : oid_to_hex(oid));
-- name = get_rev_name(obj, &buf);
-+ name = get_rev_name(obj, format_ctx, &buf);
- if (name)
- printf("%s\n", name);
- else if (allow_undefined)
-@@ builtin/name-rev.c: static char const * const name_rev_usage[] = {
- NULL
- };
-
--static void name_rev_line(char *p, struct name_ref_data *data)
-+static void name_rev_line(char *p,
-+ struct name_ref_data *data,
-+ struct pretty_format *format_ctx)
- {
- struct strbuf buf = STRBUF_INIT;
- int counter = 0;
-@@ builtin/name-rev.c: static void name_rev_line(char *p, struct name_ref_data *data)
- struct object *o =
- lookup_object(the_repository, &oid);
- if (o)
-- name = get_rev_name(o, &buf);
-+ name = get_rev_name(o, format_ctx, &buf);
+@@ builtin/name-rev.c: static void name_rev_line(char *p, struct command *cmd)
+ else
+ printf("%.*s (%s)", p_len, p_start, name);
+ break;
++ case FORMAT_REV:
++ if (!oid_ret)
++ o = parse_object(the_repository, &oid);
++ if (o && o->type == OBJ_COMMIT)
++ name = get_format_rev((const struct commit *)o,
++ cmd->u.pretty_format,
++ &buf);
++ *(p + 1) = c;
++ if (name)
++ printf("%.*s%s", p_len - hexsz, p_start, name);
++ else
++ printf("%.*s", p_len, p_start);
++ break;
+ default:
+ BUG("uncovered case: %d", cmd->type);
}
- *(p+1) = c;
-
-@@ builtin/name-rev.c: static void name_rev_line(char *p, struct name_ref_data *data)
- strbuf_release(&buf);
+@@ builtin/name-rev.c: int cmd_name_rev(int argc,
+ object_array_clear(&revs);
+ return 0;
}
-
-+static int format_cb(const struct option *option,
-+ const char *arg,
-+ int unset)
++
++static enum stdin_mode parse_stdin_mode(const char *stdin_mode)
+{
-+ struct format_cb_data *data = option->value;
-+ data->format = arg;
-+ *data->name_only = !unset;
-+ return 0;
++ if (!strcmp(stdin_mode, "text"))
++ return TEXT;
++ else if (!strcmp(stdin_mode, "revs") ||
++ !strcmp(stdin_mode, "rev"))
++ return REVS;
++ else
++ die(_("'%s' needs to be either text, revs, or rev"),
++ "--stdin-mode");
+}
+
- int cmd_name_rev(int argc,
- const char **argv,
- const char *prefix,
-@@ builtin/name-rev.c: int cmd_name_rev(int argc,
- #endif
- int all = 0, annotate_stdin = 0, allow_undefined = 1, always = 0, peel_tag = 0;
- struct name_ref_data data = { 0, 0, STRING_LIST_INIT_NODUP, STRING_LIST_INIT_NODUP };
-+ static struct format_cb_data format_cb_data = { 0 };
++static char const *const format_rev_usage[] = {
++ N_("(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> --format=<pretty> [--notes=<ref>]"),
++ NULL
++};
++
++int cmd_format_rev(int argc,
++ const char **argv,
++ const char *prefix,
++ struct repository *repo UNUSED)
++{
++ const char *format = NULL;
++ enum stdin_mode stdin_mode;
++ const char *stdin_mode_arg = NULL;
+ struct display_notes_opt format_notes_opt;
+ struct rev_info format_rev = REV_INFO_INIT;
-+ struct pretty_format *format_ctx = NULL;
+ struct pretty_format format_pp = { 0 };
+ struct string_list notes = STRING_LIST_INIT_NODUP;
- struct option opts[] = {
- OPT_BOOL(0, "name-only", &data.name_only, N_("print only ref-based names (no object names)")),
- OPT_BOOL(0, "tags", &data.tags_only, N_("only use tags to name the commits")),
-@@ builtin/name-rev.c: int cmd_name_rev(int argc,
- PARSE_OPT_HIDDEN),
- #endif /* WITH_BREAKING_CHANGES */
- OPT_BOOL(0, "annotate-stdin", &annotate_stdin, N_("annotate text from stdin")),
-+ OPT_CALLBACK(0, "format", &format_cb_data, N_("format"),
-+ N_("pretty-print output instead"), format_cb),
++ struct strbuf scratch_buf = STRBUF_INIT;
++ struct command cmd;
++ struct option opts[] = {
++ OPT_STRING(0, "format", &format, N_("format"),
++ N_("pretty format to use")),
++ OPT_STRING(0, "stdin-mode", &stdin_mode_arg, N_("stdin-mode"),
++ N_("how revs are processed")),
+ OPT_STRING_LIST(0, "notes", ¬es, N_("notes"),
-+ N_("display notes for --format")),
- OPT_BOOL(0, "undefined", &allow_undefined, N_("allow to print `undefined` names (default)")),
- OPT_BOOL(0, "always", &always,
- N_("show abbreviated commit object as fallback")),
-@@ builtin/name-rev.c: int cmd_name_rev(int argc,
- OPT_END(),
- };
-
++ N_("display notes for pretty format")),
++ OPT_END(),
++ };
++
++ argc = parse_options(argc, argv, prefix, opts, format_rev_usage, 0);
++
++ if (argc > 0) {
++ error(_("too many arguments"));
++ usage_with_options(format_rev_usage, opts);
++ }
++
++ if (!format)
++ die(_("'%s' is required"), "--format");
++ if (!stdin_mode_arg)
++ die(_("'%s' is required"), "--stdin-mode");
++
+ init_display_notes(&format_notes_opt);
-+ format_cb_data.name_only = &data.name_only;
- mem_pool_init(&string_pool, 0);
- init_commit_rev_name(&rev_names);
- repo_config(the_repository, git_default_config, NULL);
-@@ builtin/name-rev.c: int cmd_name_rev(int argc,
- }
- #endif
-
-+ if (format_cb_data.format) {
-+ get_commit_format(format_cb_data.format, &format_rev);
-+ format_pp.ctx.rev = &format_rev;
-+ format_pp.ctx.fmt = format_rev.commit_format;
-+ format_pp.ctx.abbrev = format_rev.abbrev;
-+ format_pp.ctx.date_mode_explicit = format_rev.date_mode_explicit;
-+ format_pp.ctx.date_mode = format_rev.date_mode;
-+ format_pp.ctx.color = GIT_COLOR_AUTO;
-+
-+ userformat_find_requirements(format_cb_data.format,
-+ &format_pp.want);
-+ if (format_pp.want.notes) {
-+ int ignore_show_notes = 0;
-+ struct string_list_item *n;
-+
-+ for_each_string_list_item(n, ¬es)
-+ enable_ref_display_notes(&format_notes_opt,
-+ &ignore_show_notes,
-+ n->string);
-+ load_display_notes(&format_notes_opt);
++ stdin_mode = parse_stdin_mode(stdin_mode_arg);
++
++ get_commit_format(format, &format_rev);
++ format_pp.ctx.rev = &format_rev;
++ format_pp.ctx.fmt = format_rev.commit_format;
++ format_pp.ctx.abbrev = format_rev.abbrev;
++ format_pp.ctx.date_mode_explicit = format_rev.date_mode_explicit;
++ format_pp.ctx.date_mode = format_rev.date_mode;
++ format_pp.ctx.color = GIT_COLOR_AUTO;
++
++ userformat_find_requirements(format,
++ &format_pp.want);
++ if (format_pp.want.notes) {
++ int ignore_show_notes = 0;
++ struct string_list_item *n;
++
++ for_each_string_list_item(n, ¬es)
++ enable_ref_display_notes(&format_notes_opt,
++ &ignore_show_notes,
++ n->string);
++ load_display_notes(&format_notes_opt);
++ }
++
++ init_format_rev_command(&cmd, &format_pp);
++
++ switch (stdin_mode) {
++ case TEXT:
++ while (strbuf_getline(&scratch_buf, stdin) != EOF) {
++ strbuf_addch(&scratch_buf, '\n');
++ name_rev_line(scratch_buf.buf, &cmd);
+ }
++ break;
++ case REVS:
++ while (strbuf_getline(&scratch_buf, stdin) != EOF) {
++ struct object_id oid;
++ struct object *object;
++ struct object *peeled;
++ struct commit *commit;
++
++ if (repo_get_oid(the_repository, scratch_buf.buf, &oid)) {
++ fprintf(stderr, "Could not get sha1 for %s. Skipping.\n",
++ scratch_buf.buf);
++ continue;
++ }
++
++ object = parse_object(the_repository, &oid);
++ if (!object) {
++ fprintf(stderr, "Could not get object for %s. Skipping.\n",
++ scratch_buf.buf);
++ continue;
++ }
+
-+ format_ctx = &format_pp;
++ peeled = deref_tag(the_repository, object, scratch_buf.buf, 0);
++ if (peeled && peeled->type == OBJ_COMMIT)
++ commit = (struct commit *)peeled;
++ if (!commit) {
++ fprintf(stderr, "Could not get commit for %s. Skipping.\n",
++ *argv);
++ continue;
++ }
++
++ get_format_rev(commit, &format_pp, &scratch_buf);
++ printf("%s\n", scratch_buf.buf);
++ strbuf_release(&scratch_buf);
++ }
++ break;
++ default:
++ BUG("uncovered case: %d", stdin_mode);
+ }
+
- if (all + annotate_stdin + !!argc > 1) {
- error("Specify either a list, or --all, not both!");
- usage_with_options(name_rev_usage, opts);
-@@ builtin/name-rev.c: int cmd_name_rev(int argc,
-
- while (strbuf_getline(&sb, stdin) != EOF) {
- strbuf_addch(&sb, '\n');
-- name_rev_line(sb.buf, &data);
-+ name_rev_line(sb.buf, &data, format_ctx);
- }
- strbuf_release(&sb);
- } else if (all) {
-@@ builtin/name-rev.c: int cmd_name_rev(int argc,
- struct object *obj = get_indexed_object(the_repository, i);
- if (!obj || obj->type != OBJ_COMMIT)
- continue;
-- show_name(obj, NULL,
-+ show_name(obj, NULL, format_ctx,
- always, allow_undefined, data.name_only);
- }
- } else {
- int i;
- for (i = 0; i < revs.nr; i++)
-- show_name(revs.objects[i].item, revs.objects[i].name,
-+ show_name(revs.objects[i].item, revs.objects[i].name, format_ctx,
- always, allow_undefined, data.name_only);
- }
-
- string_list_clear(&data.ref_filters, 0);
- string_list_clear(&data.exclude_filters, 0);
++ strbuf_release(&scratch_buf);
+ string_list_clear(¬es, 0);
+ release_display_notes(&format_notes_opt);
- mem_pool_discard(&string_pool, 0);
- object_array_clear(&revs);
- return 0;
++ return 0;
++}
+
+ ## command-list.txt ##
+@@ command-list.txt: git-fmt-merge-msg purehelpers
+ git-for-each-ref plumbinginterrogators
+ git-for-each-repo plumbinginterrogators
+ git-format-patch mainporcelain
++git-format-rev plumbinginterrogators
+ git-fsck ancillaryinterrogators complete
+ git-gc mainporcelain
+ git-get-tar-commit-id plumbinginterrogators
+
+ ## git.c ##
+@@ git.c: static struct cmd_struct commands[] = {
+ { "for-each-ref", cmd_for_each_ref, RUN_SETUP },
+ { "for-each-repo", cmd_for_each_repo, RUN_SETUP_GENTLY },
+ { "format-patch", cmd_format_patch, RUN_SETUP },
++ { "format-rev", cmd_format_rev, RUN_SETUP },
+ { "fsck", cmd_fsck, RUN_SETUP },
+ { "fsck-objects", cmd_fsck, RUN_SETUP },
+ { "fsmonitor--daemon", cmd_fsmonitor__daemon, RUN_SETUP },
+
+ ## t/t1517-outside-repo.sh ##
+@@ t/t1517-outside-repo.sh: do
+ archimport | citool | credential-netrc | credential-libsecret | \
+ credential-osxkeychain | cvsexportcommit | cvsimport | cvsserver | \
+ daemon | \
+- difftool--helper | filter-branch | fsck-objects | get-tar-commit-id | \
++ difftool--helper | filter-branch | format-rev | fsck-objects | \
++ get-tar-commit-id | \
+ gui | gui--askpass | \
+ http-backend | http-fetch | http-push | init-db | \
+ merge-octopus | merge-one-file | merge-resolve | mergetool | \
## t/t6120-describe.sh ##
-@@ t/t6120-describe.sh: test_expect_success 'name-rev --annotate-stdin works with commitGraph' '
- )
+@@ t/t6120-describe.sh: test_expect_success 'do not be fooled by invalid describe format ' '
+ test_must_fail git cat-file -t "refs/tags/super-invalid/./../...../ ~^:/?*[////\\\\\\&}/busted.lock-42-g"$(cat out)
'
+test_expect_success 'name-rev --format setup' '
@@ t/t6120-describe.sh: test_expect_success 'name-rev --annotate-stdin works with c
+ test_commit -C repo-format eighth
+'
+
-+test_expect_success 'name-rev --format --no-name-only' '
-+ cat >expect <<-\EOF &&
-+ HEAD~3 [fifth]
-+ HEAD [eighth]
-+ HEAD~5 [third]
-+ EOF
-+ git -C repo-format name-rev --format="[%s]" \
-+ --no-name-only HEAD~3 HEAD HEAD~5 >actual &&
-+ test_cmp expect actual
-+'
-+
-+test_expect_success 'name-rev --format --no-format is the same as regular name-rev' '
-+ git -C repo-format name-rev HEAD~2 HEAD~3 >expect &&
-+ test_file_not_empty expect &&
-+ git -C repo-format name-rev --format="huh?" \
-+ --no-format HEAD~2 HEAD~3 >actual &&
-+ test_cmp expect actual
-+'
-+
-+test_expect_success 'name-rev --format=%s for argument revs' '
++test_expect_success 'format-rev --stdin-mode=revs' '
+ cat >expect <<-\EOF &&
+ eighth
+ seventh
+ fifth
+ EOF
-+ git -C repo-format name-rev --format=%s \
-+ HEAD HEAD~ HEAD~3 >actual &&
++ git -C repo-format format-rev --stdin-mode=revs \
++ --format=%s >actual <<-\EOF &&
++ HEAD
++ HEAD~
++ HEAD~3
++ EOF
+ test_cmp expect actual
+'
+
-+test_expect_success '--name-rev --format=reference --annotate-stdin from rev-list same as log' '
++test_expect_success 'format-rev --stdin-mode=text from rev-list same as log' '
+ git -C repo-format log --format=reference >expect &&
+ test_file_not_empty expect &&
+ git -C repo-format rev-list HEAD >list &&
-+ git -C repo-format name-rev --format=reference \
-+ --annotate-stdin <list >actual &&
++ git -C repo-format format-rev --stdin-mode=text \
++ --format=reference <list >actual &&
+ test_cmp expect actual
+'
+
-+test_expect_success '--name-rev --format=<pretty> --annotate-stdin with running text and tree oid' '
++test_expect_success 'format-rev --stdin-mode=text with running text and tree oid' '
+ cmit_oid=$(git -C repo-format rev-parse :/fifth) &&
+ reference=$(git -C repo-format log -n1 --format=reference :/fifth) &&
+ tree=$(git -C repo-format rev-parse HEAD^{tree}) &&
@@ t/t6120-describe.sh: test_expect_success 'name-rev --annotate-stdin works with c
+ We thought we fixed this in ${reference}.
+ But look at this tree: ${tree}.
+ EOF
-+ git -C repo-format name-rev --format=reference --annotate-stdin \
++ git -C repo-format format-rev --stdin-mode=text --format=reference \
+ >actual <<-EOF &&
+ We thought we fixed this in ${cmit_oid}.
+ But look at this tree: ${tree}.
@@ t/t6120-describe.sh: test_expect_success 'name-rev --annotate-stdin works with c
+ test_cmp expect actual
+'
+
-+test_expect_success 'name-rev --format=<pretty> with %N (note)' '
++test_expect_success 'format-rev with %N (note)' '
+ test_when_finished "git -C repo-format notes remove" &&
+ git -C repo-format notes add -m"Make a note" &&
+ printf "Make a note\n\n\n" >expect &&
-+ git -C repo-format name-rev --format="tformat:%N" \
-+ HEAD HEAD~ >actual &&
++ git -C repo-format format-rev --stdin-mode=revs \
++ --format="tformat:%N" \
++ >actual <<-\EOF &&
++ HEAD
++ HEAD~
++ EOF
+ test_cmp expect actual
+'
+
-+test_expect_success 'name-rev --format=<pretty> --notes<ref>' '
++test_expect_success 'format-rev --notes<ref> (custom notes ref)' '
+ # One custom notes ref
+ test_when_finished "git -C repo-format notes remove" &&
+ test_when_finished "git -C repo-format notes --ref=word remove" &&
+ git -C repo-format notes add -m"default" &&
+ git -C repo-format notes --ref=word add -m"custom" &&
+ printf "custom\n\n" >expect &&
-+ git -C repo-format name-rev --format="tformat:%N" \
++ git -C repo-format format-rev --stdin-mode=revs \
++ --format="tformat:%N" \
+ --notes=word \
-+ HEAD >actual &&
++ >actual <<-\EOF &&
++ HEAD
++ EOF
+ test_cmp expect actual &&
+ # Glob all
+ printf "default\ncustom\n\n" >expect &&
-+ git -C repo-format name-rev --format="tformat:%N" \
-+ --notes=* \
-+ HEAD >actual &&
++ git -C repo-format format-rev --stdin-mode=revs \
++ --format="tformat:%N" \
++ --notes=* >actual <<-\EOF &&
++ HEAD
++ EOF
++ test_cmp expect actual
++'
++
++test_expect_success 'format-rev --stdin-mode=revs on annotated tag peels to commit' '
++ test_when_finished "git -C repo-format tag -d version" &&
++ git -C repo-format tag -a -m"new version" version &&
++ cat >expect <<-\EOF &&
++ eighth
++ EOF
++ git -C repo-format format-rev --stdin-mode=revs \
++ --format=%s \
++ --notes=* >actual <<-\EOF &&
++ version
++ EOF
+ test_cmp expect actual
+'
+
- # B
- # o
- # H \
+ test_done
base-commit: 67006b9db8b772423ad0706029286096307d2567
--
2.54.0.13.g9c7419e39f8
^ permalink raw reply related [flat|nested] 45+ messages in thread
* [PATCH v3 1/5] name-rev: wrap both blocks in braces
2026-04-28 22:25 ` [PATCH v3 0/5] format-rev: introduce builtin for on-demand pretty formatting kristofferhaugsbakk
@ 2026-04-28 22:25 ` kristofferhaugsbakk
2026-04-28 22:25 ` [PATCH v3 2/5] name-rev: run clang-format before factoring code kristofferhaugsbakk
` (4 subsequent siblings)
5 siblings, 0 replies; 45+ messages in thread
From: kristofferhaugsbakk @ 2026-04-28 22:25 UTC (permalink / raw)
To: git; +Cc: Kristoffer Haugsbakk, ben.knoble
From: Kristoffer Haugsbakk <code@khaugsbakk.name>
See `CodingGuidelines`:
- When there are multiple arms to a conditional and some of them
require braces, enclose even a single line block in braces for
consistency. [...]
Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
---
Notes (series):
v2:
Fix stray formatting of `(p+1)`
builtin/name-rev.c | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/builtin/name-rev.c b/builtin/name-rev.c
index 6188cf98ce0..171e7bd0e98 100644
--- a/builtin/name-rev.c
+++ b/builtin/name-rev.c
@@ -466,9 +466,9 @@ static const char *get_rev_name(const struct object *o, struct strbuf *buf)
if (!n)
return NULL;
- if (!n->generation)
+ if (!n->generation) {
return n->tip_name;
- else {
+ } else {
strbuf_reset(buf);
strbuf_addstr(buf, n->tip_name);
strbuf_strip_suffix(buf, "^0");
@@ -516,9 +516,9 @@ static void name_rev_line(char *p, struct name_ref_data *data)
for (p_start = p; *p; p++) {
#define ishex(x) (isdigit((x)) || ((x) >= 'a' && (x) <= 'f'))
- if (!ishex(*p))
+ if (!ishex(*p)) {
counter = 0;
- else if (++counter == hexsz &&
+ } else if (++counter == hexsz &&
!ishex(*(p+1))) {
struct object_id oid;
const char *name = NULL;
--
2.54.0.13.g9c7419e39f8
^ permalink raw reply related [flat|nested] 45+ messages in thread
* [PATCH v3 2/5] name-rev: run clang-format before factoring code
2026-04-28 22:25 ` [PATCH v3 0/5] format-rev: introduce builtin for on-demand pretty formatting kristofferhaugsbakk
2026-04-28 22:25 ` [PATCH v3 1/5] name-rev: wrap both blocks in braces kristofferhaugsbakk
@ 2026-04-28 22:25 ` kristofferhaugsbakk
2026-04-28 22:25 ` [PATCH v3 3/5] name-rev: factor code for sharing with a new command kristofferhaugsbakk
` (3 subsequent siblings)
5 siblings, 0 replies; 45+ messages in thread
From: kristofferhaugsbakk @ 2026-04-28 22:25 UTC (permalink / raw)
To: git; +Cc: Kristoffer Haugsbakk, ben.knoble
From: Kristoffer Haugsbakk <code@khaugsbakk.name>
We are about to move code around to prepare for adding a new
command. Let’s deal with clang-format changes first in the affected
areas.
Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
---
builtin/name-rev.c | 18 +++++++++---------
1 file changed, 9 insertions(+), 9 deletions(-)
diff --git a/builtin/name-rev.c b/builtin/name-rev.c
index 171e7bd0e98..6357eaa76d0 100644
--- a/builtin/name-rev.c
+++ b/builtin/name-rev.c
@@ -519,22 +519,22 @@ static void name_rev_line(char *p, struct name_ref_data *data)
if (!ishex(*p)) {
counter = 0;
} else if (++counter == hexsz &&
- !ishex(*(p+1))) {
+ !ishex(*(p + 1))) {
struct object_id oid;
const char *name = NULL;
- char c = *(p+1);
+ char c = *(p + 1);
int p_len = p - p_start + 1;
counter = 0;
- *(p+1) = 0;
+ *(p + 1) = 0;
if (!repo_get_oid(the_repository, p - (hexsz - 1), &oid)) {
struct object *o =
lookup_object(the_repository, &oid);
if (o)
name = get_rev_name(o, &buf);
}
- *(p+1) = c;
+ *(p + 1) = c;
if (!name)
continue;
@@ -571,9 +571,9 @@ int cmd_name_rev(int argc,
OPT_BOOL(0, "name-only", &data.name_only, N_("print only ref-based names (no object names)")),
OPT_BOOL(0, "tags", &data.tags_only, N_("only use tags to name the commits")),
OPT_STRING_LIST(0, "refs", &data.ref_filters, N_("pattern"),
- N_("only use refs matching <pattern>")),
+ N_("only use refs matching <pattern>")),
OPT_STRING_LIST(0, "exclude", &data.exclude_filters, N_("pattern"),
- N_("ignore refs matching <pattern>")),
+ N_("ignore refs matching <pattern>")),
OPT_GROUP(""),
OPT_BOOL(0, "all", &all, N_("list all commits reachable from all refs")),
#ifndef WITH_BREAKING_CHANGES
@@ -585,10 +585,10 @@ int cmd_name_rev(int argc,
#endif /* WITH_BREAKING_CHANGES */
OPT_BOOL(0, "annotate-stdin", &annotate_stdin, N_("annotate text from stdin")),
OPT_BOOL(0, "undefined", &allow_undefined, N_("allow to print `undefined` names (default)")),
- OPT_BOOL(0, "always", &always,
- N_("show abbreviated commit object as fallback")),
+ OPT_BOOL(0, "always", &always,
+ N_("show abbreviated commit object as fallback")),
OPT_HIDDEN_BOOL(0, "peel-tag", &peel_tag,
- N_("dereference tags in the input (internal use)")),
+ N_("dereference tags in the input (internal use)")),
OPT_END(),
};
--
2.54.0.13.g9c7419e39f8
^ permalink raw reply related [flat|nested] 45+ messages in thread
* [PATCH v3 3/5] name-rev: factor code for sharing with a new command
2026-04-28 22:25 ` [PATCH v3 0/5] format-rev: introduce builtin for on-demand pretty formatting kristofferhaugsbakk
2026-04-28 22:25 ` [PATCH v3 1/5] name-rev: wrap both blocks in braces kristofferhaugsbakk
2026-04-28 22:25 ` [PATCH v3 2/5] name-rev: run clang-format before factoring code kristofferhaugsbakk
@ 2026-04-28 22:25 ` kristofferhaugsbakk
2026-04-30 13:54 ` Phillip Wood
2026-04-28 22:25 ` [PATCH v3 4/5] name-rev: make dedicated --annotate-stdin --name-only test kristofferhaugsbakk
` (2 subsequent siblings)
5 siblings, 1 reply; 45+ messages in thread
From: kristofferhaugsbakk @ 2026-04-28 22:25 UTC (permalink / raw)
To: git; +Cc: Kristoffer Haugsbakk, ben.knoble
From: Kristoffer Haugsbakk <code@khaugsbakk.name>
We are about to introduce a new command git-format-rev(1) to this
file. Let’s factor some code so that we can share it with the new
command.
We want to be able to format commits found in freeform text, and
git-name-rev(1) already has a function for that but for symbolic
names. Let’s use a tagged union for the command-specific payload.
No functional changes.
Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
---
builtin/name-rev.c | 54 +++++++++++++++++++++++++++++++++++-----------
1 file changed, 41 insertions(+), 13 deletions(-)
diff --git a/builtin/name-rev.c b/builtin/name-rev.c
index 6357eaa76d0..dc4136f4de3 100644
--- a/builtin/name-rev.c
+++ b/builtin/name-rev.c
@@ -272,6 +272,24 @@ struct name_ref_data {
struct string_list exclude_filters;
};
+enum command_type {
+ NAME_REV = 1,
+};
+
+struct command {
+ enum command_type type;
+ union {
+ int name_only;
+ } u;
+};
+
+static void init_name_rev_command(struct command *cmd,
+ int name_only)
+{
+ cmd->type = NAME_REV;
+ cmd->u.name_only = name_only;
+}
+
static struct tip_table {
struct tip_table_entry {
struct object_id oid;
@@ -507,7 +525,7 @@ static char const * const name_rev_usage[] = {
NULL
};
-static void name_rev_line(char *p, struct name_ref_data *data)
+static void name_rev_line(char *p, struct command *cmd)
{
struct strbuf buf = STRBUF_INIT;
int counter = 0;
@@ -516,6 +534,7 @@ static void name_rev_line(char *p, struct name_ref_data *data)
for (p_start = p; *p; p++) {
#define ishex(x) (isdigit((x)) || ((x) >= 'a' && (x) <= 'f'))
+ start:
if (!ishex(*p)) {
counter = 0;
} else if (++counter == hexsz &&
@@ -524,25 +543,32 @@ static void name_rev_line(char *p, struct name_ref_data *data)
const char *name = NULL;
char c = *(p + 1);
int p_len = p - p_start + 1;
+ struct object *o = NULL;
+ int oid_ret = 1;
counter = 0;
*(p + 1) = 0;
- if (!repo_get_oid(the_repository, p - (hexsz - 1), &oid)) {
- struct object *o =
- lookup_object(the_repository, &oid);
+ oid_ret = repo_get_oid(the_repository, p - (hexsz - 1), &oid);
+
+ switch (cmd->type) {
+ case NAME_REV:
+ if (!oid_ret)
+ o = lookup_object(the_repository, &oid);
if (o)
name = get_rev_name(o, &buf);
+ *(p + 1) = c;
+ if (!name)
+ goto start;
+ if (cmd->u.name_only)
+ printf("%.*s%s", p_len - hexsz, p_start, name);
+ else
+ printf("%.*s (%s)", p_len, p_start, name);
+ break;
+ default:
+ BUG("uncovered case: %d", cmd->type);
}
- *(p + 1) = c;
-
- if (!name)
- continue;
- if (data->name_only)
- printf("%.*s%s", p_len - hexsz, p_start, name);
- else
- printf("%.*s (%s)", p_len, p_start, name);
p_start = p + 1;
}
}
@@ -567,6 +593,7 @@ int cmd_name_rev(int argc,
#endif
int all = 0, annotate_stdin = 0, allow_undefined = 1, always = 0, peel_tag = 0;
struct name_ref_data data = { 0, 0, STRING_LIST_INIT_NODUP, STRING_LIST_INIT_NODUP };
+ struct command cmd;
struct option opts[] = {
OPT_BOOL(0, "name-only", &data.name_only, N_("print only ref-based names (no object names)")),
OPT_BOOL(0, "tags", &data.tags_only, N_("only use tags to name the commits")),
@@ -596,6 +623,7 @@ int cmd_name_rev(int argc,
init_commit_rev_name(&rev_names);
repo_config(the_repository, git_default_config, NULL);
argc = parse_options(argc, argv, prefix, opts, name_rev_usage, 0);
+ init_name_rev_command(&cmd, data.name_only);
#ifndef WITH_BREAKING_CHANGES
if (transform_stdin) {
@@ -663,7 +691,7 @@ int cmd_name_rev(int argc,
while (strbuf_getline(&sb, stdin) != EOF) {
strbuf_addch(&sb, '\n');
- name_rev_line(sb.buf, &data);
+ name_rev_line(sb.buf, &cmd);
}
strbuf_release(&sb);
} else if (all) {
--
2.54.0.13.g9c7419e39f8
^ permalink raw reply related [flat|nested] 45+ messages in thread
* [PATCH v3 4/5] name-rev: make dedicated --annotate-stdin --name-only test
2026-04-28 22:25 ` [PATCH v3 0/5] format-rev: introduce builtin for on-demand pretty formatting kristofferhaugsbakk
` (2 preceding siblings ...)
2026-04-28 22:25 ` [PATCH v3 3/5] name-rev: factor code for sharing with a new command kristofferhaugsbakk
@ 2026-04-28 22:25 ` kristofferhaugsbakk
2026-04-28 22:25 ` [PATCH v3 5/5] format-rev: introduce builtin for on-demand pretty formatting kristofferhaugsbakk
2026-05-07 19:34 ` [PATCH v4 0/5] " kristofferhaugsbakk
5 siblings, 0 replies; 45+ messages in thread
From: kristofferhaugsbakk @ 2026-04-28 22:25 UTC (permalink / raw)
To: git; +Cc: Kristoffer Haugsbakk, ben.knoble
From: Kristoffer Haugsbakk <code@khaugsbakk.name>
The previous commit split the `--name-only` handling:
1. `--annotate-stdin`: uses the new `struct command`
2. The rest: uses `struct name_ref_data`
But there is no dedicated test for the option combination in (1). That
means that the following tests will fail if you neglect to set
`command.u.name_only` properly:
name-rev --annotate-stdin works with commitGraph
name-rev --annotate-stdin works with non-monotonic timestamps
even though it has nothing to do with what these tests are supposed
to test.
Let’s add another regression test now that it is relevant.
Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
---
t/t6120-describe.sh | 14 ++++++++++++++
1 file changed, 14 insertions(+)
diff --git a/t/t6120-describe.sh b/t/t6120-describe.sh
index 2c70cc561ad..62789f76381 100755
--- a/t/t6120-describe.sh
+++ b/t/t6120-describe.sh
@@ -298,6 +298,20 @@ test_expect_success 'name-rev --annotate-stdin' '
test_cmp expect actual
'
+test_expect_success 'name-rev --annotate-stdin --name-only' '
+ >expect.unsorted &&
+ for rev in $(git rev-list --all)
+ do
+ name=$(git name-rev --name-only $rev) &&
+ echo "$name" >>expect.unsorted || return 1
+ done &&
+ sort <expect.unsorted >expect &&
+ git name-rev --annotate-stdin --name-only \
+ <list >actual.unsorted &&
+ sort <actual.unsorted >actual &&
+ test_cmp expect actual
+'
+
test_expect_success 'name-rev --stdin deprecated' '
git rev-list --all >list &&
if ! test_have_prereq WITH_BREAKING_CHANGES
--
2.54.0.13.g9c7419e39f8
^ permalink raw reply related [flat|nested] 45+ messages in thread
* [PATCH v3 5/5] format-rev: introduce builtin for on-demand pretty formatting
2026-04-28 22:25 ` [PATCH v3 0/5] format-rev: introduce builtin for on-demand pretty formatting kristofferhaugsbakk
` (3 preceding siblings ...)
2026-04-28 22:25 ` [PATCH v3 4/5] name-rev: make dedicated --annotate-stdin --name-only test kristofferhaugsbakk
@ 2026-04-28 22:25 ` kristofferhaugsbakk
2026-04-29 13:41 ` Kristoffer Haugsbakk
` (3 more replies)
2026-05-07 19:34 ` [PATCH v4 0/5] " kristofferhaugsbakk
5 siblings, 4 replies; 45+ messages in thread
From: kristofferhaugsbakk @ 2026-04-28 22:25 UTC (permalink / raw)
To: git; +Cc: Kristoffer Haugsbakk, ben.knoble
From: Kristoffer Haugsbakk <code@khaugsbakk.name>
Introduce a new builtin for pretty formatting one revision expression
per line or commit object names found in running text.
Sometimes you want to format commits. Most of the time you’re
walking the graph, e.g. getting a range of commits like
`master..topic`. That’s a job for git-log(1).
But there are times when you want to format commits that you encounter
on demand:
• Full hashes in running text that you might want to pretty-print
• git-last-modified(1) outputs full hashes that you can do the same
with
• git-cherry(1) has `-v` for commit subject, but maybe you want
something else?
But now you can’t use git-log(1), git-show(1), or git-rev-list(1):
• You can’t feed commits piecemeal to these commands, one input
for one output; they block until standard in is closed
• You can’t feed a list of possibly duplicate commits, like the output
of git-last-modified(1); they effectively deduplicate the output
Beyond these two points there’s also the input massage problem: you
cannot feed mixed input (revisions mixed with arbitrary text).
One might hope that git-cat-file(1) can save us. But it doesn’t
support pretty formats.
But there is one command that already both handles revisions as
arguments, revisions on standard input, and even revisions mixed in
with arbitrary text. Namely git-name-rev(1): the command for outputting
symbolic names for commits.
We made some room in `builtin/name-rev.c` two commits ago. Let’s
now add this new git-format-rev(1) command. Taking inspiration from
git-name-rev(1), there are two modes:
• revs: like git-name-rev(1) in argv mode, but one revision per line
on standard in
• text: like git-name-rev(1) with `--annotate-stdin`
***
We need to add this command to the exception list in
`t/t1517-outside-repo.sh` because it uses “EXPERIMENTAL!”
in the usage line.
Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
---
Notes (series):
v3:
• And don’t forget to document --notes this time
https://lore.kernel.org/git/CALnO6CB5WOTp_e7Kv3CrEbQ+3XE-gDxNVHf7qATBEbyKWfxpLg@mail.gmail.com/
Documentation/git-format-rev.adoc | 148 ++++++++++++++++++++++++
Makefile | 1 +
builtin.h | 1 +
builtin/name-rev.c | 186 ++++++++++++++++++++++++++++++
command-list.txt | 1 +
git.c | 1 +
t/t1517-outside-repo.sh | 3 +-
t/t6120-describe.sh | 104 +++++++++++++++++
8 files changed, 444 insertions(+), 1 deletion(-)
create mode 100644 Documentation/git-format-rev.adoc
diff --git a/Documentation/git-format-rev.adoc b/Documentation/git-format-rev.adoc
new file mode 100644
index 00000000000..d960001d750
--- /dev/null
+++ b/Documentation/git-format-rev.adoc
@@ -0,0 +1,148 @@
+git-format-rev(1)
+=================
+
+NAME
+----
+git-format-rev - EXPERIMENTAL: Pretty format revisions on demand
+
+
+SYNOPSIS
+--------
+[synopsis]
+(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> --format=<pretty> [--notes=<ref>]
+
+DESCRIPTION
+-----------
+
+Pretty format revisions from standard input.
+
+THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
+
+OPTIONS
+-------
+
+`--stdin-mode=<mode>`::
+ How to interpret standard input data:
++
+--
+`revs`:: Each line is interpreted as a commit. Any kind of revision
+ expression can be used (see linkgit:gitrevisions[7]). Annotated
+ tags are peeled (see linkgit:gitglossary[7]).
++
+The argument `rev` is also accepted.
+`text`:: Formats all commit object names found in freeform text. These
+ must the full object names, i.e. abbreviated hexidecimal object
+ names will not be interpreted.
+--
+
+`--format=<pretty>`::
+ Pretty format string.
+
+`--notes=<ref>`::
+`--no-notes`::
+ Custom notes ref. Notes are displayed when using the `%N`
+ atom. See linkgit:git-notes[1].
+
+EXAMPLES
+--------
+
+The command linkgit:git-last-modified[1] shows the commit that each file
+was last modified in.
+
+----
+$ git last-modified -- README.md Makefile
+7798034171030be0909c56377a4e0e10e6d2df93 Makefile
+c50fbb2dd225e7e82abba4380423ae105089f4d7 README.md
+----
+
+We can pipe the result to this command in order to replace the object
+name with the commit author.
+
+----
+$ git last-modified -- README.md Makefile |
+ git format-rev --stdin-mode=text --format=%an
+Junio C Hamano Makefile
+Todd Zullinger README.md
+----
+
+Another example is _formatting commits in commit messages_. Given this commit message:
+
+----
+Fix off-by-one error
+
+Fix off-by-one error introduced in
+e83c5163316f89bfbde7d9ab23ca2e25604af290.
+
+We thought we fixed this in 5569bf9bbedd63a00780fc5c110e0cfab3aa97b9 but
+that only covered 1/3 of the faulty cases.
+----
+
+We can format the commits and use par(1) to reflow the text, say in a
+`commit-msg` hook:
+
+----
+$ git config set hook.reference-commits.event commit-msg
+$ git config set hook.reference-commits.command reference-commits
+$ cat $(which reference-commits)
+#/bin/sh
+
+msg="$1"
+rewritten=$(mktemp)
+git format-rev --stdin-mode=text --format=reference <"$msg" |
+ par >"$rewritten"
+mv "$rewritten" "$msg"
+----
+
+Which will produce something like this:
+
+----
+Fix off-by-one error
+
+Fix off-by-one error introduced in e83c5163316 (Implement better memory
+allocator, 2005-04-07).
+
+We thought we fixed this in 5569bf9bbed (Fix memory allocator,
+2005-06-22) but that only covered 1/3 of the faulty cases.
+----
+
+DISCUSSION
+----------
+
+This command lets you format any number of revisions in any order
+through one command invocation. Consider the
+linkgit:git-last-modified[1] case from the "EXAMPLES" section above:
+
+1. There might be hundreds of files
+2. Commits can be repeated, i.e. two or more files were last modified in
+ the same commit
+
+Two widely-used commands which pretty formats commits are
+linkgit:git-log[1] and linkgit:git-show[1]. It turns out that they are
+not a good fit for the above use case.
+
+- The output of linkgit:git-last-modified[1] would have to be processed
+ in stages since you need to transform the first column separately and
+ then link the author to the filename. But this is surmountable.
+- You can feed each commit to `git show` or `git show --no-walk -1`. But
+ that means that you need to create a process for each line.
+- Let’s say that you want to use one process, not one per line. So you
+ want to feed all the commits to the command. Now you face the problem
+ that you have to feed all the commits to the commands before you get
+ any output (this is also the case for the `--stdin` modes). In other
+ words, you cannot loop through each line, get the author for the
+ commit, and output the author and the filename. You need to feed all
+ the commits, get back all the output, and match the output with the
+ filename.
+- But the next problem is that commands will deduplicate the input and
+ only output one commit one single time only. Thus you cannot make the
+ output order match the input order, since a commit could have been
+ repeated in the original input.
+
+In short, it is straightforward to use these two commands if you use one
+process per line. It is much more work if you just want to use one
+process, but still doable. In contrast, this problem is just another
+shell pipeline with this command.
+
+GIT
+---
+Part of the linkgit:git[1] suite
diff --git a/Makefile b/Makefile
index 15b1ded1a0b..cbaf91fd846 100644
--- a/Makefile
+++ b/Makefile
@@ -895,6 +895,7 @@ BUILT_INS += $(patsubst builtin/%.o,git-%$X,$(BUILTIN_OBJS))
BUILT_INS += git-cherry$X
BUILT_INS += git-cherry-pick$X
BUILT_INS += git-format-patch$X
+BUILT_INS += git-format-rev$X
BUILT_INS += git-fsck-objects$X
BUILT_INS += git-init$X
BUILT_INS += git-maintenance$X
diff --git a/builtin.h b/builtin.h
index 235c51f30e5..63813c90125 100644
--- a/builtin.h
+++ b/builtin.h
@@ -189,6 +189,7 @@ int cmd_fmt_merge_msg(int argc, const char **argv, const char *prefix, struct re
int cmd_for_each_ref(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_for_each_repo(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_format_patch(int argc, const char **argv, const char *prefix, struct repository *repo);
+int cmd_format_rev(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_fsck(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_fsmonitor__daemon(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_gc(int argc, const char **argv, const char *prefix, struct repository *repo);
diff --git a/builtin/name-rev.c b/builtin/name-rev.c
index dc4136f4de3..b60cc766279 100644
--- a/builtin/name-rev.c
+++ b/builtin/name-rev.c
@@ -18,6 +18,9 @@
#include "commit-graph.h"
#include "wildmatch.h"
#include "mem-pool.h"
+#include "pretty.h"
+#include "revision.h"
+#include "notes.h"
/*
* One day. See the 'name a rev shortly after epoch' test in t6120 when
@@ -272,14 +275,26 @@ struct name_ref_data {
struct string_list exclude_filters;
};
+struct pretty_format {
+ struct pretty_print_context ctx;
+ struct userformat_want want;
+};
+
enum command_type {
NAME_REV = 1,
+ FORMAT_REV = 2,
+};
+
+enum stdin_mode {
+ TEXT = 1,
+ REVS = 2,
};
struct command {
enum command_type type;
union {
int name_only;
+ struct pretty_format *pretty_format;
} u;
};
@@ -290,6 +305,13 @@ static void init_name_rev_command(struct command *cmd,
cmd->u.name_only = name_only;
}
+static void init_format_rev_command(struct command *cmd,
+ struct pretty_format *pretty_format)
+{
+ cmd->type = FORMAT_REV;
+ cmd->u.pretty_format = pretty_format;
+}
+
static struct tip_table {
struct tip_table_entry {
struct object_id oid;
@@ -495,6 +517,27 @@ static const char *get_rev_name(const struct object *o, struct strbuf *buf)
}
}
+static const char *get_format_rev(const struct commit *c,
+ struct pretty_format *format_ctx,
+ struct strbuf *buf)
+{
+ strbuf_reset(buf);
+
+ if (format_ctx->want.notes) {
+ struct strbuf notebuf = STRBUF_INIT;
+
+ format_display_notes(&c->object.oid, ¬ebuf,
+ get_log_output_encoding(),
+ format_ctx->ctx.fmt == CMIT_FMT_USERFORMAT);
+ format_ctx->ctx.notes_message = strbuf_detach(¬ebuf, NULL);
+ }
+
+ pretty_print_commit(&format_ctx->ctx, c, buf);
+ FREE_AND_NULL(format_ctx->ctx.notes_message);
+
+ return buf->buf;
+}
+
static void show_name(const struct object *obj,
const char *caller_name,
int always, int allow_undefined, int name_only)
@@ -565,6 +608,19 @@ static void name_rev_line(char *p, struct command *cmd)
else
printf("%.*s (%s)", p_len, p_start, name);
break;
+ case FORMAT_REV:
+ if (!oid_ret)
+ o = parse_object(the_repository, &oid);
+ if (o && o->type == OBJ_COMMIT)
+ name = get_format_rev((const struct commit *)o,
+ cmd->u.pretty_format,
+ &buf);
+ *(p + 1) = c;
+ if (name)
+ printf("%.*s%s", p_len - hexsz, p_start, name);
+ else
+ printf("%.*s", p_len, p_start);
+ break;
default:
BUG("uncovered case: %d", cmd->type);
}
@@ -718,3 +774,133 @@ int cmd_name_rev(int argc,
object_array_clear(&revs);
return 0;
}
+
+static enum stdin_mode parse_stdin_mode(const char *stdin_mode)
+{
+ if (!strcmp(stdin_mode, "text"))
+ return TEXT;
+ else if (!strcmp(stdin_mode, "revs") ||
+ !strcmp(stdin_mode, "rev"))
+ return REVS;
+ else
+ die(_("'%s' needs to be either text, revs, or rev"),
+ "--stdin-mode");
+}
+
+static char const *const format_rev_usage[] = {
+ N_("(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> --format=<pretty> [--notes=<ref>]"),
+ NULL
+};
+
+int cmd_format_rev(int argc,
+ const char **argv,
+ const char *prefix,
+ struct repository *repo UNUSED)
+{
+ const char *format = NULL;
+ enum stdin_mode stdin_mode;
+ const char *stdin_mode_arg = NULL;
+ struct display_notes_opt format_notes_opt;
+ struct rev_info format_rev = REV_INFO_INIT;
+ struct pretty_format format_pp = { 0 };
+ struct string_list notes = STRING_LIST_INIT_NODUP;
+ struct strbuf scratch_buf = STRBUF_INIT;
+ struct command cmd;
+ struct option opts[] = {
+ OPT_STRING(0, "format", &format, N_("format"),
+ N_("pretty format to use")),
+ OPT_STRING(0, "stdin-mode", &stdin_mode_arg, N_("stdin-mode"),
+ N_("how revs are processed")),
+ OPT_STRING_LIST(0, "notes", ¬es, N_("notes"),
+ N_("display notes for pretty format")),
+ OPT_END(),
+ };
+
+ argc = parse_options(argc, argv, prefix, opts, format_rev_usage, 0);
+
+ if (argc > 0) {
+ error(_("too many arguments"));
+ usage_with_options(format_rev_usage, opts);
+ }
+
+ if (!format)
+ die(_("'%s' is required"), "--format");
+ if (!stdin_mode_arg)
+ die(_("'%s' is required"), "--stdin-mode");
+
+ init_display_notes(&format_notes_opt);
+ stdin_mode = parse_stdin_mode(stdin_mode_arg);
+
+ get_commit_format(format, &format_rev);
+ format_pp.ctx.rev = &format_rev;
+ format_pp.ctx.fmt = format_rev.commit_format;
+ format_pp.ctx.abbrev = format_rev.abbrev;
+ format_pp.ctx.date_mode_explicit = format_rev.date_mode_explicit;
+ format_pp.ctx.date_mode = format_rev.date_mode;
+ format_pp.ctx.color = GIT_COLOR_AUTO;
+
+ userformat_find_requirements(format,
+ &format_pp.want);
+ if (format_pp.want.notes) {
+ int ignore_show_notes = 0;
+ struct string_list_item *n;
+
+ for_each_string_list_item(n, ¬es)
+ enable_ref_display_notes(&format_notes_opt,
+ &ignore_show_notes,
+ n->string);
+ load_display_notes(&format_notes_opt);
+ }
+
+ init_format_rev_command(&cmd, &format_pp);
+
+ switch (stdin_mode) {
+ case TEXT:
+ while (strbuf_getline(&scratch_buf, stdin) != EOF) {
+ strbuf_addch(&scratch_buf, '\n');
+ name_rev_line(scratch_buf.buf, &cmd);
+ }
+ break;
+ case REVS:
+ while (strbuf_getline(&scratch_buf, stdin) != EOF) {
+ struct object_id oid;
+ struct object *object;
+ struct object *peeled;
+ struct commit *commit;
+
+ if (repo_get_oid(the_repository, scratch_buf.buf, &oid)) {
+ fprintf(stderr, "Could not get sha1 for %s. Skipping.\n",
+ scratch_buf.buf);
+ continue;
+ }
+
+ object = parse_object(the_repository, &oid);
+ if (!object) {
+ fprintf(stderr, "Could not get object for %s. Skipping.\n",
+ scratch_buf.buf);
+ continue;
+ }
+
+ peeled = deref_tag(the_repository, object, scratch_buf.buf, 0);
+ if (peeled && peeled->type == OBJ_COMMIT)
+ commit = (struct commit *)peeled;
+ if (!commit) {
+ fprintf(stderr, "Could not get commit for %s. Skipping.\n",
+ *argv);
+ continue;
+ }
+
+ get_format_rev(commit, &format_pp, &scratch_buf);
+ printf("%s\n", scratch_buf.buf);
+ strbuf_release(&scratch_buf);
+ }
+ break;
+ default:
+ BUG("uncovered case: %d", stdin_mode);
+ }
+
+ strbuf_release(&scratch_buf);
+ string_list_clear(¬es, 0);
+ release_display_notes(&format_notes_opt);
+ return 0;
+}
diff --git a/command-list.txt b/command-list.txt
index f9005cf4597..df729872dca 100644
--- a/command-list.txt
+++ b/command-list.txt
@@ -108,6 +108,7 @@ git-fmt-merge-msg purehelpers
git-for-each-ref plumbinginterrogators
git-for-each-repo plumbinginterrogators
git-format-patch mainporcelain
+git-format-rev plumbinginterrogators
git-fsck ancillaryinterrogators complete
git-gc mainporcelain
git-get-tar-commit-id plumbinginterrogators
diff --git a/git.c b/git.c
index 2b212e6675d..af5b0422b00 100644
--- a/git.c
+++ b/git.c
@@ -578,6 +578,7 @@ static struct cmd_struct commands[] = {
{ "for-each-ref", cmd_for_each_ref, RUN_SETUP },
{ "for-each-repo", cmd_for_each_repo, RUN_SETUP_GENTLY },
{ "format-patch", cmd_format_patch, RUN_SETUP },
+ { "format-rev", cmd_format_rev, RUN_SETUP },
{ "fsck", cmd_fsck, RUN_SETUP },
{ "fsck-objects", cmd_fsck, RUN_SETUP },
{ "fsmonitor--daemon", cmd_fsmonitor__daemon, RUN_SETUP },
diff --git a/t/t1517-outside-repo.sh b/t/t1517-outside-repo.sh
index c824c1a25cf..360a9323343 100755
--- a/t/t1517-outside-repo.sh
+++ b/t/t1517-outside-repo.sh
@@ -114,7 +114,8 @@ do
archimport | citool | credential-netrc | credential-libsecret | \
credential-osxkeychain | cvsexportcommit | cvsimport | cvsserver | \
daemon | \
- difftool--helper | filter-branch | fsck-objects | get-tar-commit-id | \
+ difftool--helper | filter-branch | format-rev | fsck-objects | \
+ get-tar-commit-id | \
gui | gui--askpass | \
http-backend | http-fetch | http-push | init-db | \
merge-octopus | merge-one-file | merge-resolve | mergetool | \
diff --git a/t/t6120-describe.sh b/t/t6120-describe.sh
index 62789f76381..725f7d81b6b 100755
--- a/t/t6120-describe.sh
+++ b/t/t6120-describe.sh
@@ -801,4 +801,108 @@ test_expect_success 'do not be fooled by invalid describe format ' '
test_must_fail git cat-file -t "refs/tags/super-invalid/./../...../ ~^:/?*[////\\\\\\&}/busted.lock-42-g"$(cat out)
'
+test_expect_success 'name-rev --format setup' '
+ mkdir repo-format &&
+ git -C repo-format init &&
+ test_commit -C repo-format first &&
+ test_commit -C repo-format second &&
+ test_commit -C repo-format third &&
+ test_commit -C repo-format fourth &&
+ test_commit -C repo-format fifth &&
+ test_commit -C repo-format sixth &&
+ test_commit -C repo-format seventh &&
+ test_commit -C repo-format eighth
+'
+
+test_expect_success 'format-rev --stdin-mode=revs' '
+ cat >expect <<-\EOF &&
+ eighth
+ seventh
+ fifth
+ EOF
+ git -C repo-format format-rev --stdin-mode=revs \
+ --format=%s >actual <<-\EOF &&
+ HEAD
+ HEAD~
+ HEAD~3
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success 'format-rev --stdin-mode=text from rev-list same as log' '
+ git -C repo-format log --format=reference >expect &&
+ test_file_not_empty expect &&
+ git -C repo-format rev-list HEAD >list &&
+ git -C repo-format format-rev --stdin-mode=text \
+ --format=reference <list >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'format-rev --stdin-mode=text with running text and tree oid' '
+ cmit_oid=$(git -C repo-format rev-parse :/fifth) &&
+ reference=$(git -C repo-format log -n1 --format=reference :/fifth) &&
+ tree=$(git -C repo-format rev-parse HEAD^{tree}) &&
+ cat >expect <<-EOF &&
+ We thought we fixed this in ${reference}.
+ But look at this tree: ${tree}.
+ EOF
+ git -C repo-format format-rev --stdin-mode=text --format=reference \
+ >actual <<-EOF &&
+ We thought we fixed this in ${cmit_oid}.
+ But look at this tree: ${tree}.
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success 'format-rev with %N (note)' '
+ test_when_finished "git -C repo-format notes remove" &&
+ git -C repo-format notes add -m"Make a note" &&
+ printf "Make a note\n\n\n" >expect &&
+ git -C repo-format format-rev --stdin-mode=revs \
+ --format="tformat:%N" \
+ >actual <<-\EOF &&
+ HEAD
+ HEAD~
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success 'format-rev --notes<ref> (custom notes ref)' '
+ # One custom notes ref
+ test_when_finished "git -C repo-format notes remove" &&
+ test_when_finished "git -C repo-format notes --ref=word remove" &&
+ git -C repo-format notes add -m"default" &&
+ git -C repo-format notes --ref=word add -m"custom" &&
+ printf "custom\n\n" >expect &&
+ git -C repo-format format-rev --stdin-mode=revs \
+ --format="tformat:%N" \
+ --notes=word \
+ >actual <<-\EOF &&
+ HEAD
+ EOF
+ test_cmp expect actual &&
+ # Glob all
+ printf "default\ncustom\n\n" >expect &&
+ git -C repo-format format-rev --stdin-mode=revs \
+ --format="tformat:%N" \
+ --notes=* >actual <<-\EOF &&
+ HEAD
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success 'format-rev --stdin-mode=revs on annotated tag peels to commit' '
+ test_when_finished "git -C repo-format tag -d version" &&
+ git -C repo-format tag -a -m"new version" version &&
+ cat >expect <<-\EOF &&
+ eighth
+ EOF
+ git -C repo-format format-rev --stdin-mode=revs \
+ --format=%s \
+ --notes=* >actual <<-\EOF &&
+ version
+ EOF
+ test_cmp expect actual
+'
+
test_done
--
2.54.0.13.g9c7419e39f8
^ permalink raw reply related [flat|nested] 45+ messages in thread
* Re: [PATCH v3 5/5] format-rev: introduce builtin for on-demand pretty formatting
2026-04-28 22:25 ` [PATCH v3 5/5] format-rev: introduce builtin for on-demand pretty formatting kristofferhaugsbakk
@ 2026-04-29 13:41 ` Kristoffer Haugsbakk
2026-04-30 6:23 ` Kristoffer Haugsbakk
2026-04-30 9:21 ` Kristoffer Haugsbakk
` (2 subsequent siblings)
3 siblings, 1 reply; 45+ messages in thread
From: Kristoffer Haugsbakk @ 2026-04-29 13:41 UTC (permalink / raw)
To: git; +Cc: D. Ben Knoble
On Wed, Apr 29, 2026, at 00:25, kristofferhaugsbakk@fastmail.com wrote:
> From: Kristoffer Haugsbakk <code@khaugsbakk.name>
>[snip]
CI returned an error.
builtin/name-rev.c:893:25: error: ‘commit’ may be used uninitialized in this function [-Werror=maybe-uninitialized]
893 | get_format_rev(commit, &format_pp, &scratch_buf);
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [PATCH v3 5/5] format-rev: introduce builtin for on-demand pretty formatting
2026-04-29 13:41 ` Kristoffer Haugsbakk
@ 2026-04-30 6:23 ` Kristoffer Haugsbakk
0 siblings, 0 replies; 45+ messages in thread
From: Kristoffer Haugsbakk @ 2026-04-30 6:23 UTC (permalink / raw)
To: git; +Cc: D. Ben Knoble
On Wed, Apr 29, 2026, at 15:41, Kristoffer Haugsbakk wrote:
> On Wed, Apr 29, 2026, at 00:25, kristofferhaugsbakk@fastmail.com wrote:
>> From: Kristoffer Haugsbakk <code@khaugsbakk.name>
>>[snip]
>
> CI returned an error.
>
> builtin/name-rev.c:893:25: error: ‘commit’ may be used
> uninitialized in this function [-Werror=maybe-uninitialized]
> 893 | get_format_rev(commit, &format_pp,
> &scratch_buf);
> |
> ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Now Im using `config.mak.dev`.
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [PATCH v3 5/5] format-rev: introduce builtin for on-demand pretty formatting
2026-04-28 22:25 ` [PATCH v3 5/5] format-rev: introduce builtin for on-demand pretty formatting kristofferhaugsbakk
2026-04-29 13:41 ` Kristoffer Haugsbakk
@ 2026-04-30 9:21 ` Kristoffer Haugsbakk
2026-05-01 10:16 ` Phillip Wood
2026-05-03 19:19 ` Junio C Hamano
3 siblings, 0 replies; 45+ messages in thread
From: Kristoffer Haugsbakk @ 2026-04-30 9:21 UTC (permalink / raw)
To: git; +Cc: D. Ben Knoble
On Wed, Apr 29, 2026, at 00:25, kristofferhaugsbakk@fastmail.com wrote:
> From: Kristoffer Haugsbakk <code@khaugsbakk.name>
>[snip]
> + peeled = deref_tag(the_repository, object, scratch_buf.buf, 0);
> + if (peeled && peeled->type == OBJ_COMMIT)
> + commit = (struct commit *)peeled;
> + if (!commit) {
> + fprintf(stderr, "Could not get commit for %s. Skipping.\n",
> + *argv);
s/*argv/scratch_buf.buf/
> + continue;
> + }
>[snip]
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [PATCH v3 3/5] name-rev: factor code for sharing with a new command
2026-04-28 22:25 ` [PATCH v3 3/5] name-rev: factor code for sharing with a new command kristofferhaugsbakk
@ 2026-04-30 13:54 ` Phillip Wood
2026-05-01 17:24 ` kristofferhaugsbakk
0 siblings, 1 reply; 45+ messages in thread
From: Phillip Wood @ 2026-04-30 13:54 UTC (permalink / raw)
To: kristofferhaugsbakk, git; +Cc: Kristoffer Haugsbakk, ben.knoble
Hi Kristoffer
On 28/04/2026 23:25, kristofferhaugsbakk@fastmail.com wrote:
> From: Kristoffer Haugsbakk <code@khaugsbakk.name>
>
> @@ -516,6 +534,7 @@ static void name_rev_line(char *p, struct name_ref_data *data)
>
> for (p_start = p; *p; p++) {
> #define ishex(x) (isdigit((x)) || ((x) >= 'a' && (x) <= 'f'))
> + start:
> if (!ishex(*p)) {
> counter = 0;
> } else if (++counter == hexsz &&
> @@ -524,25 +543,32 @@ static void name_rev_line(char *p, struct name_ref_data *data)
> const char *name = NULL;
> char c = *(p + 1);
> int p_len = p - p_start + 1;
> + struct object *o = NULL;
> + int oid_ret = 1;
>
> counter = 0;
>
> *(p + 1) = 0;
> - if (!repo_get_oid(the_repository, p - (hexsz - 1), &oid)) {
> - struct object *o =
> - lookup_object(the_repository, &oid);
> + oid_ret = repo_get_oid(the_repository, p - (hexsz - 1), &oid);
It would be safer to restore *(p + 1) here rather that relying on each
case block to do it.
*(p + 1) = c;
> +
> + switch (cmd->type) {
> + case NAME_REV:
> + if (!oid_ret)
> + o = lookup_object(the_repository, &oid);
> if (o)
> name = get_rev_name(o, &buf);
> + *(p + 1) = c;
> + if (!name)
> + goto start;
The pre-image uses "continue" which will increment p - why the change in
behavior?
Thanks
Phillip
> + if (cmd->u.name_only)
> + printf("%.*s%s", p_len - hexsz, p_start, name);
> + else
> + printf("%.*s (%s)", p_len, p_start, name);
> + break;
> + default:
> + BUG("uncovered case: %d", cmd->type);
> }
> - *(p + 1) = c;
> -
> - if (!name)
> - continue;
>
> - if (data->name_only)
> - printf("%.*s%s", p_len - hexsz, p_start, name);
> - else
> - printf("%.*s (%s)", p_len, p_start, name);
> p_start = p + 1;
> }
> }
> @@ -567,6 +593,7 @@ int cmd_name_rev(int argc,
> #endif
> int all = 0, annotate_stdin = 0, allow_undefined = 1, always = 0, peel_tag = 0;
> struct name_ref_data data = { 0, 0, STRING_LIST_INIT_NODUP, STRING_LIST_INIT_NODUP };
> + struct command cmd;
> struct option opts[] = {
> OPT_BOOL(0, "name-only", &data.name_only, N_("print only ref-based names (no object names)")),
> OPT_BOOL(0, "tags", &data.tags_only, N_("only use tags to name the commits")),
> @@ -596,6 +623,7 @@ int cmd_name_rev(int argc,
> init_commit_rev_name(&rev_names);
> repo_config(the_repository, git_default_config, NULL);
> argc = parse_options(argc, argv, prefix, opts, name_rev_usage, 0);
> + init_name_rev_command(&cmd, data.name_only);
>
> #ifndef WITH_BREAKING_CHANGES
> if (transform_stdin) {
> @@ -663,7 +691,7 @@ int cmd_name_rev(int argc,
>
> while (strbuf_getline(&sb, stdin) != EOF) {
> strbuf_addch(&sb, '\n');
> - name_rev_line(sb.buf, &data);
> + name_rev_line(sb.buf, &cmd);
> }
> strbuf_release(&sb);
> } else if (all) {
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [PATCH v3 5/5] format-rev: introduce builtin for on-demand pretty formatting
2026-04-28 22:25 ` [PATCH v3 5/5] format-rev: introduce builtin for on-demand pretty formatting kristofferhaugsbakk
2026-04-29 13:41 ` Kristoffer Haugsbakk
2026-04-30 9:21 ` Kristoffer Haugsbakk
@ 2026-05-01 10:16 ` Phillip Wood
2026-05-01 18:27 ` kristofferhaugsbakk
2026-05-03 19:19 ` Junio C Hamano
3 siblings, 1 reply; 45+ messages in thread
From: Phillip Wood @ 2026-05-01 10:16 UTC (permalink / raw)
To: kristofferhaugsbakk, git; +Cc: Kristoffer Haugsbakk, ben.knoble
Hi Kristoffer
On 28/04/2026 23:25, kristofferhaugsbakk@fastmail.com wrote:
> From: Kristoffer Haugsbakk <code@khaugsbakk.name>
>
> Introduce a new builtin for pretty formatting one revision expression
> per line or commit object names found in running text.
>
> Sometimes you want to format commits. Most of the time you’re
> walking the graph, e.g. getting a range of commits like
> `master..topic`. That’s a job for git-log(1).
>
> But there are times when you want to format commits that you encounter
> on demand:
>
> • Full hashes in running text that you might want to pretty-print
> • git-last-modified(1) outputs full hashes that you can do the same
> with
> • git-cherry(1) has `-v` for commit subject, but maybe you want
> something else?
>
> But now you can’t use git-log(1), git-show(1), or git-rev-list(1):
>
> • You can’t feed commits piecemeal to these commands, one input
> for one output; they block until standard in is closed
So you can feed them piecemeal but you don't get any output until you
close stdin. That can be helpful as it means the calling process can
write to "git log --stdin" and then read the output without worrying
about getting deadlocked. The Implementation below works fine if there
are separate processes or threads writing to and reading from "git
format-rev", but if we want a single process to be able to read from and
write to "git format-rev --stdin-mode=text" there will need to be a way
to delimit message boundaries so that git knows where the input message
ends and the caller knows where the response ends. We'll also need to be
careful about flushing the output at the end of a processed message.
For "--stdin-mode=revs" the caller cannot know how many lines the output
will span because formats like %(trailers) will produce a variable
number of lines depending on which trailers are present. It is also
possible for a rev name to span more than one line. The following
example finds the most recent commit that mentions 'cherry-pick' in the
subject line
:/^[^
]cherry-pick
so we need a way to delimit the input and output records there as well.
I think the functionality implemented here is useful (transforming the
output of 'git blame' or 'git-last-modified' are convicing examples) and
it is probably better to do it as a command rather than adding a
"--format" option to name-rev.
> • You can’t feed a list of possibly duplicate commits, like the output
> of git-last-modified(1); they effectively deduplicate the output
That is definitely a problem
> Beyond these two points there’s also the input massage problem: you
s/massagge/message/?
Thanks
Phillip
> cannot feed mixed input (revisions mixed with arbitrary text).
>
> One might hope that git-cat-file(1) can save us. But it doesn’t
> support pretty formats.
>
> But there is one command that already both handles revisions as
> arguments, revisions on standard input, and even revisions mixed in
> with arbitrary text. Namely git-name-rev(1): the command for outputting
> symbolic names for commits.
>
> We made some room in `builtin/name-rev.c` two commits ago. Let’s
> now add this new git-format-rev(1) command. Taking inspiration from
> git-name-rev(1), there are two modes:
>
> • revs: like git-name-rev(1) in argv mode, but one revision per line
> on standard in
> • text: like git-name-rev(1) with `--annotate-stdin`
>
> ***
>
> We need to add this command to the exception list in
> `t/t1517-outside-repo.sh` because it uses “EXPERIMENTAL!”
> in the usage line.
>
> Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
> ---
>
> Notes (series):
> v3:
> • And don’t forget to document --notes this time
>
> https://lore.kernel.org/git/CALnO6CB5WOTp_e7Kv3CrEbQ+3XE-gDxNVHf7qATBEbyKWfxpLg@mail.gmail.com/
>
> Documentation/git-format-rev.adoc | 148 ++++++++++++++++++++++++
> Makefile | 1 +
> builtin.h | 1 +
> builtin/name-rev.c | 186 ++++++++++++++++++++++++++++++
> command-list.txt | 1 +
> git.c | 1 +
> t/t1517-outside-repo.sh | 3 +-
> t/t6120-describe.sh | 104 +++++++++++++++++
> 8 files changed, 444 insertions(+), 1 deletion(-)
> create mode 100644 Documentation/git-format-rev.adoc
>
> diff --git a/Documentation/git-format-rev.adoc b/Documentation/git-format-rev.adoc
> new file mode 100644
> index 00000000000..d960001d750
> --- /dev/null
> +++ b/Documentation/git-format-rev.adoc
> @@ -0,0 +1,148 @@
> +git-format-rev(1)
> +=================
> +
> +NAME
> +----
> +git-format-rev - EXPERIMENTAL: Pretty format revisions on demand
> +
> +
> +SYNOPSIS
> +--------
> +[synopsis]
> +(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> --format=<pretty> [--notes=<ref>]
> +
> +DESCRIPTION
> +-----------
> +
> +Pretty format revisions from standard input.
> +
> +THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
> +
> +OPTIONS
> +-------
> +
> +`--stdin-mode=<mode>`::
> + How to interpret standard input data:
> ++
> +--
> +`revs`:: Each line is interpreted as a commit. Any kind of revision
> + expression can be used (see linkgit:gitrevisions[7]). Annotated
> + tags are peeled (see linkgit:gitglossary[7]).
> ++
> +The argument `rev` is also accepted.
> +`text`:: Formats all commit object names found in freeform text. These
> + must the full object names, i.e. abbreviated hexidecimal object
> + names will not be interpreted.
> +--
> +
> +`--format=<pretty>`::
> + Pretty format string.
> +
> +`--notes=<ref>`::
> +`--no-notes`::
> + Custom notes ref. Notes are displayed when using the `%N`
> + atom. See linkgit:git-notes[1].
> +
> +EXAMPLES
> +--------
> +
> +The command linkgit:git-last-modified[1] shows the commit that each file
> +was last modified in.
> +
> +----
> +$ git last-modified -- README.md Makefile
> +7798034171030be0909c56377a4e0e10e6d2df93 Makefile
> +c50fbb2dd225e7e82abba4380423ae105089f4d7 README.md
> +----
> +
> +We can pipe the result to this command in order to replace the object
> +name with the commit author.
> +
> +----
> +$ git last-modified -- README.md Makefile |
> + git format-rev --stdin-mode=text --format=%an
> +Junio C Hamano Makefile
> +Todd Zullinger README.md
> +----
> +
> +Another example is _formatting commits in commit messages_. Given this commit message:
> +
> +----
> +Fix off-by-one error
> +
> +Fix off-by-one error introduced in
> +e83c5163316f89bfbde7d9ab23ca2e25604af290.
> +
> +We thought we fixed this in 5569bf9bbedd63a00780fc5c110e0cfab3aa97b9 but
> +that only covered 1/3 of the faulty cases.
> +----
> +
> +We can format the commits and use par(1) to reflow the text, say in a
> +`commit-msg` hook:
> +
> +----
> +$ git config set hook.reference-commits.event commit-msg
> +$ git config set hook.reference-commits.command reference-commits
> +$ cat $(which reference-commits)
> +#/bin/sh
> +
> +msg="$1"
> +rewritten=$(mktemp)
> +git format-rev --stdin-mode=text --format=reference <"$msg" |
> + par >"$rewritten"
> +mv "$rewritten" "$msg"
> +----
> +
> +Which will produce something like this:
> +
> +----
> +Fix off-by-one error
> +
> +Fix off-by-one error introduced in e83c5163316 (Implement better memory
> +allocator, 2005-04-07).
> +
> +We thought we fixed this in 5569bf9bbed (Fix memory allocator,
> +2005-06-22) but that only covered 1/3 of the faulty cases.
> +----
> +
> +DISCUSSION
> +----------
> +
> +This command lets you format any number of revisions in any order
> +through one command invocation. Consider the
> +linkgit:git-last-modified[1] case from the "EXAMPLES" section above:
> +
> +1. There might be hundreds of files
> +2. Commits can be repeated, i.e. two or more files were last modified in
> + the same commit
> +
> +Two widely-used commands which pretty formats commits are
> +linkgit:git-log[1] and linkgit:git-show[1]. It turns out that they are
> +not a good fit for the above use case.
> +
> +- The output of linkgit:git-last-modified[1] would have to be processed
> + in stages since you need to transform the first column separately and
> + then link the author to the filename. But this is surmountable.
> +- You can feed each commit to `git show` or `git show --no-walk -1`. But
> + that means that you need to create a process for each line.
> +- Let’s say that you want to use one process, not one per line. So you
> + want to feed all the commits to the command. Now you face the problem
> + that you have to feed all the commits to the commands before you get
> + any output (this is also the case for the `--stdin` modes). In other
> + words, you cannot loop through each line, get the author for the
> + commit, and output the author and the filename. You need to feed all
> + the commits, get back all the output, and match the output with the
> + filename.
> +- But the next problem is that commands will deduplicate the input and
> + only output one commit one single time only. Thus you cannot make the
> + output order match the input order, since a commit could have been
> + repeated in the original input.
> +
> +In short, it is straightforward to use these two commands if you use one
> +process per line. It is much more work if you just want to use one
> +process, but still doable. In contrast, this problem is just another
> +shell pipeline with this command.
> +
> +GIT
> +---
> +Part of the linkgit:git[1] suite
> diff --git a/Makefile b/Makefile
> index 15b1ded1a0b..cbaf91fd846 100644
> --- a/Makefile
> +++ b/Makefile
> @@ -895,6 +895,7 @@ BUILT_INS += $(patsubst builtin/%.o,git-%$X,$(BUILTIN_OBJS))
> BUILT_INS += git-cherry$X
> BUILT_INS += git-cherry-pick$X
> BUILT_INS += git-format-patch$X
> +BUILT_INS += git-format-rev$X
> BUILT_INS += git-fsck-objects$X
> BUILT_INS += git-init$X
> BUILT_INS += git-maintenance$X
> diff --git a/builtin.h b/builtin.h
> index 235c51f30e5..63813c90125 100644
> --- a/builtin.h
> +++ b/builtin.h
> @@ -189,6 +189,7 @@ int cmd_fmt_merge_msg(int argc, const char **argv, const char *prefix, struct re
> int cmd_for_each_ref(int argc, const char **argv, const char *prefix, struct repository *repo);
> int cmd_for_each_repo(int argc, const char **argv, const char *prefix, struct repository *repo);
> int cmd_format_patch(int argc, const char **argv, const char *prefix, struct repository *repo);
> +int cmd_format_rev(int argc, const char **argv, const char *prefix, struct repository *repo);
> int cmd_fsck(int argc, const char **argv, const char *prefix, struct repository *repo);
> int cmd_fsmonitor__daemon(int argc, const char **argv, const char *prefix, struct repository *repo);
> int cmd_gc(int argc, const char **argv, const char *prefix, struct repository *repo);
> diff --git a/builtin/name-rev.c b/builtin/name-rev.c
> index dc4136f4de3..b60cc766279 100644
> --- a/builtin/name-rev.c
> +++ b/builtin/name-rev.c
> @@ -18,6 +18,9 @@
> #include "commit-graph.h"
> #include "wildmatch.h"
> #include "mem-pool.h"
> +#include "pretty.h"
> +#include "revision.h"
> +#include "notes.h"
>
> /*
> * One day. See the 'name a rev shortly after epoch' test in t6120 when
> @@ -272,14 +275,26 @@ struct name_ref_data {
> struct string_list exclude_filters;
> };
>
> +struct pretty_format {
> + struct pretty_print_context ctx;
> + struct userformat_want want;
> +};
> +
> enum command_type {
> NAME_REV = 1,
> + FORMAT_REV = 2,
> +};
> +
> +enum stdin_mode {
> + TEXT = 1,
> + REVS = 2,
> };
>
> struct command {
> enum command_type type;
> union {
> int name_only;
> + struct pretty_format *pretty_format;
> } u;
> };
>
> @@ -290,6 +305,13 @@ static void init_name_rev_command(struct command *cmd,
> cmd->u.name_only = name_only;
> }
>
> +static void init_format_rev_command(struct command *cmd,
> + struct pretty_format *pretty_format)
> +{
> + cmd->type = FORMAT_REV;
> + cmd->u.pretty_format = pretty_format;
> +}
> +
> static struct tip_table {
> struct tip_table_entry {
> struct object_id oid;
> @@ -495,6 +517,27 @@ static const char *get_rev_name(const struct object *o, struct strbuf *buf)
> }
> }
>
> +static const char *get_format_rev(const struct commit *c,
> + struct pretty_format *format_ctx,
> + struct strbuf *buf)
> +{
> + strbuf_reset(buf);
> +
> + if (format_ctx->want.notes) {
> + struct strbuf notebuf = STRBUF_INIT;
> +
> + format_display_notes(&c->object.oid, ¬ebuf,
> + get_log_output_encoding(),
> + format_ctx->ctx.fmt == CMIT_FMT_USERFORMAT);
> + format_ctx->ctx.notes_message = strbuf_detach(¬ebuf, NULL);
> + }
> +
> + pretty_print_commit(&format_ctx->ctx, c, buf);
> + FREE_AND_NULL(format_ctx->ctx.notes_message);
> +
> + return buf->buf;
> +}
> +
> static void show_name(const struct object *obj,
> const char *caller_name,
> int always, int allow_undefined, int name_only)
> @@ -565,6 +608,19 @@ static void name_rev_line(char *p, struct command *cmd)
> else
> printf("%.*s (%s)", p_len, p_start, name);
> break;
> + case FORMAT_REV:
> + if (!oid_ret)
> + o = parse_object(the_repository, &oid);
> + if (o && o->type == OBJ_COMMIT)
> + name = get_format_rev((const struct commit *)o,
> + cmd->u.pretty_format,
> + &buf);
> + *(p + 1) = c;
> + if (name)
> + printf("%.*s%s", p_len - hexsz, p_start, name);
> + else
> + printf("%.*s", p_len, p_start);
> + break;
> default:
> BUG("uncovered case: %d", cmd->type);
> }
> @@ -718,3 +774,133 @@ int cmd_name_rev(int argc,
> object_array_clear(&revs);
> return 0;
> }
> +
> +static enum stdin_mode parse_stdin_mode(const char *stdin_mode)
> +{
> + if (!strcmp(stdin_mode, "text"))
> + return TEXT;
> + else if (!strcmp(stdin_mode, "revs") ||
> + !strcmp(stdin_mode, "rev"))
> + return REVS;
> + else
> + die(_("'%s' needs to be either text, revs, or rev"),
> + "--stdin-mode");
> +}
> +
> +static char const *const format_rev_usage[] = {
> + N_("(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> --format=<pretty> [--notes=<ref>]"),
> + NULL
> +};
> +
> +int cmd_format_rev(int argc,
> + const char **argv,
> + const char *prefix,
> + struct repository *repo UNUSED)
> +{
> + const char *format = NULL;
> + enum stdin_mode stdin_mode;
> + const char *stdin_mode_arg = NULL;
> + struct display_notes_opt format_notes_opt;
> + struct rev_info format_rev = REV_INFO_INIT;
> + struct pretty_format format_pp = { 0 };
> + struct string_list notes = STRING_LIST_INIT_NODUP;
> + struct strbuf scratch_buf = STRBUF_INIT;
> + struct command cmd;
> + struct option opts[] = {
> + OPT_STRING(0, "format", &format, N_("format"),
> + N_("pretty format to use")),
> + OPT_STRING(0, "stdin-mode", &stdin_mode_arg, N_("stdin-mode"),
> + N_("how revs are processed")),
> + OPT_STRING_LIST(0, "notes", ¬es, N_("notes"),
> + N_("display notes for pretty format")),
> + OPT_END(),
> + };
> +
> + argc = parse_options(argc, argv, prefix, opts, format_rev_usage, 0);
> +
> + if (argc > 0) {
> + error(_("too many arguments"));
> + usage_with_options(format_rev_usage, opts);
> + }
> +
> + if (!format)
> + die(_("'%s' is required"), "--format");
> + if (!stdin_mode_arg)
> + die(_("'%s' is required"), "--stdin-mode");
> +
> + init_display_notes(&format_notes_opt);
> + stdin_mode = parse_stdin_mode(stdin_mode_arg);
> +
> + get_commit_format(format, &format_rev);
> + format_pp.ctx.rev = &format_rev;
> + format_pp.ctx.fmt = format_rev.commit_format;
> + format_pp.ctx.abbrev = format_rev.abbrev;
> + format_pp.ctx.date_mode_explicit = format_rev.date_mode_explicit;
> + format_pp.ctx.date_mode = format_rev.date_mode;
> + format_pp.ctx.color = GIT_COLOR_AUTO;
> +
> + userformat_find_requirements(format,
> + &format_pp.want);
> + if (format_pp.want.notes) {
> + int ignore_show_notes = 0;
> + struct string_list_item *n;
> +
> + for_each_string_list_item(n, ¬es)
> + enable_ref_display_notes(&format_notes_opt,
> + &ignore_show_notes,
> + n->string);
> + load_display_notes(&format_notes_opt);
> + }
> +
> + init_format_rev_command(&cmd, &format_pp);
> +
> + switch (stdin_mode) {
> + case TEXT:
> + while (strbuf_getline(&scratch_buf, stdin) != EOF) {
> + strbuf_addch(&scratch_buf, '\n');
> + name_rev_line(scratch_buf.buf, &cmd);
> + }
> + break;
> + case REVS:
> + while (strbuf_getline(&scratch_buf, stdin) != EOF) {
> + struct object_id oid;
> + struct object *object;
> + struct object *peeled;
> + struct commit *commit;
> +
> + if (repo_get_oid(the_repository, scratch_buf.buf, &oid)) {
> + fprintf(stderr, "Could not get sha1 for %s. Skipping.\n",
> + scratch_buf.buf);
> + continue;
> + }
> +
> + object = parse_object(the_repository, &oid);
> + if (!object) {
> + fprintf(stderr, "Could not get object for %s. Skipping.\n",
> + scratch_buf.buf);
> + continue;
> + }
> +
> + peeled = deref_tag(the_repository, object, scratch_buf.buf, 0);
> + if (peeled && peeled->type == OBJ_COMMIT)
> + commit = (struct commit *)peeled;
> + if (!commit) {
> + fprintf(stderr, "Could not get commit for %s. Skipping.\n",
> + *argv);
> + continue;
> + }
> +
> + get_format_rev(commit, &format_pp, &scratch_buf);
> + printf("%s\n", scratch_buf.buf);
> + strbuf_release(&scratch_buf);
> + }
> + break;
> + default:
> + BUG("uncovered case: %d", stdin_mode);
> + }
> +
> + strbuf_release(&scratch_buf);
> + string_list_clear(¬es, 0);
> + release_display_notes(&format_notes_opt);
> + return 0;
> +}
> diff --git a/command-list.txt b/command-list.txt
> index f9005cf4597..df729872dca 100644
> --- a/command-list.txt
> +++ b/command-list.txt
> @@ -108,6 +108,7 @@ git-fmt-merge-msg purehelpers
> git-for-each-ref plumbinginterrogators
> git-for-each-repo plumbinginterrogators
> git-format-patch mainporcelain
> +git-format-rev plumbinginterrogators
> git-fsck ancillaryinterrogators complete
> git-gc mainporcelain
> git-get-tar-commit-id plumbinginterrogators
> diff --git a/git.c b/git.c
> index 2b212e6675d..af5b0422b00 100644
> --- a/git.c
> +++ b/git.c
> @@ -578,6 +578,7 @@ static struct cmd_struct commands[] = {
> { "for-each-ref", cmd_for_each_ref, RUN_SETUP },
> { "for-each-repo", cmd_for_each_repo, RUN_SETUP_GENTLY },
> { "format-patch", cmd_format_patch, RUN_SETUP },
> + { "format-rev", cmd_format_rev, RUN_SETUP },
> { "fsck", cmd_fsck, RUN_SETUP },
> { "fsck-objects", cmd_fsck, RUN_SETUP },
> { "fsmonitor--daemon", cmd_fsmonitor__daemon, RUN_SETUP },
> diff --git a/t/t1517-outside-repo.sh b/t/t1517-outside-repo.sh
> index c824c1a25cf..360a9323343 100755
> --- a/t/t1517-outside-repo.sh
> +++ b/t/t1517-outside-repo.sh
> @@ -114,7 +114,8 @@ do
> archimport | citool | credential-netrc | credential-libsecret | \
> credential-osxkeychain | cvsexportcommit | cvsimport | cvsserver | \
> daemon | \
> - difftool--helper | filter-branch | fsck-objects | get-tar-commit-id | \
> + difftool--helper | filter-branch | format-rev | fsck-objects | \
> + get-tar-commit-id | \
> gui | gui--askpass | \
> http-backend | http-fetch | http-push | init-db | \
> merge-octopus | merge-one-file | merge-resolve | mergetool | \
> diff --git a/t/t6120-describe.sh b/t/t6120-describe.sh
> index 62789f76381..725f7d81b6b 100755
> --- a/t/t6120-describe.sh
> +++ b/t/t6120-describe.sh
> @@ -801,4 +801,108 @@ test_expect_success 'do not be fooled by invalid describe format ' '
> test_must_fail git cat-file -t "refs/tags/super-invalid/./../...../ ~^:/?*[////\\\\\\&}/busted.lock-42-g"$(cat out)
> '
>
> +test_expect_success 'name-rev --format setup' '
> + mkdir repo-format &&
> + git -C repo-format init &&
> + test_commit -C repo-format first &&
> + test_commit -C repo-format second &&
> + test_commit -C repo-format third &&
> + test_commit -C repo-format fourth &&
> + test_commit -C repo-format fifth &&
> + test_commit -C repo-format sixth &&
> + test_commit -C repo-format seventh &&
> + test_commit -C repo-format eighth
> +'
> +
> +test_expect_success 'format-rev --stdin-mode=revs' '
> + cat >expect <<-\EOF &&
> + eighth
> + seventh
> + fifth
> + EOF
> + git -C repo-format format-rev --stdin-mode=revs \
> + --format=%s >actual <<-\EOF &&
> + HEAD
> + HEAD~
> + HEAD~3
> + EOF
> + test_cmp expect actual
> +'
> +
> +test_expect_success 'format-rev --stdin-mode=text from rev-list same as log' '
> + git -C repo-format log --format=reference >expect &&
> + test_file_not_empty expect &&
> + git -C repo-format rev-list HEAD >list &&
> + git -C repo-format format-rev --stdin-mode=text \
> + --format=reference <list >actual &&
> + test_cmp expect actual
> +'
> +
> +test_expect_success 'format-rev --stdin-mode=text with running text and tree oid' '
> + cmit_oid=$(git -C repo-format rev-parse :/fifth) &&
> + reference=$(git -C repo-format log -n1 --format=reference :/fifth) &&
> + tree=$(git -C repo-format rev-parse HEAD^{tree}) &&
> + cat >expect <<-EOF &&
> + We thought we fixed this in ${reference}.
> + But look at this tree: ${tree}.
> + EOF
> + git -C repo-format format-rev --stdin-mode=text --format=reference \
> + >actual <<-EOF &&
> + We thought we fixed this in ${cmit_oid}.
> + But look at this tree: ${tree}.
> + EOF
> + test_cmp expect actual
> +'
> +
> +test_expect_success 'format-rev with %N (note)' '
> + test_when_finished "git -C repo-format notes remove" &&
> + git -C repo-format notes add -m"Make a note" &&
> + printf "Make a note\n\n\n" >expect &&
> + git -C repo-format format-rev --stdin-mode=revs \
> + --format="tformat:%N" \
> + >actual <<-\EOF &&
> + HEAD
> + HEAD~
> + EOF
> + test_cmp expect actual
> +'
> +
> +test_expect_success 'format-rev --notes<ref> (custom notes ref)' '
> + # One custom notes ref
> + test_when_finished "git -C repo-format notes remove" &&
> + test_when_finished "git -C repo-format notes --ref=word remove" &&
> + git -C repo-format notes add -m"default" &&
> + git -C repo-format notes --ref=word add -m"custom" &&
> + printf "custom\n\n" >expect &&
> + git -C repo-format format-rev --stdin-mode=revs \
> + --format="tformat:%N" \
> + --notes=word \
> + >actual <<-\EOF &&
> + HEAD
> + EOF
> + test_cmp expect actual &&
> + # Glob all
> + printf "default\ncustom\n\n" >expect &&
> + git -C repo-format format-rev --stdin-mode=revs \
> + --format="tformat:%N" \
> + --notes=* >actual <<-\EOF &&
> + HEAD
> + EOF
> + test_cmp expect actual
> +'
> +
> +test_expect_success 'format-rev --stdin-mode=revs on annotated tag peels to commit' '
> + test_when_finished "git -C repo-format tag -d version" &&
> + git -C repo-format tag -a -m"new version" version &&
> + cat >expect <<-\EOF &&
> + eighth
> + EOF
> + git -C repo-format format-rev --stdin-mode=revs \
> + --format=%s \
> + --notes=* >actual <<-\EOF &&
> + version
> + EOF
> + test_cmp expect actual
> +'
> +
> test_done
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [PATCH v3 3/5] name-rev: factor code for sharing with a new command
2026-04-30 13:54 ` Phillip Wood
@ 2026-05-01 17:24 ` kristofferhaugsbakk
2026-05-02 10:00 ` Phillip Wood
0 siblings, 1 reply; 45+ messages in thread
From: kristofferhaugsbakk @ 2026-05-01 17:24 UTC (permalink / raw)
To: phillip.wood123; +Cc: ben.knoble, git, kristofferhaugsbakk, phillip.wood
Hi Phillip. Thanks for taking a look.
On Thu, Apr 30, 2026, at 15:54, Phillip Wood wrote:
>>[snip]
>> @@ -524,25 +543,32 @@ static void name_rev_line(char *p, struct name_ref_data *data)
>> const char *name = NULL;
>> char c = *(p + 1);
>> int p_len = p - p_start + 1;
>> + struct object *o = NULL;
>> + int oid_ret = 1;
>>
>> counter = 0;
>>
>> *(p + 1) = 0;
>> - if (!repo_get_oid(the_repository, p - (hexsz - 1), &oid)) {
>> - struct object *o =
>> - lookup_object(the_repository, &oid);
>> + oid_ret = repo_get_oid(the_repository, p - (hexsz - 1), &oid);
>
> It would be safer to restore *(p + 1) here rather that relying on each
> case block to do it.
Yeah, I didn’t want to repeat that bookkeeping but in some iteration it
looked necessary. But it’s good that it isn’t.
>
> *(p + 1) = c;
>> +
>> + switch (cmd->type) {
>> + case NAME_REV:
>> + if (!oid_ret)
>> + o = lookup_object(the_repository, &oid);
>> if (o)
>> name = get_rev_name(o, &buf);
>> + *(p + 1) = c;
>> + if (!name)
>> + goto start;
>
> The pre-image uses "continue" which will increment p - why the change in
> behavior?
They looked the same to me. So I will need to think about this some
more. Just a lack of C experience on my part.
Replacing the `continue` with a goto at the start of the loop was also
unnecessary. Of course the `continue` breaks out of the loop and not the
switch-block (unlike `break`).
But I didn’t break `t6120-describe.sh`. So I’ll also take a look to see
if there are any holes.
Thanks again.
>[snip]
--
Happy May Day
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [PATCH v3 5/5] format-rev: introduce builtin for on-demand pretty formatting
2026-05-01 10:16 ` Phillip Wood
@ 2026-05-01 18:27 ` kristofferhaugsbakk
2026-05-02 10:00 ` Phillip Wood
0 siblings, 1 reply; 45+ messages in thread
From: kristofferhaugsbakk @ 2026-05-01 18:27 UTC (permalink / raw)
To: phillip.wood123; +Cc: ben.knoble, git, kristofferhaugsbakk
On Fri, May 1, 2026, at 12:16, Phillip Wood wrote:
>>[snip]
>> • You can’t feed commits piecemeal to these commands, one input
>> for one output; they block until standard in is closed
>
> So you can feed them piecemeal but you don't get any output until you
> close stdin. That can be helpful as it means the calling process can
> write to "git log --stdin" and then read the output without worrying
> about getting deadlocked.
Okay. I don’t have much experience with concurrent programming.
> The Implementation below works fine if there
> are separate processes or threads writing to and reading from "git
> format-rev", but if we want a single process to be able to read from and
> write to "git format-rev --stdin-mode=text" there will need to be a way
> to delimit message boundaries so that git knows where the input message
> ends and the caller knows where the response ends.
Okay, so I guess a null-terminator mode for output.
> We'll also need to be
> careful about flushing the output at the end of a processed message.
I don’t get why this takes special care. I’ll think about it.
> For "--stdin-mode=revs" the caller cannot know how many lines the output
> will span because formats like %(trailers) will produce a variable
> number of lines depending on which trailers are present. It is also
> possible for a rev name to span more than one line. The following
> example finds the most recent commit that mentions 'cherry-pick' in the
> subject line
>
> :/^[^
> ]cherry-pick
>
> so we need a way to delimit the input and output records there as well.
Okay, so a null-terminator mode for input as well?
> I think the functionality implemented here is useful (transforming the
> output of 'git blame' or 'git-last-modified' are convicing examples) and
> it is probably better to do it as a command rather than adding a
> "--format" option to name-rev.
>
>> • You can’t feed a list of possibly duplicate commits, like the output
>> of git-last-modified(1); they effectively deduplicate the output
>
> That is definitely a problem
Great, thanks.
>> Beyond these two points there’s also the input massage problem: you
>
> s/massagge/message/?
No. I meant massaging the input so that it can be processed by whatever
tool you have. :) In this case splitting the object name column and file
column because tools like git-log(1) can only deal with revision input.
Thanks for reviewing the usability design.
--
Happy May Day
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [PATCH v3 5/5] format-rev: introduce builtin for on-demand pretty formatting
2026-05-01 18:27 ` kristofferhaugsbakk
@ 2026-05-02 10:00 ` Phillip Wood
2026-05-05 19:27 ` Kristoffer Haugsbakk
0 siblings, 1 reply; 45+ messages in thread
From: Phillip Wood @ 2026-05-02 10:00 UTC (permalink / raw)
To: kristofferhaugsbakk; +Cc: ben.knoble, git
Hi Kristoffer
On 01/05/2026 19:27, kristofferhaugsbakk@fastmail.com wrote:
> On Fri, May 1, 2026, at 12:16, Phillip Wood wrote:
>>> [snip]
>>> • You can’t feed commits piecemeal to these commands, one input
>>> for one output; they block until standard in is closed
>>
>> So you can feed them piecemeal but you don't get any output until you
>> close stdin. That can be helpful as it means the calling process can
>> write to "git log --stdin" and then read the output without worrying
>> about getting deadlocked.
>
> Okay. I don’t have much experience with concurrent programming.
>
>> The Implementation below works fine if there
>> are separate processes or threads writing to and reading from "git
>> format-rev", but if we want a single process to be able to read from and
>> write to "git format-rev --stdin-mode=text" there will need to be a way
>> to delimit message boundaries so that git knows where the input message
>> ends and the caller knows where the response ends.
>
> Okay, so I guess a null-terminator mode for output.
I think that's a good idea
>> We'll also need to be
>> careful about flushing the output at the end of a processed message.
>
> I don’t get why this takes special care. I’ll think about it.
Because the output from printf() is buffered, unless you explicitly
flush it you can get into a state where git thinks it has printed the
output and is waiting for the caller to write more input, but the caller
is still waiting to read git's output and so they are deadlocked.
Calling maybe_flush_or_die() is the usual way to handle this I think -
see 344a107b55 (merge-tree --stdin: flush stdout to avoid deadlock,
2025-02-18)
>> For "--stdin-mode=revs" the caller cannot know how many lines the output
>> will span because formats like %(trailers) will produce a variable
>> number of lines depending on which trailers are present. It is also
>> possible for a rev name to span more than one line. The following
>> example finds the most recent commit that mentions 'cherry-pick' in the
>> subject line
>>
>> :/^[^
>> ]cherry-pick
>>
>> so we need a way to delimit the input and output records there as well.
>
> Okay, so a null-terminator mode for input as well?
Yes I think "-z" should mean NUL terminated input and output.
>> I think the functionality implemented here is useful (transforming the
>> output of 'git blame' or 'git-last-modified' are convicing examples) and
>> it is probably better to do it as a command rather than adding a
>> "--format" option to name-rev.
>>
>>> • You can’t feed a list of possibly duplicate commits, like the output
>>> of git-last-modified(1); they effectively deduplicate the output
>>
>> That is definitely a problem
>
> Great, thanks.
>
>>> Beyond these two points there’s also the input massage problem: you
>>
>> s/massagge/message/?
>
> No. I meant massaging the input so that it can be processed by whatever
> tool you have. :) In this case splitting the object name column and file
> column because tools like git-log(1) can only deal with revision input.
Oh I see
Thanks
Phillip
> Thanks for reviewing the usability design.
>
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [PATCH v3 3/5] name-rev: factor code for sharing with a new command
2026-05-01 17:24 ` kristofferhaugsbakk
@ 2026-05-02 10:00 ` Phillip Wood
2026-05-05 19:21 ` Kristoffer Haugsbakk
0 siblings, 1 reply; 45+ messages in thread
From: Phillip Wood @ 2026-05-02 10:00 UTC (permalink / raw)
To: kristofferhaugsbakk; +Cc: ben.knoble, git, phillip.wood
On 01/05/2026 18:24, kristofferhaugsbakk@fastmail.com wrote:
> Hi Phillip. Thanks for taking a look.
>
> On Thu, Apr 30, 2026, at 15:54, Phillip Wood wrote:
>>> [snip]
>>> @@ -524,25 +543,32 @@ static void name_rev_line(char *p, struct name_ref_data *data)
>>> const char *name = NULL;
>>> char c = *(p + 1);
>>> int p_len = p - p_start + 1;
>>> + struct object *o = NULL;
>>> + int oid_ret = 1;
>>>
>>> counter = 0;
>>>
>>> *(p + 1) = 0;
>>> - if (!repo_get_oid(the_repository, p - (hexsz - 1), &oid)) {
>>> - struct object *o =
>>> - lookup_object(the_repository, &oid);
>>> + oid_ret = repo_get_oid(the_repository, p - (hexsz - 1), &oid);
>>
>> It would be safer to restore *(p + 1) here rather that relying on each
>> case block to do it.
>
> Yeah, I didn’t want to repeat that bookkeeping but in some iteration it
> looked necessary. But it’s good that it isn’t.
It looks like the printing code is shared between the two case blocks in
patch 5 as well so we should move that outside them as well and just set
"name" inside the switch statement.
>> *(p + 1) = c;
>>> +
>>> + switch (cmd->type) {
>>> + case NAME_REV:
>>> + if (!oid_ret)
>>> + o = lookup_object(the_repository, &oid);
>>> if (o)
>>> name = get_rev_name(o, &buf);
>>> + *(p + 1) = c;
>>> + if (!name)
>>> + goto start;
>>
>> The pre-image uses "continue" which will increment p - why the change in
>> behavior?
>
> They looked the same to me. So I will need to think about this some
> more. Just a lack of C experience on my part.
>
> Replacing the `continue` with a goto at the start of the loop was also
> unnecessary. Of course the `continue` breaks out of the loop and not the
> switch-block (unlike `break`).
>
> But I didn’t break `t6120-describe.sh`. So I’ll also take a look to see
> if there are any holes.
I think the difference only matters in pathological cases as "goto
start" means we end up looking at the same character twice but the loop
carries on as normal after that. We should just keep using "continue",
I'm not sure we need a new test case.
Thanks
Phillip
>
> Thanks again.
>
>> [snip]
>
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [PATCH v3 5/5] format-rev: introduce builtin for on-demand pretty formatting
2026-04-28 22:25 ` [PATCH v3 5/5] format-rev: introduce builtin for on-demand pretty formatting kristofferhaugsbakk
` (2 preceding siblings ...)
2026-05-01 10:16 ` Phillip Wood
@ 2026-05-03 19:19 ` Junio C Hamano
3 siblings, 0 replies; 45+ messages in thread
From: Junio C Hamano @ 2026-05-03 19:19 UTC (permalink / raw)
To: kristofferhaugsbakk; +Cc: git, Kristoffer Haugsbakk, ben.knoble
builtin/name-rev.c:883:8: error: variable 'commit' is used uninitialized whenever 'if' condition is false [-Werror,-Wsometimes-uninitialized]
883 | if (peeled && peeled->type == OBJ_COMMIT)
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
builtin/name-rev.c:885:9: note: uninitialized use occurs here
885 | if (!commit) {
| ^~~~~~
builtin/name-rev.c:883:4: note: remove the 'if' if its condition is always true
883 | if (peeled && peeled->type == OBJ_COMMIT)
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
884 | commit = (struct commit *)peeled;
builtin/name-rev.c:883:8: error: variable 'commit' is used uninitialized whenever '&&' condition is false [-Werror,-Wsometimes-uninitialized]
883 | if (peeled && peeled->type == OBJ_COMMIT)
| ^~~~~~
builtin/name-rev.c:885:9: note: uninitialized use occurs here
885 | if (!commit) {
| ^~~~~~
builtin/name-rev.c:883:8: note: remove the '&&' if its condition is always true
883 | if (peeled && peeled->type == OBJ_COMMIT)
| ^~~~~~~~~
builtin/name-rev.c:867:25: note: initialize the variable 'commit' to silence this warning
867 | struct commit *commit;
| ^
| = NULL
2 errors generated.
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [PATCH v3 3/5] name-rev: factor code for sharing with a new command
2026-05-02 10:00 ` Phillip Wood
@ 2026-05-05 19:21 ` Kristoffer Haugsbakk
0 siblings, 0 replies; 45+ messages in thread
From: Kristoffer Haugsbakk @ 2026-05-05 19:21 UTC (permalink / raw)
To: Phillip Wood; +Cc: D. Ben Knoble, git, Phillip Wood
On Sat, May 2, 2026, at 12:00, Phillip Wood wrote:
>>>[snip]
>>
>> Yeah, I didn’t want to repeat that bookkeeping but in some iteration it
>> looked necessary. But it’s good that it isn’t.
>
> It looks like the printing code is shared between the two case blocks in
> patch 5 as well so we should move that outside them as well and just set
> "name" inside the switch statement.
They’re not shared. Both commands work the same: either the object name
is consumed and replaced or it is written out as-is, in other words when
commit lookup fails. But somehow the name-rev path prints that *failure
to look up* case before continuing here (I don’t know how):
if (!name)
continue;
Because the printf(3) only prints when a symbolic name was found. Either
name-only:
<symbolic name>
Or not name-only:
<object name> (<symbolic name>)
On the other hand format-rev uses those two print statements to output
either the name lookup case or the lookup failure case.
>>>[snip]
>>
>> They looked the same to me. So I will need to think about this some
>> more. Just a lack of C experience on my part.
>>
>> Replacing the `continue` with a goto at the start of the loop was also
>> unnecessary. Of course the `continue` breaks out of the loop and not the
>> switch-block (unlike `break`).
>>
>> But I didn’t break `t6120-describe.sh`. So I’ll also take a look to see
>> if there are any holes.
>
> I think the difference only matters in pathological cases as "goto
> start" means we end up looking at the same character twice but the loop
> carries on as normal after that. We should just keep using "continue",
> I'm not sure we need a new test case.
Yeah, I have gone back to the sensible `continue`. Thanks.
(To be honest I tried to provoke a parsing bug here but I was unable
to. Somewhat annoying.)
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [PATCH v3 5/5] format-rev: introduce builtin for on-demand pretty formatting
2026-05-02 10:00 ` Phillip Wood
@ 2026-05-05 19:27 ` Kristoffer Haugsbakk
0 siblings, 0 replies; 45+ messages in thread
From: Kristoffer Haugsbakk @ 2026-05-05 19:27 UTC (permalink / raw)
To: Phillip Wood; +Cc: D. Ben Knoble, git
On Sat, May 2, 2026, at 12:00, Phillip Wood wrote:
>>>[snip]
>>> We'll also need to be
>>> careful about flushing the output at the end of a processed message.
>>
>> I don’t get why this takes special care. I’ll think about it.
>
> Because the output from printf() is buffered, unless you explicitly
> flush it you can get into a state where git thinks it has printed the
> output and is waiting for the caller to write more input, but the caller
> is still waiting to read git's output and so they are deadlocked.
> Calling maybe_flush_or_die() is the usual way to handle this I think -
> see 344a107b55 (merge-tree --stdin: flush stdout to avoid deadlock,
> 2025-02-18)
Ah, I understand now. Very well explained. Thanks :)
>>> For "--stdin-mode=revs" the caller cannot know how many lines the output
>>> will span because formats like %(trailers) will produce a variable
>>> number of lines depending on which trailers are present. It is also
>>> possible for a rev name to span more than one line. The following
>>> example finds the most recent commit that mentions 'cherry-pick' in the
>>> subject line
>>>
>>> :/^[^
>>> ]cherry-pick
>>>
>>> so we need a way to delimit the input and output records there as well.
>>
>> Okay, so a null-terminator mode for input as well?
>
> Yes I think "-z" should mean NUL terminated input and output.
I will use `-z` (and `--null`) to mean NUL terminated input and output
based on your recommendation and because I see that it is the approach
used in other commands that I have found that have NUL termination for
both stdin and stdout.
I also want to supply `--null-input` and `--null-output` since I think
`--null-output` will be more generally useful.
***
That these long options ended up being called `--null` instead of
`--nul` by convention is maybe just a historical accident? Considering
one writes NUL byte/character.
>[snip]
^ permalink raw reply [flat|nested] 45+ messages in thread
* [PATCH v4 0/5] format-rev: introduce builtin for on-demand pretty formatting
2026-04-28 22:25 ` [PATCH v3 0/5] format-rev: introduce builtin for on-demand pretty formatting kristofferhaugsbakk
` (4 preceding siblings ...)
2026-04-28 22:25 ` [PATCH v3 5/5] format-rev: introduce builtin for on-demand pretty formatting kristofferhaugsbakk
@ 2026-05-07 19:34 ` kristofferhaugsbakk
2026-05-07 19:34 ` [PATCH v4 1/5] name-rev: wrap both blocks in braces kristofferhaugsbakk
` (5 more replies)
5 siblings, 6 replies; 45+ messages in thread
From: kristofferhaugsbakk @ 2026-05-07 19:34 UTC (permalink / raw)
To: git; +Cc: Kristoffer Haugsbakk, ben.knoble, Phillip Wood, Ramsay Jones
From: Kristoffer Haugsbakk <code@khaugsbakk.name>
(Subject from v2: name-rev: learn --format=<pretty>)
Topic name (applied): kh/name-rev-custom-format
Topic summary: Introduce a new builtin for pretty formatting either (1) one
revision expression per line or (2) commit object names found in running
text.
See the last patch for the motivation. In short there isn’t anything that I
have found that lets you format however many commits you want through one
process (so looping over `git show` is excepted).
The other patches prepare for this change.
§ Changes in v4
Main changes are all in the final patch, with details on the Note there:
• Fix `-Werror=maybe-uninitialized` error
• Add NUL terminator options
• Use `maybe_flush_or_die(...)` to avoid potential deadlocks.
Also changes to patch 3/5 (see details on the Note).
There is also a lot of discussion about the input and output format and how
it works. I don’t know if it is valuable or just noise. Like this part:
Regarding input in this mode: using `-z` or `--null-input` makes sure
that _NUL_ characters in the input are passed through correctly.
This is just saying that NUL in the input will cut the processing short
unless `--null-input` is given (because we loop over a NUL-terminated C
string in `name_rev_line(...)`).
§ CI/CB
This time I ran GitHub actions (in my private fork) and let it
finish *before* sending this out.[1]
A lot of failures there but I did not see any errors relating to my
changes. I could just see `git-compat-util.h` errors and C11 `Generic`.
† 1: https://lore.kernel.org/git/374661c1-4676-4538-af24-0564f38469ca@app.fastmail.com/
§ Why experimental command? (unchanged in v4)
This command is marked Experimental three times (like git-replay(1)).[2]
Not so much because the UI design seems difficult. It’s more so that it can
be thrown out if it doesn’t end up being worth having around.
(*Experimental* also implies wholesale trashing. Right? That’s one possible
change in behavior.)
Or the behavior here could be moved somewhere else.
† 2: I vaguely recall reading a good argument for emphasizing this on the
mailing list. I wasn’t able to find back to it right now.
§ Outstanding work
I could look more into how to do the translation strings for “X is
require” (arg), I’m probably missing something that already exists.
§ Link to v3
https://lore.kernel.org/git/V3_format-rev_new_builtin.66f@msgid.xyz/
[1/5] name-rev: wrap both blocks in braces
[2/5] name-rev: run clang-format before factoring code
[3/5] name-rev: factor code for sharing with a new command
[4/5] name-rev: make dedicated --annotate-stdin --name-only test
[5/5] format-rev: introduce builtin for on-demand pretty formatting
.gitignore | 1 +
Documentation/git-format-rev.adoc | 215 +++++++++++++++++++++
Documentation/meson.build | 1 +
Makefile | 1 +
builtin.h | 1 +
builtin/name-rev.c | 300 +++++++++++++++++++++++++++---
command-list.txt | 1 +
git.c | 1 +
t/t1517-outside-repo.sh | 3 +-
t/t6120-describe.sh | 208 +++++++++++++++++++++
10 files changed, 706 insertions(+), 26 deletions(-)
create mode 100644 Documentation/git-format-rev.adoc
Interdiff against v3:
diff --git a/.gitignore b/.gitignore
index 24635cf2d6f..e406d3741cd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -71,6 +71,7 @@
/git-for-each-ref
/git-for-each-repo
/git-format-patch
+/git-format-rev
/git-fsck
/git-fsck-objects
/git-fsmonitor--daemon
diff --git a/Documentation/git-format-rev.adoc b/Documentation/git-format-rev.adoc
index d960001d750..436980012bc 100644
--- a/Documentation/git-format-rev.adoc
+++ b/Documentation/git-format-rev.adoc
@@ -9,7 +9,7 @@ git-format-rev - EXPERIMENTAL: Pretty format revisions on demand
SYNOPSIS
--------
[synopsis]
-(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> --format=<pretty> [--notes=<ref>]
+(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> --format=<pretty> [--[no-]notes=<ref>] [-z] [--[no-]null-output] [--[no-]null-input]
DESCRIPTION
-----------
@@ -25,7 +25,8 @@ OPTIONS
How to interpret standard input data:
+
--
-`revs`:: Each line is interpreted as a commit. Any kind of revision
+`revs`:: Each line or record (see the <<io,INPUT AND OUTPUT FORMATS>>
+ section) is interpreted as a commit. Any kind of revision
expression can be used (see linkgit:gitrevisions[7]). Annotated
tags are peeled (see linkgit:gitglossary[7]).
+
@@ -33,6 +34,9 @@ The argument `rev` is also accepted.
`text`:: Formats all commit object names found in freeform text. These
must the full object names, i.e. abbreviated hexidecimal object
names will not be interpreted.
++
+Anything that is parsed as an object name but that is not found to be a
+commit object name is left alone (echoed).
--
`--format=<pretty>`::
@@ -43,6 +47,63 @@ The argument `rev` is also accepted.
Custom notes ref. Notes are displayed when using the `%N`
atom. See linkgit:git-notes[1].
+`-z`::
+`--null`::
+ Use _NUL_ character to terminate both input and output instead
+ of newline. This option cannot be negated.
++
+This is useful if both the input and output could contain newlines or if
+the input could contain _NUL_ characters; see the <<io,INPUT AND OUTPUT
+FORMATS>> section.
+
+`--null-output`::
+`--no-null-output`::
+ Use _NUL_ character to terminate output instead of newline. The
+ default is `--no-null-output`.
++
+This is useful if the output could contain newlines, for example if the
+`%n` (newline) atom is used.
+
+`--null-input`::
+`--no-null-input`::
+ Use _NUL_ character to terminate input instead of newline. The
+ default is `--no-null-input`.
++
+This is useful if the input revision expressions could contain newlines.
+It is also useful if the input could contain _NUL_ characters; see the
+<<io,INPUT AND OUTPUT FORMATS>> section.
+
+[[io]]
+INPUT AND OUTPUT FORMAT
+-----------------------
+
+The command uses newlines for both input and output termination by
+default. See the `-z`, `--null-output`, and `--null-input` options for
+using _NUL_ character as the terminator.
+
+The mode `--stdin-mode=revs` outputs one formatted commit followed by
+the terminator. This could either be called a _line_ or a _record_ in
+case "line" is too suggestive of newline termination.
+
+Note that this means that the terminator character (newline or _NUL_)
+acts as a _terminator_, not a _separator_. In other words, the final
+line or record is also terminated by the terminator character.
+
+The mode `--stdin-mode=text` replaces each object name with the
+formatted commit, i.e. the format `%s` would transform the object name
+`abcdef012...` to `<subject>` without any termination. Like this:
+
+----
+Did we not fix this in "<subject>"?
+----
+
+Regarding input in this mode: using `-z` or `--null-input` makes sure
+that _NUL_ characters in the input are passed through correctly.
+
+It is safe to interactively read and write from this command since each
+record is immediately flushed.
+
+[[examples]]
EXAMPLES
--------
@@ -110,7 +171,8 @@ DISCUSSION
This command lets you format any number of revisions in any order
through one command invocation. Consider the
-linkgit:git-last-modified[1] case from the "EXAMPLES" section above:
+linkgit:git-last-modified[1] case from the <<examples,EXAMPLES>> section
+above:
1. There might be hundreds of files
2. Commits can be repeated, i.e. two or more files were last modified in
@@ -123,7 +185,7 @@ not a good fit for the above use case.
- The output of linkgit:git-last-modified[1] would have to be processed
in stages since you need to transform the first column separately and
then link the author to the filename. But this is surmountable.
-- You can feed each commit to `git show` or `git show --no-walk -1`. But
+- You can feed each commit to `git show` or `git log --no-walk -1`. But
that means that you need to create a process for each line.
- Let’s say that you want to use one process, not one per line. So you
want to feed all the commits to the command. Now you face the problem
@@ -143,6 +205,11 @@ process per line. It is much more work if you just want to use one
process, but still doable. In contrast, this problem is just another
shell pipeline with this command.
+SEE ALSO
+--------
+linkgit:git-name-rev[1],
+linkgit:git-log[1].
+
GIT
---
Part of the linkgit:git[1] suite
diff --git a/Documentation/meson.build b/Documentation/meson.build
index d6365b888bb..58e7c6a0b8a 100644
--- a/Documentation/meson.build
+++ b/Documentation/meson.build
@@ -55,6 +55,7 @@ manpages = {
'git-for-each-ref.adoc' : 1,
'git-for-each-repo.adoc' : 1,
'git-format-patch.adoc' : 1,
+ 'git-format-rev.adoc' : 1,
'git-fsck-objects.adoc' : 1,
'git-fsck.adoc' : 1,
'git-fsmonitor--daemon.adoc' : 1,
diff --git a/builtin/name-rev.c b/builtin/name-rev.c
index b60cc766279..5494b0424b3 100644
--- a/builtin/name-rev.c
+++ b/builtin/name-rev.c
@@ -21,6 +21,7 @@
#include "pretty.h"
#include "revision.h"
#include "notes.h"
+#include "write-or-die.h"
/*
* One day. See the 'name a rev shortly after epoch' test in t6120 when
@@ -577,7 +578,6 @@ static void name_rev_line(char *p, struct command *cmd)
for (p_start = p; *p; p++) {
#define ishex(x) (isdigit((x)) || ((x) >= 'a' && (x) <= 'f'))
- start:
if (!ishex(*p)) {
counter = 0;
} else if (++counter == hexsz &&
@@ -593,6 +593,7 @@ static void name_rev_line(char *p, struct command *cmd)
*(p + 1) = 0;
oid_ret = repo_get_oid(the_repository, p - (hexsz - 1), &oid);
+ *(p + 1) = c;
switch (cmd->type) {
case NAME_REV:
@@ -600,9 +601,8 @@ static void name_rev_line(char *p, struct command *cmd)
o = lookup_object(the_repository, &oid);
if (o)
name = get_rev_name(o, &buf);
- *(p + 1) = c;
if (!name)
- goto start;
+ continue;
if (cmd->u.name_only)
printf("%.*s%s", p_len - hexsz, p_start, name);
else
@@ -615,7 +615,6 @@ static void name_rev_line(char *p, struct command *cmd)
name = get_format_rev((const struct commit *)o,
cmd->u.pretty_format,
&buf);
- *(p + 1) = c;
if (name)
printf("%.*s%s", p_len - hexsz, p_start, name);
else
@@ -775,6 +774,23 @@ int cmd_name_rev(int argc,
return 0;
}
+struct format_nul_data {
+ bool nul_input;
+ bool nul_output;
+};
+
+static int format_nul_cb(const struct option *option,
+ const char *arg,
+ int unset)
+{
+ struct format_nul_data *data = option->value;
+ data->nul_input = 1;
+ data->nul_output = 1;
+ BUG_ON_OPT_NEG(unset);
+ BUG_ON_OPT_ARG(arg);
+ return 0;
+}
+
static enum stdin_mode parse_stdin_mode(const char *stdin_mode)
{
if (!strcmp(stdin_mode, "text"))
@@ -788,7 +804,9 @@ static enum stdin_mode parse_stdin_mode(const char *stdin_mode)
}
static char const *const format_rev_usage[] = {
- N_("(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> --format=<pretty> [--notes=<ref>]"),
+ N_("(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> "
+ "--format=<pretty> [--[no-]notes=<ref>] "
+ "[-z] [--[no-]null-output] [--[no-]null-input]"),
NULL
};
@@ -800,6 +818,9 @@ int cmd_format_rev(int argc,
const char *format = NULL;
enum stdin_mode stdin_mode;
const char *stdin_mode_arg = NULL;
+ struct format_nul_data nul_data = { 0, 0 };
+ char output_terminator;
+ strbuf_getline_fn getline_fn;
struct display_notes_opt format_notes_opt;
struct rev_info format_rev = REV_INFO_INIT;
struct pretty_format format_pp = { 0 };
@@ -813,6 +834,13 @@ int cmd_format_rev(int argc,
N_("how revs are processed")),
OPT_STRING_LIST(0, "notes", ¬es, N_("notes"),
N_("display notes for pretty format")),
+ OPT_CALLBACK_F('z', "null", &nul_data, N_("z"),
+ N_("Use NUL for input and output termination"),
+ PARSE_OPT_NOARG | PARSE_OPT_NONEG, format_nul_cb),
+ OPT_BOOL(0, "null-input", &nul_data.nul_input,
+ N_("Use NUL for input termination")),
+ OPT_BOOL(0, "null-output", &nul_data.nul_output,
+ N_("Use NUL for output termination")),
OPT_END(),
};
@@ -828,6 +856,9 @@ int cmd_format_rev(int argc,
if (!stdin_mode_arg)
die(_("'%s' is required"), "--stdin-mode");
+ getline_fn = nul_data.nul_input ? strbuf_getline_nul : strbuf_getline_lf;
+ output_terminator = nul_data.nul_output ? '\0' : '\n';
+
init_display_notes(&format_notes_opt);
stdin_mode = parse_stdin_mode(stdin_mode_arg);
@@ -856,20 +887,24 @@ int cmd_format_rev(int argc,
switch (stdin_mode) {
case TEXT:
- while (strbuf_getline(&scratch_buf, stdin) != EOF) {
- strbuf_addch(&scratch_buf, '\n');
+ while (getline_fn(&scratch_buf, stdin) != EOF) {
name_rev_line(scratch_buf.buf, &cmd);
+ /*
+ * We do not pass on the terminator to name_rev_line,
+ * unlike name-rev.
+ */
+ printf("%c", output_terminator);
+ maybe_flush_or_die(stdout, "stdout");
}
break;
case REVS:
- while (strbuf_getline(&scratch_buf, stdin) != EOF) {
+ while (getline_fn(&scratch_buf, stdin) != EOF) {
struct object_id oid;
struct object *object;
struct object *peeled;
- struct commit *commit;
if (repo_get_oid(the_repository, scratch_buf.buf, &oid)) {
- fprintf(stderr, "Could not get sha1 for %s. Skipping.\n",
+ fprintf(stderr, "Could not get object name for %s. Skipping.\n",
scratch_buf.buf);
continue;
}
@@ -882,16 +917,17 @@ int cmd_format_rev(int argc,
}
peeled = deref_tag(the_repository, object, scratch_buf.buf, 0);
- if (peeled && peeled->type == OBJ_COMMIT)
- commit = (struct commit *)peeled;
- if (!commit) {
- fprintf(stderr, "Could not get commit for %s. Skipping.\n",
- *argv);
+ if (!peeled || peeled->type != OBJ_COMMIT) {
+ fprintf(stderr,
+ "Could not get commit for %s. Skipping.\n",
+ scratch_buf.buf);
continue;
}
- get_format_rev(commit, &format_pp, &scratch_buf);
- printf("%s\n", scratch_buf.buf);
+ get_format_rev((struct commit *)peeled,
+ &format_pp, &scratch_buf);
+ printf("%s%c", scratch_buf.buf, output_terminator);
+ maybe_flush_or_die(stdout, "stdout");
strbuf_release(&scratch_buf);
}
break;
diff --git a/t/t6120-describe.sh b/t/t6120-describe.sh
index 725f7d81b6b..8ee3d2c37d0 100755
--- a/t/t6120-describe.sh
+++ b/t/t6120-describe.sh
@@ -801,7 +801,7 @@ test_expect_success 'do not be fooled by invalid describe format ' '
test_must_fail git cat-file -t "refs/tags/super-invalid/./../...../ ~^:/?*[////\\\\\\&}/busted.lock-42-g"$(cat out)
'
-test_expect_success 'name-rev --format setup' '
+test_expect_success 'setup: format-rev' '
mkdir repo-format &&
git -C repo-format init &&
test_commit -C repo-format first &&
@@ -839,8 +839,8 @@ test_expect_success 'format-rev --stdin-mode=text from rev-list same as log' '
'
test_expect_success 'format-rev --stdin-mode=text with running text and tree oid' '
- cmit_oid=$(git -C repo-format rev-parse :/fifth) &&
- reference=$(git -C repo-format log -n1 --format=reference :/fifth) &&
+ cmit_oid=$(git -C repo-format rev-parse fifth) &&
+ reference=$(git -C repo-format log -n1 --format=reference fifth) &&
tree=$(git -C repo-format rev-parse HEAD^{tree}) &&
cat >expect <<-EOF &&
We thought we fixed this in ${reference}.
@@ -899,10 +899,100 @@ test_expect_success 'format-rev --stdin-mode=revs on annotated tag peels to comm
EOF
git -C repo-format format-rev --stdin-mode=revs \
--format=%s \
- --notes=* >actual <<-\EOF &&
+ >actual <<-\EOF &&
version
EOF
test_cmp expect actual
'
+test_expect_success 'format-rev --stdin-mode=revs lookup failures' '
+ test_when_finished "git -C repo-format tag -d tag-to-tree" &&
+ invalid_syntax=not-valid &&
+ non_existing_oid=${EMPTY_BLOB} &&
+ tree=$(git -C repo-format rev-parse eighth^{tree}) &&
+ git -C repo-format tag -a -mmessage tag-to-tree "$tree" &&
+ tag_to_tree=$(git -C repo-format rev-parse tag-to-tree) &&
+ cat >expect <<-EOF &&
+ Could not get object name for ${invalid_syntax}. Skipping.
+ Could not get object for ${non_existing_oid}. Skipping.
+ Could not get commit for ${tree}. Skipping.
+ Could not get commit for ${tag_to_tree}. Skipping.
+ EOF
+ git -C repo-format format-rev --stdin-mode=revs \
+ --format=%s \
+ 2>actual >out <<-EOF &&
+ ${invalid_syntax}
+ ${non_existing_oid}
+ ${tree}
+ ${tag_to_tree}
+ EOF
+ test_line_count = 0 out &&
+ test_cmp expect actual
+'
+
+
+test_expect_success 'format-rev -z --stdin-mode=text with object name lookup failures' '
+ printf "%s\0" "$(git -C repo-format rev-parse HEAD)" >input &&
+ printf "%s\0" "$(git -C repo-format rev-parse HEAD^{tree})" >>input &&
+ printf "%s\0" "$EMPTY_BLOB" >>input &&
+ printf "%s\0" "$(git -C repo-format log --format=%s -1)" >expect &&
+ printf "%s\0" "$(git -C repo-format rev-parse HEAD^{tree})" >>expect &&
+ printf "%s\0" "$EMPTY_BLOB" >>expect &&
+ git -C repo-format format-rev --stdin-mode=text \
+ --format=%s -z <input >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'setup: format-rev input and output separators' '
+ git -C repo-format rev-list HEAD >input-lf &&
+ git -C repo-format rev-list -z HEAD >input-nul &&
+ git -C repo-format log --format=%s >output-lf &&
+ git -C repo-format log -z --format=%s >output-nul &&
+ echo revs >stdin-modes &&
+ echo text >>stdin-modes
+'
+
+while read mode
+do
+ test_expect_success "format-rev -z --stdin-mode=$mode" '
+ cat output-nul >expect &&
+ git -C repo-format format-rev --stdin-mode="$mode" \
+ --format=%s -z <input-nul >actual &&
+ test_cmp expect actual
+ '
+
+ test_expect_success "format-rev -z --no-null-input --no-null-output --stdin-mode=$mode" '
+ cat output-lf >expect &&
+ git -C repo-format format-rev --stdin-mode="$mode" \
+ --format=%s -z --no-null-input --no-null-output \
+ <input-lf >actual &&
+ test_cmp expect actual
+ '
+
+ test_expect_success "format-rev ---null-input --stdin-mode=$mode" '
+ cat output-lf >expect &&
+ git -C repo-format format-rev --stdin-mode="$mode" \
+ --format=%s --null-input \
+ <input-nul >actual &&
+ test_cmp expect actual
+ '
+
+ test_expect_success "format-rev --null-output --stdin-mode=$mode" '
+ cat output-nul >expect &&
+ git -C repo-format format-rev --stdin-mode="$mode" \
+ --format=%s --null-output \
+ <input-lf >actual &&
+ test_cmp expect actual
+ '
+
+ test_expect_success "format-rev -z --stdin-mode=$mode with multi-line output" '
+ format="%s%n%aI" &&
+ git -C repo-format log -z --format="$format" \
+ >expect &&
+ git -C repo-format format-rev --stdin-mode="$mode" \
+ --format="$format" -z <input-nul >actual &&
+ test_cmp expect actual
+ '
+done <stdin-modes
+
test_done
Range-diff against v3:
1: 9cb5cfd1ec3 = 1: 9cb5cfd1ec3 name-rev: wrap both blocks in braces
2: 14900271321 = 2: 14900271321 name-rev: run clang-format before factoring code
3: 1f17f0b0090 ! 3: 724ec022894 name-rev: factor code for sharing with a new command
@@ builtin/name-rev.c: static char const * const name_rev_usage[] = {
struct strbuf buf = STRBUF_INIT;
int counter = 0;
@@ builtin/name-rev.c: static void name_rev_line(char *p, struct name_ref_data *data)
-
- for (p_start = p; *p; p++) {
- #define ishex(x) (isdigit((x)) || ((x) >= 'a' && (x) <= 'f'))
-+ start:
- if (!ishex(*p)) {
- counter = 0;
- } else if (++counter == hexsz &&
-@@ builtin/name-rev.c: static void name_rev_line(char *p, struct name_ref_data *data)
const char *name = NULL;
char c = *(p + 1);
int p_len = p - p_start + 1;
@@ builtin/name-rev.c: static void name_rev_line(char *p, struct name_ref_data *dat
- struct object *o =
- lookup_object(the_repository, &oid);
+ oid_ret = repo_get_oid(the_repository, p - (hexsz - 1), &oid);
++ *(p + 1) = c;
+
+ switch (cmd->type) {
+ case NAME_REV:
@@ builtin/name-rev.c: static void name_rev_line(char *p, struct name_ref_data *dat
+ o = lookup_object(the_repository, &oid);
if (o)
name = get_rev_name(o, &buf);
-+ *(p + 1) = c;
+ if (!name)
-+ goto start;
++ continue;
+ if (cmd->u.name_only)
+ printf("%.*s%s", p_len - hexsz, p_start, name);
+ else
4: 40989b3672a = 4: 382efc3ddb8 name-rev: make dedicated --annotate-stdin --name-only test
5: bb54e8f753e ! 5: 049a45e32bc format-rev: introduce builtin for on-demand pretty formatting
@@ Commit message
`t/t1517-outside-repo.sh` because it uses “EXPERIMENTAL!”
in the usage line.
+ Helped-by: Phillip Wood <phillip.wood@dunelm.org.uk>
+ Helped-by: Ramsay Jones <ramsay@ramsayjones.plus.com>
+ Helped-by: Junio C Hamano <gitster@pobox.com>
Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
+ ## .gitignore ##
+@@
+ /git-for-each-ref
+ /git-for-each-repo
+ /git-format-patch
++/git-format-rev
+ /git-fsck
+ /git-fsck-objects
+ /git-fsmonitor--daemon
+
## Documentation/git-format-rev.adoc (new) ##
@@
+git-format-rev(1)
@@ Documentation/git-format-rev.adoc (new)
+SYNOPSIS
+--------
+[synopsis]
-+(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> --format=<pretty> [--notes=<ref>]
++(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> --format=<pretty> [--[no-]notes=<ref>] [-z] [--[no-]null-output] [--[no-]null-input]
+
+DESCRIPTION
+-----------
@@ Documentation/git-format-rev.adoc (new)
+ How to interpret standard input data:
++
+--
-+`revs`:: Each line is interpreted as a commit. Any kind of revision
++`revs`:: Each line or record (see the <<io,INPUT AND OUTPUT FORMATS>>
++ section) is interpreted as a commit. Any kind of revision
+ expression can be used (see linkgit:gitrevisions[7]). Annotated
+ tags are peeled (see linkgit:gitglossary[7]).
++
@@ Documentation/git-format-rev.adoc (new)
+`text`:: Formats all commit object names found in freeform text. These
+ must the full object names, i.e. abbreviated hexidecimal object
+ names will not be interpreted.
+++
++Anything that is parsed as an object name but that is not found to be a
++commit object name is left alone (echoed).
+--
+
+`--format=<pretty>`::
@@ Documentation/git-format-rev.adoc (new)
+ Custom notes ref. Notes are displayed when using the `%N`
+ atom. See linkgit:git-notes[1].
+
++`-z`::
++`--null`::
++ Use _NUL_ character to terminate both input and output instead
++ of newline. This option cannot be negated.
+++
++This is useful if both the input and output could contain newlines or if
++the input could contain _NUL_ characters; see the <<io,INPUT AND OUTPUT
++FORMATS>> section.
++
++`--null-output`::
++`--no-null-output`::
++ Use _NUL_ character to terminate output instead of newline. The
++ default is `--no-null-output`.
+++
++This is useful if the output could contain newlines, for example if the
++`%n` (newline) atom is used.
++
++`--null-input`::
++`--no-null-input`::
++ Use _NUL_ character to terminate input instead of newline. The
++ default is `--no-null-input`.
+++
++This is useful if the input revision expressions could contain newlines.
++It is also useful if the input could contain _NUL_ characters; see the
++<<io,INPUT AND OUTPUT FORMATS>> section.
++
++[[io]]
++INPUT AND OUTPUT FORMAT
++-----------------------
++
++The command uses newlines for both input and output termination by
++default. See the `-z`, `--null-output`, and `--null-input` options for
++using _NUL_ character as the terminator.
++
++The mode `--stdin-mode=revs` outputs one formatted commit followed by
++the terminator. This could either be called a _line_ or a _record_ in
++case "line" is too suggestive of newline termination.
++
++Note that this means that the terminator character (newline or _NUL_)
++acts as a _terminator_, not a _separator_. In other words, the final
++line or record is also terminated by the terminator character.
++
++The mode `--stdin-mode=text` replaces each object name with the
++formatted commit, i.e. the format `%s` would transform the object name
++`abcdef012...` to `<subject>` without any termination. Like this:
++
++----
++Did we not fix this in "<subject>"?
++----
++
++Regarding input in this mode: using `-z` or `--null-input` makes sure
++that _NUL_ characters in the input are passed through correctly.
++
++It is safe to interactively read and write from this command since each
++record is immediately flushed.
++
++[[examples]]
+EXAMPLES
+--------
+
@@ Documentation/git-format-rev.adoc (new)
+
+This command lets you format any number of revisions in any order
+through one command invocation. Consider the
-+linkgit:git-last-modified[1] case from the "EXAMPLES" section above:
++linkgit:git-last-modified[1] case from the <<examples,EXAMPLES>> section
++above:
+
+1. There might be hundreds of files
+2. Commits can be repeated, i.e. two or more files were last modified in
@@ Documentation/git-format-rev.adoc (new)
+- The output of linkgit:git-last-modified[1] would have to be processed
+ in stages since you need to transform the first column separately and
+ then link the author to the filename. But this is surmountable.
-+- You can feed each commit to `git show` or `git show --no-walk -1`. But
++- You can feed each commit to `git show` or `git log --no-walk -1`. But
+ that means that you need to create a process for each line.
+- Let’s say that you want to use one process, not one per line. So you
+ want to feed all the commits to the command. Now you face the problem
@@ Documentation/git-format-rev.adoc (new)
+process, but still doable. In contrast, this problem is just another
+shell pipeline with this command.
+
++SEE ALSO
++--------
++linkgit:git-name-rev[1],
++linkgit:git-log[1].
++
+GIT
+---
+Part of the linkgit:git[1] suite
+ ## Documentation/meson.build ##
+@@ Documentation/meson.build: manpages = {
+ 'git-for-each-ref.adoc' : 1,
+ 'git-for-each-repo.adoc' : 1,
+ 'git-format-patch.adoc' : 1,
++ 'git-format-rev.adoc' : 1,
+ 'git-fsck-objects.adoc' : 1,
+ 'git-fsck.adoc' : 1,
+ 'git-fsmonitor--daemon.adoc' : 1,
+
## Makefile ##
@@ Makefile: BUILT_INS += $(patsubst builtin/%.o,git-%$X,$(BUILTIN_OBJS))
BUILT_INS += git-cherry$X
@@ builtin/name-rev.c
+#include "pretty.h"
+#include "revision.h"
+#include "notes.h"
++#include "write-or-die.h"
/*
* One day. See the 'name a rev shortly after epoch' test in t6120 when
@@ builtin/name-rev.c: static void name_rev_line(char *p, struct command *cmd)
+ name = get_format_rev((const struct commit *)o,
+ cmd->u.pretty_format,
+ &buf);
-+ *(p + 1) = c;
+ if (name)
+ printf("%.*s%s", p_len - hexsz, p_start, name);
+ else
@@ builtin/name-rev.c: int cmd_name_rev(int argc,
return 0;
}
+
++struct format_nul_data {
++ bool nul_input;
++ bool nul_output;
++};
++
++static int format_nul_cb(const struct option *option,
++ const char *arg,
++ int unset)
++{
++ struct format_nul_data *data = option->value;
++ data->nul_input = 1;
++ data->nul_output = 1;
++ BUG_ON_OPT_NEG(unset);
++ BUG_ON_OPT_ARG(arg);
++ return 0;
++}
++
+static enum stdin_mode parse_stdin_mode(const char *stdin_mode)
+{
+ if (!strcmp(stdin_mode, "text"))
@@ builtin/name-rev.c: int cmd_name_rev(int argc,
+}
+
+static char const *const format_rev_usage[] = {
-+ N_("(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> --format=<pretty> [--notes=<ref>]"),
++ N_("(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> "
++ "--format=<pretty> [--[no-]notes=<ref>] "
++ "[-z] [--[no-]null-output] [--[no-]null-input]"),
+ NULL
+};
+
@@ builtin/name-rev.c: int cmd_name_rev(int argc,
+ const char *format = NULL;
+ enum stdin_mode stdin_mode;
+ const char *stdin_mode_arg = NULL;
++ struct format_nul_data nul_data = { 0, 0 };
++ char output_terminator;
++ strbuf_getline_fn getline_fn;
+ struct display_notes_opt format_notes_opt;
+ struct rev_info format_rev = REV_INFO_INIT;
+ struct pretty_format format_pp = { 0 };
@@ builtin/name-rev.c: int cmd_name_rev(int argc,
+ N_("how revs are processed")),
+ OPT_STRING_LIST(0, "notes", ¬es, N_("notes"),
+ N_("display notes for pretty format")),
++ OPT_CALLBACK_F('z', "null", &nul_data, N_("z"),
++ N_("Use NUL for input and output termination"),
++ PARSE_OPT_NOARG | PARSE_OPT_NONEG, format_nul_cb),
++ OPT_BOOL(0, "null-input", &nul_data.nul_input,
++ N_("Use NUL for input termination")),
++ OPT_BOOL(0, "null-output", &nul_data.nul_output,
++ N_("Use NUL for output termination")),
+ OPT_END(),
+ };
+
@@ builtin/name-rev.c: int cmd_name_rev(int argc,
+ if (!stdin_mode_arg)
+ die(_("'%s' is required"), "--stdin-mode");
+
++ getline_fn = nul_data.nul_input ? strbuf_getline_nul : strbuf_getline_lf;
++ output_terminator = nul_data.nul_output ? '\0' : '\n';
++
+ init_display_notes(&format_notes_opt);
+ stdin_mode = parse_stdin_mode(stdin_mode_arg);
+
@@ builtin/name-rev.c: int cmd_name_rev(int argc,
+
+ switch (stdin_mode) {
+ case TEXT:
-+ while (strbuf_getline(&scratch_buf, stdin) != EOF) {
-+ strbuf_addch(&scratch_buf, '\n');
++ while (getline_fn(&scratch_buf, stdin) != EOF) {
+ name_rev_line(scratch_buf.buf, &cmd);
++ /*
++ * We do not pass on the terminator to name_rev_line,
++ * unlike name-rev.
++ */
++ printf("%c", output_terminator);
++ maybe_flush_or_die(stdout, "stdout");
+ }
+ break;
+ case REVS:
-+ while (strbuf_getline(&scratch_buf, stdin) != EOF) {
++ while (getline_fn(&scratch_buf, stdin) != EOF) {
+ struct object_id oid;
+ struct object *object;
+ struct object *peeled;
-+ struct commit *commit;
+
+ if (repo_get_oid(the_repository, scratch_buf.buf, &oid)) {
-+ fprintf(stderr, "Could not get sha1 for %s. Skipping.\n",
++ fprintf(stderr, "Could not get object name for %s. Skipping.\n",
+ scratch_buf.buf);
+ continue;
+ }
@@ builtin/name-rev.c: int cmd_name_rev(int argc,
+ }
+
+ peeled = deref_tag(the_repository, object, scratch_buf.buf, 0);
-+ if (peeled && peeled->type == OBJ_COMMIT)
-+ commit = (struct commit *)peeled;
-+ if (!commit) {
-+ fprintf(stderr, "Could not get commit for %s. Skipping.\n",
-+ *argv);
++ if (!peeled || peeled->type != OBJ_COMMIT) {
++ fprintf(stderr,
++ "Could not get commit for %s. Skipping.\n",
++ scratch_buf.buf);
+ continue;
+ }
+
-+ get_format_rev(commit, &format_pp, &scratch_buf);
-+ printf("%s\n", scratch_buf.buf);
++ get_format_rev((struct commit *)peeled,
++ &format_pp, &scratch_buf);
++ printf("%s%c", scratch_buf.buf, output_terminator);
++ maybe_flush_or_die(stdout, "stdout");
+ strbuf_release(&scratch_buf);
+ }
+ break;
@@ t/t6120-describe.sh: test_expect_success 'do not be fooled by invalid describe f
test_must_fail git cat-file -t "refs/tags/super-invalid/./../...../ ~^:/?*[////\\\\\\&}/busted.lock-42-g"$(cat out)
'
-+test_expect_success 'name-rev --format setup' '
++test_expect_success 'setup: format-rev' '
+ mkdir repo-format &&
+ git -C repo-format init &&
+ test_commit -C repo-format first &&
@@ t/t6120-describe.sh: test_expect_success 'do not be fooled by invalid describe f
+'
+
+test_expect_success 'format-rev --stdin-mode=text with running text and tree oid' '
-+ cmit_oid=$(git -C repo-format rev-parse :/fifth) &&
-+ reference=$(git -C repo-format log -n1 --format=reference :/fifth) &&
++ cmit_oid=$(git -C repo-format rev-parse fifth) &&
++ reference=$(git -C repo-format log -n1 --format=reference fifth) &&
+ tree=$(git -C repo-format rev-parse HEAD^{tree}) &&
+ cat >expect <<-EOF &&
+ We thought we fixed this in ${reference}.
@@ t/t6120-describe.sh: test_expect_success 'do not be fooled by invalid describe f
+ EOF
+ git -C repo-format format-rev --stdin-mode=revs \
+ --format=%s \
-+ --notes=* >actual <<-\EOF &&
++ >actual <<-\EOF &&
+ version
+ EOF
+ test_cmp expect actual
+'
++
++test_expect_success 'format-rev --stdin-mode=revs lookup failures' '
++ test_when_finished "git -C repo-format tag -d tag-to-tree" &&
++ invalid_syntax=not-valid &&
++ non_existing_oid=${EMPTY_BLOB} &&
++ tree=$(git -C repo-format rev-parse eighth^{tree}) &&
++ git -C repo-format tag -a -mmessage tag-to-tree "$tree" &&
++ tag_to_tree=$(git -C repo-format rev-parse tag-to-tree) &&
++ cat >expect <<-EOF &&
++ Could not get object name for ${invalid_syntax}. Skipping.
++ Could not get object for ${non_existing_oid}. Skipping.
++ Could not get commit for ${tree}. Skipping.
++ Could not get commit for ${tag_to_tree}. Skipping.
++ EOF
++ git -C repo-format format-rev --stdin-mode=revs \
++ --format=%s \
++ 2>actual >out <<-EOF &&
++ ${invalid_syntax}
++ ${non_existing_oid}
++ ${tree}
++ ${tag_to_tree}
++ EOF
++ test_line_count = 0 out &&
++ test_cmp expect actual
++'
++
++
++test_expect_success 'format-rev -z --stdin-mode=text with object name lookup failures' '
++ printf "%s\0" "$(git -C repo-format rev-parse HEAD)" >input &&
++ printf "%s\0" "$(git -C repo-format rev-parse HEAD^{tree})" >>input &&
++ printf "%s\0" "$EMPTY_BLOB" >>input &&
++ printf "%s\0" "$(git -C repo-format log --format=%s -1)" >expect &&
++ printf "%s\0" "$(git -C repo-format rev-parse HEAD^{tree})" >>expect &&
++ printf "%s\0" "$EMPTY_BLOB" >>expect &&
++ git -C repo-format format-rev --stdin-mode=text \
++ --format=%s -z <input >actual &&
++ test_cmp expect actual
++'
++
++test_expect_success 'setup: format-rev input and output separators' '
++ git -C repo-format rev-list HEAD >input-lf &&
++ git -C repo-format rev-list -z HEAD >input-nul &&
++ git -C repo-format log --format=%s >output-lf &&
++ git -C repo-format log -z --format=%s >output-nul &&
++ echo revs >stdin-modes &&
++ echo text >>stdin-modes
++'
++
++while read mode
++do
++ test_expect_success "format-rev -z --stdin-mode=$mode" '
++ cat output-nul >expect &&
++ git -C repo-format format-rev --stdin-mode="$mode" \
++ --format=%s -z <input-nul >actual &&
++ test_cmp expect actual
++ '
++
++ test_expect_success "format-rev -z --no-null-input --no-null-output --stdin-mode=$mode" '
++ cat output-lf >expect &&
++ git -C repo-format format-rev --stdin-mode="$mode" \
++ --format=%s -z --no-null-input --no-null-output \
++ <input-lf >actual &&
++ test_cmp expect actual
++ '
++
++ test_expect_success "format-rev ---null-input --stdin-mode=$mode" '
++ cat output-lf >expect &&
++ git -C repo-format format-rev --stdin-mode="$mode" \
++ --format=%s --null-input \
++ <input-nul >actual &&
++ test_cmp expect actual
++ '
++
++ test_expect_success "format-rev --null-output --stdin-mode=$mode" '
++ cat output-nul >expect &&
++ git -C repo-format format-rev --stdin-mode="$mode" \
++ --format=%s --null-output \
++ <input-lf >actual &&
++ test_cmp expect actual
++ '
++
++ test_expect_success "format-rev -z --stdin-mode=$mode with multi-line output" '
++ format="%s%n%aI" &&
++ git -C repo-format log -z --format="$format" \
++ >expect &&
++ git -C repo-format format-rev --stdin-mode="$mode" \
++ --format="$format" -z <input-nul >actual &&
++ test_cmp expect actual
++ '
++done <stdin-modes
+
test_done
base-commit: 67006b9db8b772423ad0706029286096307d2567
--
2.54.0.13.g9c7419e39f8
^ permalink raw reply related [flat|nested] 45+ messages in thread
* [PATCH v4 1/5] name-rev: wrap both blocks in braces
2026-05-07 19:34 ` [PATCH v4 0/5] " kristofferhaugsbakk
@ 2026-05-07 19:34 ` kristofferhaugsbakk
2026-05-07 19:34 ` [PATCH v4 2/5] name-rev: run clang-format before factoring code kristofferhaugsbakk
` (4 subsequent siblings)
5 siblings, 0 replies; 45+ messages in thread
From: kristofferhaugsbakk @ 2026-05-07 19:34 UTC (permalink / raw)
To: git; +Cc: Kristoffer Haugsbakk, ben.knoble, Phillip Wood, Ramsay Jones
From: Kristoffer Haugsbakk <code@khaugsbakk.name>
See `CodingGuidelines`:
- When there are multiple arms to a conditional and some of them
require braces, enclose even a single line block in braces for
consistency. [...]
Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
---
Notes (series):
v2:
Fix stray formatting of `(p+1)`
builtin/name-rev.c | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/builtin/name-rev.c b/builtin/name-rev.c
index 6188cf98ce0..171e7bd0e98 100644
--- a/builtin/name-rev.c
+++ b/builtin/name-rev.c
@@ -466,9 +466,9 @@ static const char *get_rev_name(const struct object *o, struct strbuf *buf)
if (!n)
return NULL;
- if (!n->generation)
+ if (!n->generation) {
return n->tip_name;
- else {
+ } else {
strbuf_reset(buf);
strbuf_addstr(buf, n->tip_name);
strbuf_strip_suffix(buf, "^0");
@@ -516,9 +516,9 @@ static void name_rev_line(char *p, struct name_ref_data *data)
for (p_start = p; *p; p++) {
#define ishex(x) (isdigit((x)) || ((x) >= 'a' && (x) <= 'f'))
- if (!ishex(*p))
+ if (!ishex(*p)) {
counter = 0;
- else if (++counter == hexsz &&
+ } else if (++counter == hexsz &&
!ishex(*(p+1))) {
struct object_id oid;
const char *name = NULL;
--
2.54.0.13.g9c7419e39f8
^ permalink raw reply related [flat|nested] 45+ messages in thread
* [PATCH v4 2/5] name-rev: run clang-format before factoring code
2026-05-07 19:34 ` [PATCH v4 0/5] " kristofferhaugsbakk
2026-05-07 19:34 ` [PATCH v4 1/5] name-rev: wrap both blocks in braces kristofferhaugsbakk
@ 2026-05-07 19:34 ` kristofferhaugsbakk
2026-05-07 19:34 ` [PATCH v4 3/5] name-rev: factor code for sharing with a new command kristofferhaugsbakk
` (3 subsequent siblings)
5 siblings, 0 replies; 45+ messages in thread
From: kristofferhaugsbakk @ 2026-05-07 19:34 UTC (permalink / raw)
To: git; +Cc: Kristoffer Haugsbakk, ben.knoble, Phillip Wood, Ramsay Jones
From: Kristoffer Haugsbakk <code@khaugsbakk.name>
We are about to move code around to prepare for adding a new
command. Let’s deal with clang-format changes first in the affected
areas.
Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
---
builtin/name-rev.c | 18 +++++++++---------
1 file changed, 9 insertions(+), 9 deletions(-)
diff --git a/builtin/name-rev.c b/builtin/name-rev.c
index 171e7bd0e98..6357eaa76d0 100644
--- a/builtin/name-rev.c
+++ b/builtin/name-rev.c
@@ -519,22 +519,22 @@ static void name_rev_line(char *p, struct name_ref_data *data)
if (!ishex(*p)) {
counter = 0;
} else if (++counter == hexsz &&
- !ishex(*(p+1))) {
+ !ishex(*(p + 1))) {
struct object_id oid;
const char *name = NULL;
- char c = *(p+1);
+ char c = *(p + 1);
int p_len = p - p_start + 1;
counter = 0;
- *(p+1) = 0;
+ *(p + 1) = 0;
if (!repo_get_oid(the_repository, p - (hexsz - 1), &oid)) {
struct object *o =
lookup_object(the_repository, &oid);
if (o)
name = get_rev_name(o, &buf);
}
- *(p+1) = c;
+ *(p + 1) = c;
if (!name)
continue;
@@ -571,9 +571,9 @@ int cmd_name_rev(int argc,
OPT_BOOL(0, "name-only", &data.name_only, N_("print only ref-based names (no object names)")),
OPT_BOOL(0, "tags", &data.tags_only, N_("only use tags to name the commits")),
OPT_STRING_LIST(0, "refs", &data.ref_filters, N_("pattern"),
- N_("only use refs matching <pattern>")),
+ N_("only use refs matching <pattern>")),
OPT_STRING_LIST(0, "exclude", &data.exclude_filters, N_("pattern"),
- N_("ignore refs matching <pattern>")),
+ N_("ignore refs matching <pattern>")),
OPT_GROUP(""),
OPT_BOOL(0, "all", &all, N_("list all commits reachable from all refs")),
#ifndef WITH_BREAKING_CHANGES
@@ -585,10 +585,10 @@ int cmd_name_rev(int argc,
#endif /* WITH_BREAKING_CHANGES */
OPT_BOOL(0, "annotate-stdin", &annotate_stdin, N_("annotate text from stdin")),
OPT_BOOL(0, "undefined", &allow_undefined, N_("allow to print `undefined` names (default)")),
- OPT_BOOL(0, "always", &always,
- N_("show abbreviated commit object as fallback")),
+ OPT_BOOL(0, "always", &always,
+ N_("show abbreviated commit object as fallback")),
OPT_HIDDEN_BOOL(0, "peel-tag", &peel_tag,
- N_("dereference tags in the input (internal use)")),
+ N_("dereference tags in the input (internal use)")),
OPT_END(),
};
--
2.54.0.13.g9c7419e39f8
^ permalink raw reply related [flat|nested] 45+ messages in thread
* [PATCH v4 3/5] name-rev: factor code for sharing with a new command
2026-05-07 19:34 ` [PATCH v4 0/5] " kristofferhaugsbakk
2026-05-07 19:34 ` [PATCH v4 1/5] name-rev: wrap both blocks in braces kristofferhaugsbakk
2026-05-07 19:34 ` [PATCH v4 2/5] name-rev: run clang-format before factoring code kristofferhaugsbakk
@ 2026-05-07 19:34 ` kristofferhaugsbakk
2026-05-07 19:34 ` [PATCH v4 4/5] name-rev: make dedicated --annotate-stdin --name-only test kristofferhaugsbakk
` (2 subsequent siblings)
5 siblings, 0 replies; 45+ messages in thread
From: kristofferhaugsbakk @ 2026-05-07 19:34 UTC (permalink / raw)
To: git; +Cc: Kristoffer Haugsbakk, ben.knoble, Phillip Wood, Ramsay Jones
From: Kristoffer Haugsbakk <code@khaugsbakk.name>
We are about to introduce a new command git-format-rev(1) to this
file. Let’s factor some code so that we can share it with the new
command.
We want to be able to format commits found in freeform text, and
git-name-rev(1) already has a function for that but for symbolic
names. Let’s use a tagged union for the command-specific payload.
No functional changes.
Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
---
Notes (series):
v4:
• Pull out `*(p + 1)` instead of doing it in every `case` (Phillip)
• Go back to using `continue` instead of `goto` (Phillip)
builtin/name-rev.c | 53 ++++++++++++++++++++++++++++++++++------------
1 file changed, 40 insertions(+), 13 deletions(-)
diff --git a/builtin/name-rev.c b/builtin/name-rev.c
index 6357eaa76d0..475efb0b82b 100644
--- a/builtin/name-rev.c
+++ b/builtin/name-rev.c
@@ -272,6 +272,24 @@ struct name_ref_data {
struct string_list exclude_filters;
};
+enum command_type {
+ NAME_REV = 1,
+};
+
+struct command {
+ enum command_type type;
+ union {
+ int name_only;
+ } u;
+};
+
+static void init_name_rev_command(struct command *cmd,
+ int name_only)
+{
+ cmd->type = NAME_REV;
+ cmd->u.name_only = name_only;
+}
+
static struct tip_table {
struct tip_table_entry {
struct object_id oid;
@@ -507,7 +525,7 @@ static char const * const name_rev_usage[] = {
NULL
};
-static void name_rev_line(char *p, struct name_ref_data *data)
+static void name_rev_line(char *p, struct command *cmd)
{
struct strbuf buf = STRBUF_INIT;
int counter = 0;
@@ -524,25 +542,32 @@ static void name_rev_line(char *p, struct name_ref_data *data)
const char *name = NULL;
char c = *(p + 1);
int p_len = p - p_start + 1;
+ struct object *o = NULL;
+ int oid_ret = 1;
counter = 0;
*(p + 1) = 0;
- if (!repo_get_oid(the_repository, p - (hexsz - 1), &oid)) {
- struct object *o =
- lookup_object(the_repository, &oid);
+ oid_ret = repo_get_oid(the_repository, p - (hexsz - 1), &oid);
+ *(p + 1) = c;
+
+ switch (cmd->type) {
+ case NAME_REV:
+ if (!oid_ret)
+ o = lookup_object(the_repository, &oid);
if (o)
name = get_rev_name(o, &buf);
+ if (!name)
+ continue;
+ if (cmd->u.name_only)
+ printf("%.*s%s", p_len - hexsz, p_start, name);
+ else
+ printf("%.*s (%s)", p_len, p_start, name);
+ break;
+ default:
+ BUG("uncovered case: %d", cmd->type);
}
- *(p + 1) = c;
-
- if (!name)
- continue;
- if (data->name_only)
- printf("%.*s%s", p_len - hexsz, p_start, name);
- else
- printf("%.*s (%s)", p_len, p_start, name);
p_start = p + 1;
}
}
@@ -567,6 +592,7 @@ int cmd_name_rev(int argc,
#endif
int all = 0, annotate_stdin = 0, allow_undefined = 1, always = 0, peel_tag = 0;
struct name_ref_data data = { 0, 0, STRING_LIST_INIT_NODUP, STRING_LIST_INIT_NODUP };
+ struct command cmd;
struct option opts[] = {
OPT_BOOL(0, "name-only", &data.name_only, N_("print only ref-based names (no object names)")),
OPT_BOOL(0, "tags", &data.tags_only, N_("only use tags to name the commits")),
@@ -596,6 +622,7 @@ int cmd_name_rev(int argc,
init_commit_rev_name(&rev_names);
repo_config(the_repository, git_default_config, NULL);
argc = parse_options(argc, argv, prefix, opts, name_rev_usage, 0);
+ init_name_rev_command(&cmd, data.name_only);
#ifndef WITH_BREAKING_CHANGES
if (transform_stdin) {
@@ -663,7 +690,7 @@ int cmd_name_rev(int argc,
while (strbuf_getline(&sb, stdin) != EOF) {
strbuf_addch(&sb, '\n');
- name_rev_line(sb.buf, &data);
+ name_rev_line(sb.buf, &cmd);
}
strbuf_release(&sb);
} else if (all) {
--
2.54.0.13.g9c7419e39f8
^ permalink raw reply related [flat|nested] 45+ messages in thread
* [PATCH v4 4/5] name-rev: make dedicated --annotate-stdin --name-only test
2026-05-07 19:34 ` [PATCH v4 0/5] " kristofferhaugsbakk
` (2 preceding siblings ...)
2026-05-07 19:34 ` [PATCH v4 3/5] name-rev: factor code for sharing with a new command kristofferhaugsbakk
@ 2026-05-07 19:34 ` kristofferhaugsbakk
2026-05-07 19:34 ` [PATCH v4 5/5] format-rev: introduce builtin for on-demand pretty formatting kristofferhaugsbakk
2026-05-11 15:45 ` [PATCH v5 0/5] " kristofferhaugsbakk
5 siblings, 0 replies; 45+ messages in thread
From: kristofferhaugsbakk @ 2026-05-07 19:34 UTC (permalink / raw)
To: git; +Cc: Kristoffer Haugsbakk, ben.knoble, Phillip Wood, Ramsay Jones
From: Kristoffer Haugsbakk <code@khaugsbakk.name>
The previous commit split the `--name-only` handling:
1. `--annotate-stdin`: uses the new `struct command`
2. The rest: uses `struct name_ref_data`
But there is no dedicated test for the option combination in (1). That
means that the following tests will fail if you neglect to set
`command.u.name_only` properly:
name-rev --annotate-stdin works with commitGraph
name-rev --annotate-stdin works with non-monotonic timestamps
even though it has nothing to do with what these tests are supposed
to test.
Let’s add another regression test now that it is relevant.
Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
---
t/t6120-describe.sh | 14 ++++++++++++++
1 file changed, 14 insertions(+)
diff --git a/t/t6120-describe.sh b/t/t6120-describe.sh
index 2c70cc561ad..62789f76381 100755
--- a/t/t6120-describe.sh
+++ b/t/t6120-describe.sh
@@ -298,6 +298,20 @@ test_expect_success 'name-rev --annotate-stdin' '
test_cmp expect actual
'
+test_expect_success 'name-rev --annotate-stdin --name-only' '
+ >expect.unsorted &&
+ for rev in $(git rev-list --all)
+ do
+ name=$(git name-rev --name-only $rev) &&
+ echo "$name" >>expect.unsorted || return 1
+ done &&
+ sort <expect.unsorted >expect &&
+ git name-rev --annotate-stdin --name-only \
+ <list >actual.unsorted &&
+ sort <actual.unsorted >actual &&
+ test_cmp expect actual
+'
+
test_expect_success 'name-rev --stdin deprecated' '
git rev-list --all >list &&
if ! test_have_prereq WITH_BREAKING_CHANGES
--
2.54.0.13.g9c7419e39f8
^ permalink raw reply related [flat|nested] 45+ messages in thread
* [PATCH v4 5/5] format-rev: introduce builtin for on-demand pretty formatting
2026-05-07 19:34 ` [PATCH v4 0/5] " kristofferhaugsbakk
` (3 preceding siblings ...)
2026-05-07 19:34 ` [PATCH v4 4/5] name-rev: make dedicated --annotate-stdin --name-only test kristofferhaugsbakk
@ 2026-05-07 19:34 ` kristofferhaugsbakk
2026-05-08 13:25 ` Kristoffer Haugsbakk
2026-05-11 13:25 ` Kristoffer Haugsbakk
2026-05-11 15:45 ` [PATCH v5 0/5] " kristofferhaugsbakk
5 siblings, 2 replies; 45+ messages in thread
From: kristofferhaugsbakk @ 2026-05-07 19:34 UTC (permalink / raw)
To: git
Cc: Kristoffer Haugsbakk, ben.knoble, Phillip Wood, Ramsay Jones,
Junio C Hamano
From: Kristoffer Haugsbakk <code@khaugsbakk.name>
Introduce a new builtin for pretty formatting one revision expression
per line or commit object names found in running text.
Sometimes you want to format commits. Most of the time you’re
walking the graph, e.g. getting a range of commits like
`master..topic`. That’s a job for git-log(1).
But there are times when you want to format commits that you encounter
on demand:
• Full hashes in running text that you might want to pretty-print
• git-last-modified(1) outputs full hashes that you can do the same
with
• git-cherry(1) has `-v` for commit subject, but maybe you want
something else?
But now you can’t use git-log(1), git-show(1), or git-rev-list(1):
• You can’t feed commits piecemeal to these commands, one input
for one output; they block until standard in is closed
• You can’t feed a list of possibly duplicate commits, like the output
of git-last-modified(1); they effectively deduplicate the output
Beyond these two points there’s also the input massage problem: you
cannot feed mixed input (revisions mixed with arbitrary text).
One might hope that git-cat-file(1) can save us. But it doesn’t
support pretty formats.
But there is one command that already both handles revisions as
arguments, revisions on standard input, and even revisions mixed in
with arbitrary text. Namely git-name-rev(1): the command for outputting
symbolic names for commits.
We made some room in `builtin/name-rev.c` two commits ago. Let’s
now add this new git-format-rev(1) command. Taking inspiration from
git-name-rev(1), there are two modes:
• revs: like git-name-rev(1) in argv mode, but one revision per line
on standard in
• text: like git-name-rev(1) with `--annotate-stdin`
***
We need to add this command to the exception list in
`t/t1517-outside-repo.sh` because it uses “EXPERIMENTAL!”
in the usage line.
Helped-by: Phillip Wood <phillip.wood@dunelm.org.uk>
Helped-by: Ramsay Jones <ramsay@ramsayjones.plus.com>
Helped-by: Junio C Hamano <gitster@pobox.com>
Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
---
Notes (series):
v4:
• Squash in “SQUASH???” commits from Junio’s branch:
• Fix -Werror=maybe-uninitialized at builtin/name-rev.c:893:25
• Add this new builtin to Meson build file as well
• Flush every output line/record with `maybe_flush_or_die(...)` (Phillip)
• Introduce `-z` and other NUL output options (Phillip)
• Tests:
• More tests: “name lookup failures” (This would have caught my
uninitialized mistake from v3)
• More tests: for NUL/LF termination
• Fix stale setup command which uses “name-rev” in the name
• Use `setup:` prefix for both (now two) setup tests
• Drop some `:/fifth` expressions in favor just `fifth` (the tag from
`test_commit`)
• Flesh out documentation
• --stdin-mode=revs: Replace “Could not get [sha1][object name]” since the
user could be using SHA256
• This error string was originally stolen from name-rev
• Remember to add this command to `.gitignore` this time
• Correct doc to `git [show][log] --no-walk -1` (next to `git show`)
• Plus some minor things!
v3:
• And don’t forget to document --notes this time
https://lore.kernel.org/git/CALnO6CB5WOTp_e7Kv3CrEbQ+3XE-gDxNVHf7qATBEbyKWfxpLg@mail.gmail.com/
.gitignore | 1 +
Documentation/git-format-rev.adoc | 215 ++++++++++++++++++++++++++++
Documentation/meson.build | 1 +
Makefile | 1 +
builtin.h | 1 +
builtin/name-rev.c | 223 ++++++++++++++++++++++++++++++
command-list.txt | 1 +
git.c | 1 +
t/t1517-outside-repo.sh | 3 +-
t/t6120-describe.sh | 194 ++++++++++++++++++++++++++
10 files changed, 640 insertions(+), 1 deletion(-)
create mode 100644 Documentation/git-format-rev.adoc
diff --git a/.gitignore b/.gitignore
index 24635cf2d6f..e406d3741cd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -71,6 +71,7 @@
/git-for-each-ref
/git-for-each-repo
/git-format-patch
+/git-format-rev
/git-fsck
/git-fsck-objects
/git-fsmonitor--daemon
diff --git a/Documentation/git-format-rev.adoc b/Documentation/git-format-rev.adoc
new file mode 100644
index 00000000000..436980012bc
--- /dev/null
+++ b/Documentation/git-format-rev.adoc
@@ -0,0 +1,215 @@
+git-format-rev(1)
+=================
+
+NAME
+----
+git-format-rev - EXPERIMENTAL: Pretty format revisions on demand
+
+
+SYNOPSIS
+--------
+[synopsis]
+(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> --format=<pretty> [--[no-]notes=<ref>] [-z] [--[no-]null-output] [--[no-]null-input]
+
+DESCRIPTION
+-----------
+
+Pretty format revisions from standard input.
+
+THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
+
+OPTIONS
+-------
+
+`--stdin-mode=<mode>`::
+ How to interpret standard input data:
++
+--
+`revs`:: Each line or record (see the <<io,INPUT AND OUTPUT FORMATS>>
+ section) is interpreted as a commit. Any kind of revision
+ expression can be used (see linkgit:gitrevisions[7]). Annotated
+ tags are peeled (see linkgit:gitglossary[7]).
++
+The argument `rev` is also accepted.
+`text`:: Formats all commit object names found in freeform text. These
+ must the full object names, i.e. abbreviated hexidecimal object
+ names will not be interpreted.
++
+Anything that is parsed as an object name but that is not found to be a
+commit object name is left alone (echoed).
+--
+
+`--format=<pretty>`::
+ Pretty format string.
+
+`--notes=<ref>`::
+`--no-notes`::
+ Custom notes ref. Notes are displayed when using the `%N`
+ atom. See linkgit:git-notes[1].
+
+`-z`::
+`--null`::
+ Use _NUL_ character to terminate both input and output instead
+ of newline. This option cannot be negated.
++
+This is useful if both the input and output could contain newlines or if
+the input could contain _NUL_ characters; see the <<io,INPUT AND OUTPUT
+FORMATS>> section.
+
+`--null-output`::
+`--no-null-output`::
+ Use _NUL_ character to terminate output instead of newline. The
+ default is `--no-null-output`.
++
+This is useful if the output could contain newlines, for example if the
+`%n` (newline) atom is used.
+
+`--null-input`::
+`--no-null-input`::
+ Use _NUL_ character to terminate input instead of newline. The
+ default is `--no-null-input`.
++
+This is useful if the input revision expressions could contain newlines.
+It is also useful if the input could contain _NUL_ characters; see the
+<<io,INPUT AND OUTPUT FORMATS>> section.
+
+[[io]]
+INPUT AND OUTPUT FORMAT
+-----------------------
+
+The command uses newlines for both input and output termination by
+default. See the `-z`, `--null-output`, and `--null-input` options for
+using _NUL_ character as the terminator.
+
+The mode `--stdin-mode=revs` outputs one formatted commit followed by
+the terminator. This could either be called a _line_ or a _record_ in
+case "line" is too suggestive of newline termination.
+
+Note that this means that the terminator character (newline or _NUL_)
+acts as a _terminator_, not a _separator_. In other words, the final
+line or record is also terminated by the terminator character.
+
+The mode `--stdin-mode=text` replaces each object name with the
+formatted commit, i.e. the format `%s` would transform the object name
+`abcdef012...` to `<subject>` without any termination. Like this:
+
+----
+Did we not fix this in "<subject>"?
+----
+
+Regarding input in this mode: using `-z` or `--null-input` makes sure
+that _NUL_ characters in the input are passed through correctly.
+
+It is safe to interactively read and write from this command since each
+record is immediately flushed.
+
+[[examples]]
+EXAMPLES
+--------
+
+The command linkgit:git-last-modified[1] shows the commit that each file
+was last modified in.
+
+----
+$ git last-modified -- README.md Makefile
+7798034171030be0909c56377a4e0e10e6d2df93 Makefile
+c50fbb2dd225e7e82abba4380423ae105089f4d7 README.md
+----
+
+We can pipe the result to this command in order to replace the object
+name with the commit author.
+
+----
+$ git last-modified -- README.md Makefile |
+ git format-rev --stdin-mode=text --format=%an
+Junio C Hamano Makefile
+Todd Zullinger README.md
+----
+
+Another example is _formatting commits in commit messages_. Given this commit message:
+
+----
+Fix off-by-one error
+
+Fix off-by-one error introduced in
+e83c5163316f89bfbde7d9ab23ca2e25604af290.
+
+We thought we fixed this in 5569bf9bbedd63a00780fc5c110e0cfab3aa97b9 but
+that only covered 1/3 of the faulty cases.
+----
+
+We can format the commits and use par(1) to reflow the text, say in a
+`commit-msg` hook:
+
+----
+$ git config set hook.reference-commits.event commit-msg
+$ git config set hook.reference-commits.command reference-commits
+$ cat $(which reference-commits)
+#/bin/sh
+
+msg="$1"
+rewritten=$(mktemp)
+git format-rev --stdin-mode=text --format=reference <"$msg" |
+ par >"$rewritten"
+mv "$rewritten" "$msg"
+----
+
+Which will produce something like this:
+
+----
+Fix off-by-one error
+
+Fix off-by-one error introduced in e83c5163316 (Implement better memory
+allocator, 2005-04-07).
+
+We thought we fixed this in 5569bf9bbed (Fix memory allocator,
+2005-06-22) but that only covered 1/3 of the faulty cases.
+----
+
+DISCUSSION
+----------
+
+This command lets you format any number of revisions in any order
+through one command invocation. Consider the
+linkgit:git-last-modified[1] case from the <<examples,EXAMPLES>> section
+above:
+
+1. There might be hundreds of files
+2. Commits can be repeated, i.e. two or more files were last modified in
+ the same commit
+
+Two widely-used commands which pretty formats commits are
+linkgit:git-log[1] and linkgit:git-show[1]. It turns out that they are
+not a good fit for the above use case.
+
+- The output of linkgit:git-last-modified[1] would have to be processed
+ in stages since you need to transform the first column separately and
+ then link the author to the filename. But this is surmountable.
+- You can feed each commit to `git show` or `git log --no-walk -1`. But
+ that means that you need to create a process for each line.
+- Let’s say that you want to use one process, not one per line. So you
+ want to feed all the commits to the command. Now you face the problem
+ that you have to feed all the commits to the commands before you get
+ any output (this is also the case for the `--stdin` modes). In other
+ words, you cannot loop through each line, get the author for the
+ commit, and output the author and the filename. You need to feed all
+ the commits, get back all the output, and match the output with the
+ filename.
+- But the next problem is that commands will deduplicate the input and
+ only output one commit one single time only. Thus you cannot make the
+ output order match the input order, since a commit could have been
+ repeated in the original input.
+
+In short, it is straightforward to use these two commands if you use one
+process per line. It is much more work if you just want to use one
+process, but still doable. In contrast, this problem is just another
+shell pipeline with this command.
+
+SEE ALSO
+--------
+linkgit:git-name-rev[1],
+linkgit:git-log[1].
+
+GIT
+---
+Part of the linkgit:git[1] suite
diff --git a/Documentation/meson.build b/Documentation/meson.build
index d6365b888bb..58e7c6a0b8a 100644
--- a/Documentation/meson.build
+++ b/Documentation/meson.build
@@ -55,6 +55,7 @@ manpages = {
'git-for-each-ref.adoc' : 1,
'git-for-each-repo.adoc' : 1,
'git-format-patch.adoc' : 1,
+ 'git-format-rev.adoc' : 1,
'git-fsck-objects.adoc' : 1,
'git-fsck.adoc' : 1,
'git-fsmonitor--daemon.adoc' : 1,
diff --git a/Makefile b/Makefile
index 15b1ded1a0b..cbaf91fd846 100644
--- a/Makefile
+++ b/Makefile
@@ -895,6 +895,7 @@ BUILT_INS += $(patsubst builtin/%.o,git-%$X,$(BUILTIN_OBJS))
BUILT_INS += git-cherry$X
BUILT_INS += git-cherry-pick$X
BUILT_INS += git-format-patch$X
+BUILT_INS += git-format-rev$X
BUILT_INS += git-fsck-objects$X
BUILT_INS += git-init$X
BUILT_INS += git-maintenance$X
diff --git a/builtin.h b/builtin.h
index 235c51f30e5..63813c90125 100644
--- a/builtin.h
+++ b/builtin.h
@@ -189,6 +189,7 @@ int cmd_fmt_merge_msg(int argc, const char **argv, const char *prefix, struct re
int cmd_for_each_ref(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_for_each_repo(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_format_patch(int argc, const char **argv, const char *prefix, struct repository *repo);
+int cmd_format_rev(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_fsck(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_fsmonitor__daemon(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_gc(int argc, const char **argv, const char *prefix, struct repository *repo);
diff --git a/builtin/name-rev.c b/builtin/name-rev.c
index 475efb0b82b..5494b0424b3 100644
--- a/builtin/name-rev.c
+++ b/builtin/name-rev.c
@@ -18,6 +18,10 @@
#include "commit-graph.h"
#include "wildmatch.h"
#include "mem-pool.h"
+#include "pretty.h"
+#include "revision.h"
+#include "notes.h"
+#include "write-or-die.h"
/*
* One day. See the 'name a rev shortly after epoch' test in t6120 when
@@ -272,14 +276,26 @@ struct name_ref_data {
struct string_list exclude_filters;
};
+struct pretty_format {
+ struct pretty_print_context ctx;
+ struct userformat_want want;
+};
+
enum command_type {
NAME_REV = 1,
+ FORMAT_REV = 2,
+};
+
+enum stdin_mode {
+ TEXT = 1,
+ REVS = 2,
};
struct command {
enum command_type type;
union {
int name_only;
+ struct pretty_format *pretty_format;
} u;
};
@@ -290,6 +306,13 @@ static void init_name_rev_command(struct command *cmd,
cmd->u.name_only = name_only;
}
+static void init_format_rev_command(struct command *cmd,
+ struct pretty_format *pretty_format)
+{
+ cmd->type = FORMAT_REV;
+ cmd->u.pretty_format = pretty_format;
+}
+
static struct tip_table {
struct tip_table_entry {
struct object_id oid;
@@ -495,6 +518,27 @@ static const char *get_rev_name(const struct object *o, struct strbuf *buf)
}
}
+static const char *get_format_rev(const struct commit *c,
+ struct pretty_format *format_ctx,
+ struct strbuf *buf)
+{
+ strbuf_reset(buf);
+
+ if (format_ctx->want.notes) {
+ struct strbuf notebuf = STRBUF_INIT;
+
+ format_display_notes(&c->object.oid, ¬ebuf,
+ get_log_output_encoding(),
+ format_ctx->ctx.fmt == CMIT_FMT_USERFORMAT);
+ format_ctx->ctx.notes_message = strbuf_detach(¬ebuf, NULL);
+ }
+
+ pretty_print_commit(&format_ctx->ctx, c, buf);
+ FREE_AND_NULL(format_ctx->ctx.notes_message);
+
+ return buf->buf;
+}
+
static void show_name(const struct object *obj,
const char *caller_name,
int always, int allow_undefined, int name_only)
@@ -564,6 +608,18 @@ static void name_rev_line(char *p, struct command *cmd)
else
printf("%.*s (%s)", p_len, p_start, name);
break;
+ case FORMAT_REV:
+ if (!oid_ret)
+ o = parse_object(the_repository, &oid);
+ if (o && o->type == OBJ_COMMIT)
+ name = get_format_rev((const struct commit *)o,
+ cmd->u.pretty_format,
+ &buf);
+ if (name)
+ printf("%.*s%s", p_len - hexsz, p_start, name);
+ else
+ printf("%.*s", p_len, p_start);
+ break;
default:
BUG("uncovered case: %d", cmd->type);
}
@@ -717,3 +773,170 @@ int cmd_name_rev(int argc,
object_array_clear(&revs);
return 0;
}
+
+struct format_nul_data {
+ bool nul_input;
+ bool nul_output;
+};
+
+static int format_nul_cb(const struct option *option,
+ const char *arg,
+ int unset)
+{
+ struct format_nul_data *data = option->value;
+ data->nul_input = 1;
+ data->nul_output = 1;
+ BUG_ON_OPT_NEG(unset);
+ BUG_ON_OPT_ARG(arg);
+ return 0;
+}
+
+static enum stdin_mode parse_stdin_mode(const char *stdin_mode)
+{
+ if (!strcmp(stdin_mode, "text"))
+ return TEXT;
+ else if (!strcmp(stdin_mode, "revs") ||
+ !strcmp(stdin_mode, "rev"))
+ return REVS;
+ else
+ die(_("'%s' needs to be either text, revs, or rev"),
+ "--stdin-mode");
+}
+
+static char const *const format_rev_usage[] = {
+ N_("(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> "
+ "--format=<pretty> [--[no-]notes=<ref>] "
+ "[-z] [--[no-]null-output] [--[no-]null-input]"),
+ NULL
+};
+
+int cmd_format_rev(int argc,
+ const char **argv,
+ const char *prefix,
+ struct repository *repo UNUSED)
+{
+ const char *format = NULL;
+ enum stdin_mode stdin_mode;
+ const char *stdin_mode_arg = NULL;
+ struct format_nul_data nul_data = { 0, 0 };
+ char output_terminator;
+ strbuf_getline_fn getline_fn;
+ struct display_notes_opt format_notes_opt;
+ struct rev_info format_rev = REV_INFO_INIT;
+ struct pretty_format format_pp = { 0 };
+ struct string_list notes = STRING_LIST_INIT_NODUP;
+ struct strbuf scratch_buf = STRBUF_INIT;
+ struct command cmd;
+ struct option opts[] = {
+ OPT_STRING(0, "format", &format, N_("format"),
+ N_("pretty format to use")),
+ OPT_STRING(0, "stdin-mode", &stdin_mode_arg, N_("stdin-mode"),
+ N_("how revs are processed")),
+ OPT_STRING_LIST(0, "notes", ¬es, N_("notes"),
+ N_("display notes for pretty format")),
+ OPT_CALLBACK_F('z', "null", &nul_data, N_("z"),
+ N_("Use NUL for input and output termination"),
+ PARSE_OPT_NOARG | PARSE_OPT_NONEG, format_nul_cb),
+ OPT_BOOL(0, "null-input", &nul_data.nul_input,
+ N_("Use NUL for input termination")),
+ OPT_BOOL(0, "null-output", &nul_data.nul_output,
+ N_("Use NUL for output termination")),
+ OPT_END(),
+ };
+
+ argc = parse_options(argc, argv, prefix, opts, format_rev_usage, 0);
+
+ if (argc > 0) {
+ error(_("too many arguments"));
+ usage_with_options(format_rev_usage, opts);
+ }
+
+ if (!format)
+ die(_("'%s' is required"), "--format");
+ if (!stdin_mode_arg)
+ die(_("'%s' is required"), "--stdin-mode");
+
+ getline_fn = nul_data.nul_input ? strbuf_getline_nul : strbuf_getline_lf;
+ output_terminator = nul_data.nul_output ? '\0' : '\n';
+
+ init_display_notes(&format_notes_opt);
+ stdin_mode = parse_stdin_mode(stdin_mode_arg);
+
+ get_commit_format(format, &format_rev);
+ format_pp.ctx.rev = &format_rev;
+ format_pp.ctx.fmt = format_rev.commit_format;
+ format_pp.ctx.abbrev = format_rev.abbrev;
+ format_pp.ctx.date_mode_explicit = format_rev.date_mode_explicit;
+ format_pp.ctx.date_mode = format_rev.date_mode;
+ format_pp.ctx.color = GIT_COLOR_AUTO;
+
+ userformat_find_requirements(format,
+ &format_pp.want);
+ if (format_pp.want.notes) {
+ int ignore_show_notes = 0;
+ struct string_list_item *n;
+
+ for_each_string_list_item(n, ¬es)
+ enable_ref_display_notes(&format_notes_opt,
+ &ignore_show_notes,
+ n->string);
+ load_display_notes(&format_notes_opt);
+ }
+
+ init_format_rev_command(&cmd, &format_pp);
+
+ switch (stdin_mode) {
+ case TEXT:
+ while (getline_fn(&scratch_buf, stdin) != EOF) {
+ name_rev_line(scratch_buf.buf, &cmd);
+ /*
+ * We do not pass on the terminator to name_rev_line,
+ * unlike name-rev.
+ */
+ printf("%c", output_terminator);
+ maybe_flush_or_die(stdout, "stdout");
+ }
+ break;
+ case REVS:
+ while (getline_fn(&scratch_buf, stdin) != EOF) {
+ struct object_id oid;
+ struct object *object;
+ struct object *peeled;
+
+ if (repo_get_oid(the_repository, scratch_buf.buf, &oid)) {
+ fprintf(stderr, "Could not get object name for %s. Skipping.\n",
+ scratch_buf.buf);
+ continue;
+ }
+
+ object = parse_object(the_repository, &oid);
+ if (!object) {
+ fprintf(stderr, "Could not get object for %s. Skipping.\n",
+ scratch_buf.buf);
+ continue;
+ }
+
+ peeled = deref_tag(the_repository, object, scratch_buf.buf, 0);
+ if (!peeled || peeled->type != OBJ_COMMIT) {
+ fprintf(stderr,
+ "Could not get commit for %s. Skipping.\n",
+ scratch_buf.buf);
+ continue;
+ }
+
+ get_format_rev((struct commit *)peeled,
+ &format_pp, &scratch_buf);
+ printf("%s%c", scratch_buf.buf, output_terminator);
+ maybe_flush_or_die(stdout, "stdout");
+ strbuf_release(&scratch_buf);
+ }
+ break;
+ default:
+ BUG("uncovered case: %d", stdin_mode);
+ }
+
+ strbuf_release(&scratch_buf);
+ string_list_clear(¬es, 0);
+ release_display_notes(&format_notes_opt);
+ return 0;
+}
diff --git a/command-list.txt b/command-list.txt
index f9005cf4597..df729872dca 100644
--- a/command-list.txt
+++ b/command-list.txt
@@ -108,6 +108,7 @@ git-fmt-merge-msg purehelpers
git-for-each-ref plumbinginterrogators
git-for-each-repo plumbinginterrogators
git-format-patch mainporcelain
+git-format-rev plumbinginterrogators
git-fsck ancillaryinterrogators complete
git-gc mainporcelain
git-get-tar-commit-id plumbinginterrogators
diff --git a/git.c b/git.c
index 2b212e6675d..af5b0422b00 100644
--- a/git.c
+++ b/git.c
@@ -578,6 +578,7 @@ static struct cmd_struct commands[] = {
{ "for-each-ref", cmd_for_each_ref, RUN_SETUP },
{ "for-each-repo", cmd_for_each_repo, RUN_SETUP_GENTLY },
{ "format-patch", cmd_format_patch, RUN_SETUP },
+ { "format-rev", cmd_format_rev, RUN_SETUP },
{ "fsck", cmd_fsck, RUN_SETUP },
{ "fsck-objects", cmd_fsck, RUN_SETUP },
{ "fsmonitor--daemon", cmd_fsmonitor__daemon, RUN_SETUP },
diff --git a/t/t1517-outside-repo.sh b/t/t1517-outside-repo.sh
index c824c1a25cf..360a9323343 100755
--- a/t/t1517-outside-repo.sh
+++ b/t/t1517-outside-repo.sh
@@ -114,7 +114,8 @@ do
archimport | citool | credential-netrc | credential-libsecret | \
credential-osxkeychain | cvsexportcommit | cvsimport | cvsserver | \
daemon | \
- difftool--helper | filter-branch | fsck-objects | get-tar-commit-id | \
+ difftool--helper | filter-branch | format-rev | fsck-objects | \
+ get-tar-commit-id | \
gui | gui--askpass | \
http-backend | http-fetch | http-push | init-db | \
merge-octopus | merge-one-file | merge-resolve | mergetool | \
diff --git a/t/t6120-describe.sh b/t/t6120-describe.sh
index 62789f76381..8ee3d2c37d0 100755
--- a/t/t6120-describe.sh
+++ b/t/t6120-describe.sh
@@ -801,4 +801,198 @@ test_expect_success 'do not be fooled by invalid describe format ' '
test_must_fail git cat-file -t "refs/tags/super-invalid/./../...../ ~^:/?*[////\\\\\\&}/busted.lock-42-g"$(cat out)
'
+test_expect_success 'setup: format-rev' '
+ mkdir repo-format &&
+ git -C repo-format init &&
+ test_commit -C repo-format first &&
+ test_commit -C repo-format second &&
+ test_commit -C repo-format third &&
+ test_commit -C repo-format fourth &&
+ test_commit -C repo-format fifth &&
+ test_commit -C repo-format sixth &&
+ test_commit -C repo-format seventh &&
+ test_commit -C repo-format eighth
+'
+
+test_expect_success 'format-rev --stdin-mode=revs' '
+ cat >expect <<-\EOF &&
+ eighth
+ seventh
+ fifth
+ EOF
+ git -C repo-format format-rev --stdin-mode=revs \
+ --format=%s >actual <<-\EOF &&
+ HEAD
+ HEAD~
+ HEAD~3
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success 'format-rev --stdin-mode=text from rev-list same as log' '
+ git -C repo-format log --format=reference >expect &&
+ test_file_not_empty expect &&
+ git -C repo-format rev-list HEAD >list &&
+ git -C repo-format format-rev --stdin-mode=text \
+ --format=reference <list >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'format-rev --stdin-mode=text with running text and tree oid' '
+ cmit_oid=$(git -C repo-format rev-parse fifth) &&
+ reference=$(git -C repo-format log -n1 --format=reference fifth) &&
+ tree=$(git -C repo-format rev-parse HEAD^{tree}) &&
+ cat >expect <<-EOF &&
+ We thought we fixed this in ${reference}.
+ But look at this tree: ${tree}.
+ EOF
+ git -C repo-format format-rev --stdin-mode=text --format=reference \
+ >actual <<-EOF &&
+ We thought we fixed this in ${cmit_oid}.
+ But look at this tree: ${tree}.
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success 'format-rev with %N (note)' '
+ test_when_finished "git -C repo-format notes remove" &&
+ git -C repo-format notes add -m"Make a note" &&
+ printf "Make a note\n\n\n" >expect &&
+ git -C repo-format format-rev --stdin-mode=revs \
+ --format="tformat:%N" \
+ >actual <<-\EOF &&
+ HEAD
+ HEAD~
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success 'format-rev --notes<ref> (custom notes ref)' '
+ # One custom notes ref
+ test_when_finished "git -C repo-format notes remove" &&
+ test_when_finished "git -C repo-format notes --ref=word remove" &&
+ git -C repo-format notes add -m"default" &&
+ git -C repo-format notes --ref=word add -m"custom" &&
+ printf "custom\n\n" >expect &&
+ git -C repo-format format-rev --stdin-mode=revs \
+ --format="tformat:%N" \
+ --notes=word \
+ >actual <<-\EOF &&
+ HEAD
+ EOF
+ test_cmp expect actual &&
+ # Glob all
+ printf "default\ncustom\n\n" >expect &&
+ git -C repo-format format-rev --stdin-mode=revs \
+ --format="tformat:%N" \
+ --notes=* >actual <<-\EOF &&
+ HEAD
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success 'format-rev --stdin-mode=revs on annotated tag peels to commit' '
+ test_when_finished "git -C repo-format tag -d version" &&
+ git -C repo-format tag -a -m"new version" version &&
+ cat >expect <<-\EOF &&
+ eighth
+ EOF
+ git -C repo-format format-rev --stdin-mode=revs \
+ --format=%s \
+ >actual <<-\EOF &&
+ version
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success 'format-rev --stdin-mode=revs lookup failures' '
+ test_when_finished "git -C repo-format tag -d tag-to-tree" &&
+ invalid_syntax=not-valid &&
+ non_existing_oid=${EMPTY_BLOB} &&
+ tree=$(git -C repo-format rev-parse eighth^{tree}) &&
+ git -C repo-format tag -a -mmessage tag-to-tree "$tree" &&
+ tag_to_tree=$(git -C repo-format rev-parse tag-to-tree) &&
+ cat >expect <<-EOF &&
+ Could not get object name for ${invalid_syntax}. Skipping.
+ Could not get object for ${non_existing_oid}. Skipping.
+ Could not get commit for ${tree}. Skipping.
+ Could not get commit for ${tag_to_tree}. Skipping.
+ EOF
+ git -C repo-format format-rev --stdin-mode=revs \
+ --format=%s \
+ 2>actual >out <<-EOF &&
+ ${invalid_syntax}
+ ${non_existing_oid}
+ ${tree}
+ ${tag_to_tree}
+ EOF
+ test_line_count = 0 out &&
+ test_cmp expect actual
+'
+
+
+test_expect_success 'format-rev -z --stdin-mode=text with object name lookup failures' '
+ printf "%s\0" "$(git -C repo-format rev-parse HEAD)" >input &&
+ printf "%s\0" "$(git -C repo-format rev-parse HEAD^{tree})" >>input &&
+ printf "%s\0" "$EMPTY_BLOB" >>input &&
+ printf "%s\0" "$(git -C repo-format log --format=%s -1)" >expect &&
+ printf "%s\0" "$(git -C repo-format rev-parse HEAD^{tree})" >>expect &&
+ printf "%s\0" "$EMPTY_BLOB" >>expect &&
+ git -C repo-format format-rev --stdin-mode=text \
+ --format=%s -z <input >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'setup: format-rev input and output separators' '
+ git -C repo-format rev-list HEAD >input-lf &&
+ git -C repo-format rev-list -z HEAD >input-nul &&
+ git -C repo-format log --format=%s >output-lf &&
+ git -C repo-format log -z --format=%s >output-nul &&
+ echo revs >stdin-modes &&
+ echo text >>stdin-modes
+'
+
+while read mode
+do
+ test_expect_success "format-rev -z --stdin-mode=$mode" '
+ cat output-nul >expect &&
+ git -C repo-format format-rev --stdin-mode="$mode" \
+ --format=%s -z <input-nul >actual &&
+ test_cmp expect actual
+ '
+
+ test_expect_success "format-rev -z --no-null-input --no-null-output --stdin-mode=$mode" '
+ cat output-lf >expect &&
+ git -C repo-format format-rev --stdin-mode="$mode" \
+ --format=%s -z --no-null-input --no-null-output \
+ <input-lf >actual &&
+ test_cmp expect actual
+ '
+
+ test_expect_success "format-rev ---null-input --stdin-mode=$mode" '
+ cat output-lf >expect &&
+ git -C repo-format format-rev --stdin-mode="$mode" \
+ --format=%s --null-input \
+ <input-nul >actual &&
+ test_cmp expect actual
+ '
+
+ test_expect_success "format-rev --null-output --stdin-mode=$mode" '
+ cat output-nul >expect &&
+ git -C repo-format format-rev --stdin-mode="$mode" \
+ --format=%s --null-output \
+ <input-lf >actual &&
+ test_cmp expect actual
+ '
+
+ test_expect_success "format-rev -z --stdin-mode=$mode with multi-line output" '
+ format="%s%n%aI" &&
+ git -C repo-format log -z --format="$format" \
+ >expect &&
+ git -C repo-format format-rev --stdin-mode="$mode" \
+ --format="$format" -z <input-nul >actual &&
+ test_cmp expect actual
+ '
+done <stdin-modes
+
test_done
--
2.54.0.13.g9c7419e39f8
^ permalink raw reply related [flat|nested] 45+ messages in thread
* Re: [PATCH v4 5/5] format-rev: introduce builtin for on-demand pretty formatting
2026-05-07 19:34 ` [PATCH v4 5/5] format-rev: introduce builtin for on-demand pretty formatting kristofferhaugsbakk
@ 2026-05-08 13:25 ` Kristoffer Haugsbakk
2026-05-11 13:25 ` Kristoffer Haugsbakk
1 sibling, 0 replies; 45+ messages in thread
From: Kristoffer Haugsbakk @ 2026-05-08 13:25 UTC (permalink / raw)
To: git; +Cc: D. Ben Knoble, Phillip Wood, Ramsay Jones, Junio C Hamano
On Thu, May 7, 2026, at 21:34, me myself wrote:
> From: Kristoffer Haugsbakk <code@khaugsbakk.name>
>[snip]
> +[[io]]
> +INPUT AND OUTPUT FORMAT
> +-----------------------
> +
> +The command uses newlines for both input and output termination by
> +default. See the `-z`, `--null-output`, and `--null-input` options for
> +using _NUL_ character as the terminator.
> +
> +The mode `--stdin-mode=revs` outputs one formatted commit followed by
> +the terminator. This could either be called a _line_ or a _record_ in
> +case "line" is too suggestive of newline termination.
> +
> +Note that this means that the terminator character (newline or _NUL_)
> +acts as a _terminator_, not a _separator_. In other words, the final
> +line or record is also terminated by the terminator character.
> +
> +The mode `--stdin-mode=text` replaces each object name with the
> +formatted commit, i.e. the format `%s` would transform the object name
> +`abcdef012...` to `<subject>` without any termination. Like this:
> +
> +----
> +Did we not fix this in "<subject>"?
> +----
> +
> +Regarding input in this mode: using `-z` or `--null-input` makes sure
> +that _NUL_ characters in the input are passed through correctly.
This was the paragraph that I was unhappy with (see cover letter).
We’re supposed to be dealing with text here. So on second thought I
don’t think we need to fuzz about passing through NUL characters.
Instead I could mention that `-z` for this mode allows you to further
transform running text from other commands that use NUL
termination. Like `git last-modified -z` (see the Examples section).
> +
> +It is safe to interactively read and write from this command since each
> +record is immediately flushed.
> +
> +[[examples]]
> +EXAMPLES
> +--------
> +
> +The command linkgit:git-last-modified[1] shows the commit that each file
> +was last modified in.
>[snip]
^ permalink raw reply [flat|nested] 45+ messages in thread
* Re: [PATCH v4 5/5] format-rev: introduce builtin for on-demand pretty formatting
2026-05-07 19:34 ` [PATCH v4 5/5] format-rev: introduce builtin for on-demand pretty formatting kristofferhaugsbakk
2026-05-08 13:25 ` Kristoffer Haugsbakk
@ 2026-05-11 13:25 ` Kristoffer Haugsbakk
1 sibling, 0 replies; 45+ messages in thread
From: Kristoffer Haugsbakk @ 2026-05-11 13:25 UTC (permalink / raw)
To: Kristoffer Haugsbakk, git
Cc: D. Ben Knoble, Phillip Wood, Ramsay Jones, Junio C Hamano
On Thu, May 7, 2026, at 21:34, kristofferhaugsbakk@fastmail.com wrote:
> From: Kristoffer Haugsbakk <code@khaugsbakk.name>
>
> Introduce a new builtin for pretty formatting one revision expression
> per line or commit object names found in running text.
>
>[snip]
>
> diff --git a/Documentation/git-format-rev.adoc
>[snip]
> +OPTIONS
> +-------
> +
> +`--stdin-mode=<mode>`::
> + How to interpret standard input data:
> ++
> +--
> +`revs`:: Each line or record (see the <<io,INPUT AND OUTPUT FORMATS>>
> + section) is interpreted as a commit. Any kind of revision
> + expression can be used (see linkgit:gitrevisions[7]). Annotated
> + tags are peeled (see linkgit:gitglossary[7]).
> ++
> +The argument `rev` is also accepted.
> +`text`:: Formats all commit object names found in freeform text. These
> + must the full object names, i.e. abbreviated hexidecimal object
> + names will not be interpreted.
> ++
> +Anything that is parsed as an object name but that is not found to be a
> +commit object name is left alone (echoed).
> +--
This nested definition list is malformed. I’ll need to fix that.
> +
> +`--format=<pretty>`::
> + Pretty format string.
>[snip]
^ permalink raw reply [flat|nested] 45+ messages in thread
* [PATCH v5 0/5] format-rev: introduce builtin for on-demand pretty formatting
2026-05-07 19:34 ` [PATCH v4 0/5] " kristofferhaugsbakk
` (4 preceding siblings ...)
2026-05-07 19:34 ` [PATCH v4 5/5] format-rev: introduce builtin for on-demand pretty formatting kristofferhaugsbakk
@ 2026-05-11 15:45 ` kristofferhaugsbakk
2026-05-11 15:45 ` [PATCH v5 1/5] name-rev: wrap both blocks in braces kristofferhaugsbakk
` (4 more replies)
5 siblings, 5 replies; 45+ messages in thread
From: kristofferhaugsbakk @ 2026-05-11 15:45 UTC (permalink / raw)
To: Junio C Hamano
Cc: Kristoffer Haugsbakk, git, ben.knoble, Phillip Wood, Ramsay Jones
From: Kristoffer Haugsbakk <code@khaugsbakk.name>
(Subject from v2: name-rev: learn --format=<pretty>)
Topic name (applied): kh/name-rev-custom-format
Topic summary: Introduce a new builtin for pretty formatting either (1) one
revision expression per line or (2) commit object names found in running
text.
See the last patch for the motivation. In short there isn’t anything that I
have found that lets you format however many commits you want through one
process (so looping over `git show` is excepted).
The other patches prepare for this change.
§ Changes in v5
Notes from patch 5/5, all doc changes:
• Fix definition list mistake
• No blank line between the two modes
• Also replace `::` delimiters with `;;`. This is not strictly
needed since the definition list is inside an open block. But
it is consistent with all other definition list in definition
lists I’ve seen. (The command option section is a long
definition list.)
• Rewrite part that I was unhappy about regarding
`--stdin-mode=text` and `-z`:
https://lore.kernel.org/git/c04d9cf9-e6a9-4e12-8025-9baededfdafc@app.fastmail.com/
• Replace `abcdef012...` with just “some object name”. The object
name is arbitrary and my AsciiDoc output seems to do some weird
things for `...` inside backticks for HTML.
• Replace “this problem is just another” with “this problem
*is solved* with...”
§ Link to v4
https://lore.kernel.org/git/V4_CV_format-rev.6aa@msgid.xyz/
[1/5] name-rev: wrap both blocks in braces
[2/5] name-rev: run clang-format before factoring code
[3/5] name-rev: factor code for sharing with a new command
[4/5] name-rev: make dedicated --annotate-stdin --name-only test
[5/5] format-rev: introduce builtin for on-demand pretty formatting
.gitignore | 1 +
Documentation/git-format-rev.adoc | 215 +++++++++++++++++++++
Documentation/meson.build | 1 +
Makefile | 1 +
builtin.h | 1 +
builtin/name-rev.c | 300 +++++++++++++++++++++++++++---
command-list.txt | 1 +
git.c | 1 +
t/t1517-outside-repo.sh | 3 +-
t/t6120-describe.sh | 208 +++++++++++++++++++++
10 files changed, 706 insertions(+), 26 deletions(-)
create mode 100644 Documentation/git-format-rev.adoc
Interdiff against v4:
diff --git a/Documentation/git-format-rev.adoc b/Documentation/git-format-rev.adoc
index 436980012bc..c40d52e9f6d 100644
--- a/Documentation/git-format-rev.adoc
+++ b/Documentation/git-format-rev.adoc
@@ -25,13 +25,14 @@ OPTIONS
How to interpret standard input data:
+
--
-`revs`:: Each line or record (see the <<io,INPUT AND OUTPUT FORMATS>>
+`revs`;; Each line or record (see the <<io,INPUT AND OUTPUT FORMATS>>
section) is interpreted as a commit. Any kind of revision
expression can be used (see linkgit:gitrevisions[7]). Annotated
tags are peeled (see linkgit:gitglossary[7]).
+
The argument `rev` is also accepted.
-`text`:: Formats all commit object names found in freeform text. These
+
+`text`;; Formats all commit object names found in freeform text. These
must the full object names, i.e. abbreviated hexidecimal object
names will not be interpreted.
+
@@ -53,8 +54,12 @@ commit object name is left alone (echoed).
of newline. This option cannot be negated.
+
This is useful if both the input and output could contain newlines or if
-the input could contain _NUL_ characters; see the <<io,INPUT AND OUTPUT
-FORMATS>> section.
+the input to this command also uses _NUL_ character termination; see the
+<<io,INPUT AND OUTPUT FORMATS>> section below.
++
+The mode `--stdin-mode=text` can have use for this option when it needs
+to process input like for example `git last-modified -z`; see the
+<<examples,EXAMPLES>> section below.
`--null-output`::
`--no-null-output`::
@@ -70,8 +75,6 @@ This is useful if the output could contain newlines, for example if the
default is `--no-null-input`.
+
This is useful if the input revision expressions could contain newlines.
-It is also useful if the input could contain _NUL_ characters; see the
-<<io,INPUT AND OUTPUT FORMATS>> section.
[[io]]
INPUT AND OUTPUT FORMAT
@@ -90,16 +93,13 @@ acts as a _terminator_, not a _separator_. In other words, the final
line or record is also terminated by the terminator character.
The mode `--stdin-mode=text` replaces each object name with the
-formatted commit, i.e. the format `%s` would transform the object name
-`abcdef012...` to `<subject>` without any termination. Like this:
+formatted commit, i.e. the format `%s` would transform some commit
+object name to `<subject>` without any termination. Like this:
----
Did we not fix this in "<subject>"?
----
-Regarding input in this mode: using `-z` or `--null-input` makes sure
-that _NUL_ characters in the input are passed through correctly.
-
It is safe to interactively read and write from this command since each
record is immediately flushed.
@@ -202,8 +202,8 @@ not a good fit for the above use case.
In short, it is straightforward to use these two commands if you use one
process per line. It is much more work if you just want to use one
-process, but still doable. In contrast, this problem is just another
-shell pipeline with this command.
+process, but still doable. In contrast, this problem is solved with just
+another shell pipeline with this command.
SEE ALSO
--------
Range-diff against v4:
1: 9cb5cfd1ec3 = 1: 9cb5cfd1ec3 name-rev: wrap both blocks in braces
2: 14900271321 = 2: 14900271321 name-rev: run clang-format before factoring code
3: 724ec022894 = 3: 724ec022894 name-rev: factor code for sharing with a new command
4: 382efc3ddb8 = 4: 382efc3ddb8 name-rev: make dedicated --annotate-stdin --name-only test
5: 049a45e32bc ! 5: 425eb16728c format-rev: introduce builtin for on-demand pretty formatting
@@ Documentation/git-format-rev.adoc (new)
+ How to interpret standard input data:
++
+--
-+`revs`:: Each line or record (see the <<io,INPUT AND OUTPUT FORMATS>>
++`revs`;; Each line or record (see the <<io,INPUT AND OUTPUT FORMATS>>
+ section) is interpreted as a commit. Any kind of revision
+ expression can be used (see linkgit:gitrevisions[7]). Annotated
+ tags are peeled (see linkgit:gitglossary[7]).
++
+The argument `rev` is also accepted.
-+`text`:: Formats all commit object names found in freeform text. These
++
++`text`;; Formats all commit object names found in freeform text. These
+ must the full object names, i.e. abbreviated hexidecimal object
+ names will not be interpreted.
++
@@ Documentation/git-format-rev.adoc (new)
+ of newline. This option cannot be negated.
++
+This is useful if both the input and output could contain newlines or if
-+the input could contain _NUL_ characters; see the <<io,INPUT AND OUTPUT
-+FORMATS>> section.
++the input to this command also uses _NUL_ character termination; see the
++<<io,INPUT AND OUTPUT FORMATS>> section below.
+++
++The mode `--stdin-mode=text` can have use for this option when it needs
++to process input like for example `git last-modified -z`; see the
++<<examples,EXAMPLES>> section below.
+
+`--null-output`::
+`--no-null-output`::
@@ Documentation/git-format-rev.adoc (new)
+ default is `--no-null-input`.
++
+This is useful if the input revision expressions could contain newlines.
-+It is also useful if the input could contain _NUL_ characters; see the
-+<<io,INPUT AND OUTPUT FORMATS>> section.
+
+[[io]]
+INPUT AND OUTPUT FORMAT
@@ Documentation/git-format-rev.adoc (new)
+line or record is also terminated by the terminator character.
+
+The mode `--stdin-mode=text` replaces each object name with the
-+formatted commit, i.e. the format `%s` would transform the object name
-+`abcdef012...` to `<subject>` without any termination. Like this:
++formatted commit, i.e. the format `%s` would transform some commit
++object name to `<subject>` without any termination. Like this:
+
+----
+Did we not fix this in "<subject>"?
+----
+
-+Regarding input in this mode: using `-z` or `--null-input` makes sure
-+that _NUL_ characters in the input are passed through correctly.
-+
+It is safe to interactively read and write from this command since each
+record is immediately flushed.
+
@@ Documentation/git-format-rev.adoc (new)
+
+In short, it is straightforward to use these two commands if you use one
+process per line. It is much more work if you just want to use one
-+process, but still doable. In contrast, this problem is just another
-+shell pipeline with this command.
++process, but still doable. In contrast, this problem is solved with just
++another shell pipeline with this command.
+
+SEE ALSO
+--------
base-commit: 67006b9db8b772423ad0706029286096307d2567
--
2.54.0.13.g9c7419e39f8
^ permalink raw reply related [flat|nested] 45+ messages in thread
* [PATCH v5 1/5] name-rev: wrap both blocks in braces
2026-05-11 15:45 ` [PATCH v5 0/5] " kristofferhaugsbakk
@ 2026-05-11 15:45 ` kristofferhaugsbakk
2026-05-11 15:45 ` [PATCH v5 2/5] name-rev: run clang-format before factoring code kristofferhaugsbakk
` (3 subsequent siblings)
4 siblings, 0 replies; 45+ messages in thread
From: kristofferhaugsbakk @ 2026-05-11 15:45 UTC (permalink / raw)
To: Junio C Hamano
Cc: Kristoffer Haugsbakk, git, ben.knoble, Phillip Wood, Ramsay Jones
From: Kristoffer Haugsbakk <code@khaugsbakk.name>
See `CodingGuidelines`:
- When there are multiple arms to a conditional and some of them
require braces, enclose even a single line block in braces for
consistency. [...]
Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
---
Notes (series):
v2:
Fix stray formatting of `(p+1)`
builtin/name-rev.c | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/builtin/name-rev.c b/builtin/name-rev.c
index 6188cf98ce0..171e7bd0e98 100644
--- a/builtin/name-rev.c
+++ b/builtin/name-rev.c
@@ -466,9 +466,9 @@ static const char *get_rev_name(const struct object *o, struct strbuf *buf)
if (!n)
return NULL;
- if (!n->generation)
+ if (!n->generation) {
return n->tip_name;
- else {
+ } else {
strbuf_reset(buf);
strbuf_addstr(buf, n->tip_name);
strbuf_strip_suffix(buf, "^0");
@@ -516,9 +516,9 @@ static void name_rev_line(char *p, struct name_ref_data *data)
for (p_start = p; *p; p++) {
#define ishex(x) (isdigit((x)) || ((x) >= 'a' && (x) <= 'f'))
- if (!ishex(*p))
+ if (!ishex(*p)) {
counter = 0;
- else if (++counter == hexsz &&
+ } else if (++counter == hexsz &&
!ishex(*(p+1))) {
struct object_id oid;
const char *name = NULL;
--
2.54.0.13.g9c7419e39f8
^ permalink raw reply related [flat|nested] 45+ messages in thread
* [PATCH v5 2/5] name-rev: run clang-format before factoring code
2026-05-11 15:45 ` [PATCH v5 0/5] " kristofferhaugsbakk
2026-05-11 15:45 ` [PATCH v5 1/5] name-rev: wrap both blocks in braces kristofferhaugsbakk
@ 2026-05-11 15:45 ` kristofferhaugsbakk
2026-05-11 15:45 ` [PATCH v5 3/5] name-rev: factor code for sharing with a new command kristofferhaugsbakk
` (2 subsequent siblings)
4 siblings, 0 replies; 45+ messages in thread
From: kristofferhaugsbakk @ 2026-05-11 15:45 UTC (permalink / raw)
To: Junio C Hamano
Cc: Kristoffer Haugsbakk, git, ben.knoble, Phillip Wood, Ramsay Jones
From: Kristoffer Haugsbakk <code@khaugsbakk.name>
We are about to move code around to prepare for adding a new
command. Let’s deal with clang-format changes first in the affected
areas.
Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
---
builtin/name-rev.c | 18 +++++++++---------
1 file changed, 9 insertions(+), 9 deletions(-)
diff --git a/builtin/name-rev.c b/builtin/name-rev.c
index 171e7bd0e98..6357eaa76d0 100644
--- a/builtin/name-rev.c
+++ b/builtin/name-rev.c
@@ -519,22 +519,22 @@ static void name_rev_line(char *p, struct name_ref_data *data)
if (!ishex(*p)) {
counter = 0;
} else if (++counter == hexsz &&
- !ishex(*(p+1))) {
+ !ishex(*(p + 1))) {
struct object_id oid;
const char *name = NULL;
- char c = *(p+1);
+ char c = *(p + 1);
int p_len = p - p_start + 1;
counter = 0;
- *(p+1) = 0;
+ *(p + 1) = 0;
if (!repo_get_oid(the_repository, p - (hexsz - 1), &oid)) {
struct object *o =
lookup_object(the_repository, &oid);
if (o)
name = get_rev_name(o, &buf);
}
- *(p+1) = c;
+ *(p + 1) = c;
if (!name)
continue;
@@ -571,9 +571,9 @@ int cmd_name_rev(int argc,
OPT_BOOL(0, "name-only", &data.name_only, N_("print only ref-based names (no object names)")),
OPT_BOOL(0, "tags", &data.tags_only, N_("only use tags to name the commits")),
OPT_STRING_LIST(0, "refs", &data.ref_filters, N_("pattern"),
- N_("only use refs matching <pattern>")),
+ N_("only use refs matching <pattern>")),
OPT_STRING_LIST(0, "exclude", &data.exclude_filters, N_("pattern"),
- N_("ignore refs matching <pattern>")),
+ N_("ignore refs matching <pattern>")),
OPT_GROUP(""),
OPT_BOOL(0, "all", &all, N_("list all commits reachable from all refs")),
#ifndef WITH_BREAKING_CHANGES
@@ -585,10 +585,10 @@ int cmd_name_rev(int argc,
#endif /* WITH_BREAKING_CHANGES */
OPT_BOOL(0, "annotate-stdin", &annotate_stdin, N_("annotate text from stdin")),
OPT_BOOL(0, "undefined", &allow_undefined, N_("allow to print `undefined` names (default)")),
- OPT_BOOL(0, "always", &always,
- N_("show abbreviated commit object as fallback")),
+ OPT_BOOL(0, "always", &always,
+ N_("show abbreviated commit object as fallback")),
OPT_HIDDEN_BOOL(0, "peel-tag", &peel_tag,
- N_("dereference tags in the input (internal use)")),
+ N_("dereference tags in the input (internal use)")),
OPT_END(),
};
--
2.54.0.13.g9c7419e39f8
^ permalink raw reply related [flat|nested] 45+ messages in thread
* [PATCH v5 3/5] name-rev: factor code for sharing with a new command
2026-05-11 15:45 ` [PATCH v5 0/5] " kristofferhaugsbakk
2026-05-11 15:45 ` [PATCH v5 1/5] name-rev: wrap both blocks in braces kristofferhaugsbakk
2026-05-11 15:45 ` [PATCH v5 2/5] name-rev: run clang-format before factoring code kristofferhaugsbakk
@ 2026-05-11 15:45 ` kristofferhaugsbakk
2026-05-11 15:45 ` [PATCH v5 4/5] name-rev: make dedicated --annotate-stdin --name-only test kristofferhaugsbakk
2026-05-11 15:45 ` [PATCH v5 5/5] format-rev: introduce builtin for on-demand pretty formatting kristofferhaugsbakk
4 siblings, 0 replies; 45+ messages in thread
From: kristofferhaugsbakk @ 2026-05-11 15:45 UTC (permalink / raw)
To: Junio C Hamano
Cc: Kristoffer Haugsbakk, git, ben.knoble, Phillip Wood, Ramsay Jones
From: Kristoffer Haugsbakk <code@khaugsbakk.name>
We are about to introduce a new command git-format-rev(1) to this
file. Let’s factor some code so that we can share it with the new
command.
We want to be able to format commits found in freeform text, and
git-name-rev(1) already has a function for that but for symbolic
names. Let’s use a tagged union for the command-specific payload.
No functional changes.
Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
---
Notes (series):
v4:
• Pull out `*(p + 1)` instead of doing it in every `case` (Phillip)
• Go back to using `continue` instead of `goto` (Phillip)
builtin/name-rev.c | 53 ++++++++++++++++++++++++++++++++++------------
1 file changed, 40 insertions(+), 13 deletions(-)
diff --git a/builtin/name-rev.c b/builtin/name-rev.c
index 6357eaa76d0..475efb0b82b 100644
--- a/builtin/name-rev.c
+++ b/builtin/name-rev.c
@@ -272,6 +272,24 @@ struct name_ref_data {
struct string_list exclude_filters;
};
+enum command_type {
+ NAME_REV = 1,
+};
+
+struct command {
+ enum command_type type;
+ union {
+ int name_only;
+ } u;
+};
+
+static void init_name_rev_command(struct command *cmd,
+ int name_only)
+{
+ cmd->type = NAME_REV;
+ cmd->u.name_only = name_only;
+}
+
static struct tip_table {
struct tip_table_entry {
struct object_id oid;
@@ -507,7 +525,7 @@ static char const * const name_rev_usage[] = {
NULL
};
-static void name_rev_line(char *p, struct name_ref_data *data)
+static void name_rev_line(char *p, struct command *cmd)
{
struct strbuf buf = STRBUF_INIT;
int counter = 0;
@@ -524,25 +542,32 @@ static void name_rev_line(char *p, struct name_ref_data *data)
const char *name = NULL;
char c = *(p + 1);
int p_len = p - p_start + 1;
+ struct object *o = NULL;
+ int oid_ret = 1;
counter = 0;
*(p + 1) = 0;
- if (!repo_get_oid(the_repository, p - (hexsz - 1), &oid)) {
- struct object *o =
- lookup_object(the_repository, &oid);
+ oid_ret = repo_get_oid(the_repository, p - (hexsz - 1), &oid);
+ *(p + 1) = c;
+
+ switch (cmd->type) {
+ case NAME_REV:
+ if (!oid_ret)
+ o = lookup_object(the_repository, &oid);
if (o)
name = get_rev_name(o, &buf);
+ if (!name)
+ continue;
+ if (cmd->u.name_only)
+ printf("%.*s%s", p_len - hexsz, p_start, name);
+ else
+ printf("%.*s (%s)", p_len, p_start, name);
+ break;
+ default:
+ BUG("uncovered case: %d", cmd->type);
}
- *(p + 1) = c;
-
- if (!name)
- continue;
- if (data->name_only)
- printf("%.*s%s", p_len - hexsz, p_start, name);
- else
- printf("%.*s (%s)", p_len, p_start, name);
p_start = p + 1;
}
}
@@ -567,6 +592,7 @@ int cmd_name_rev(int argc,
#endif
int all = 0, annotate_stdin = 0, allow_undefined = 1, always = 0, peel_tag = 0;
struct name_ref_data data = { 0, 0, STRING_LIST_INIT_NODUP, STRING_LIST_INIT_NODUP };
+ struct command cmd;
struct option opts[] = {
OPT_BOOL(0, "name-only", &data.name_only, N_("print only ref-based names (no object names)")),
OPT_BOOL(0, "tags", &data.tags_only, N_("only use tags to name the commits")),
@@ -596,6 +622,7 @@ int cmd_name_rev(int argc,
init_commit_rev_name(&rev_names);
repo_config(the_repository, git_default_config, NULL);
argc = parse_options(argc, argv, prefix, opts, name_rev_usage, 0);
+ init_name_rev_command(&cmd, data.name_only);
#ifndef WITH_BREAKING_CHANGES
if (transform_stdin) {
@@ -663,7 +690,7 @@ int cmd_name_rev(int argc,
while (strbuf_getline(&sb, stdin) != EOF) {
strbuf_addch(&sb, '\n');
- name_rev_line(sb.buf, &data);
+ name_rev_line(sb.buf, &cmd);
}
strbuf_release(&sb);
} else if (all) {
--
2.54.0.13.g9c7419e39f8
^ permalink raw reply related [flat|nested] 45+ messages in thread
* [PATCH v5 4/5] name-rev: make dedicated --annotate-stdin --name-only test
2026-05-11 15:45 ` [PATCH v5 0/5] " kristofferhaugsbakk
` (2 preceding siblings ...)
2026-05-11 15:45 ` [PATCH v5 3/5] name-rev: factor code for sharing with a new command kristofferhaugsbakk
@ 2026-05-11 15:45 ` kristofferhaugsbakk
2026-05-11 15:45 ` [PATCH v5 5/5] format-rev: introduce builtin for on-demand pretty formatting kristofferhaugsbakk
4 siblings, 0 replies; 45+ messages in thread
From: kristofferhaugsbakk @ 2026-05-11 15:45 UTC (permalink / raw)
To: Junio C Hamano
Cc: Kristoffer Haugsbakk, git, ben.knoble, Phillip Wood, Ramsay Jones
From: Kristoffer Haugsbakk <code@khaugsbakk.name>
The previous commit split the `--name-only` handling:
1. `--annotate-stdin`: uses the new `struct command`
2. The rest: uses `struct name_ref_data`
But there is no dedicated test for the option combination in (1). That
means that the following tests will fail if you neglect to set
`command.u.name_only` properly:
name-rev --annotate-stdin works with commitGraph
name-rev --annotate-stdin works with non-monotonic timestamps
even though it has nothing to do with what these tests are supposed
to test.
Let’s add another regression test now that it is relevant.
Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
---
t/t6120-describe.sh | 14 ++++++++++++++
1 file changed, 14 insertions(+)
diff --git a/t/t6120-describe.sh b/t/t6120-describe.sh
index 2c70cc561ad..62789f76381 100755
--- a/t/t6120-describe.sh
+++ b/t/t6120-describe.sh
@@ -298,6 +298,20 @@ test_expect_success 'name-rev --annotate-stdin' '
test_cmp expect actual
'
+test_expect_success 'name-rev --annotate-stdin --name-only' '
+ >expect.unsorted &&
+ for rev in $(git rev-list --all)
+ do
+ name=$(git name-rev --name-only $rev) &&
+ echo "$name" >>expect.unsorted || return 1
+ done &&
+ sort <expect.unsorted >expect &&
+ git name-rev --annotate-stdin --name-only \
+ <list >actual.unsorted &&
+ sort <actual.unsorted >actual &&
+ test_cmp expect actual
+'
+
test_expect_success 'name-rev --stdin deprecated' '
git rev-list --all >list &&
if ! test_have_prereq WITH_BREAKING_CHANGES
--
2.54.0.13.g9c7419e39f8
^ permalink raw reply related [flat|nested] 45+ messages in thread
* [PATCH v5 5/5] format-rev: introduce builtin for on-demand pretty formatting
2026-05-11 15:45 ` [PATCH v5 0/5] " kristofferhaugsbakk
` (3 preceding siblings ...)
2026-05-11 15:45 ` [PATCH v5 4/5] name-rev: make dedicated --annotate-stdin --name-only test kristofferhaugsbakk
@ 2026-05-11 15:45 ` kristofferhaugsbakk
4 siblings, 0 replies; 45+ messages in thread
From: kristofferhaugsbakk @ 2026-05-11 15:45 UTC (permalink / raw)
To: Junio C Hamano
Cc: Kristoffer Haugsbakk, git, ben.knoble, Phillip Wood, Ramsay Jones
From: Kristoffer Haugsbakk <code@khaugsbakk.name>
Introduce a new builtin for pretty formatting one revision expression
per line or commit object names found in running text.
Sometimes you want to format commits. Most of the time you’re
walking the graph, e.g. getting a range of commits like
`master..topic`. That’s a job for git-log(1).
But there are times when you want to format commits that you encounter
on demand:
• Full hashes in running text that you might want to pretty-print
• git-last-modified(1) outputs full hashes that you can do the same
with
• git-cherry(1) has `-v` for commit subject, but maybe you want
something else?
But now you can’t use git-log(1), git-show(1), or git-rev-list(1):
• You can’t feed commits piecemeal to these commands, one input
for one output; they block until standard in is closed
• You can’t feed a list of possibly duplicate commits, like the output
of git-last-modified(1); they effectively deduplicate the output
Beyond these two points there’s also the input massage problem: you
cannot feed mixed input (revisions mixed with arbitrary text).
One might hope that git-cat-file(1) can save us. But it doesn’t
support pretty formats.
But there is one command that already both handles revisions as
arguments, revisions on standard input, and even revisions mixed in
with arbitrary text. Namely git-name-rev(1): the command for outputting
symbolic names for commits.
We made some room in `builtin/name-rev.c` two commits ago. Let’s
now add this new git-format-rev(1) command. Taking inspiration from
git-name-rev(1), there are two modes:
• revs: like git-name-rev(1) in argv mode, but one revision per line
on standard in
• text: like git-name-rev(1) with `--annotate-stdin`
***
We need to add this command to the exception list in
`t/t1517-outside-repo.sh` because it uses “EXPERIMENTAL!”
in the usage line.
Helped-by: Phillip Wood <phillip.wood@dunelm.org.uk>
Helped-by: Ramsay Jones <ramsay@ramsayjones.plus.com>
Helped-by: Junio C Hamano <gitster@pobox.com>
Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
---
Notes (series):
v5:
• (All doc changes)
• Fix definition list mistake
• No blank line between the two modes
• Also replace `::` delimiters with `;;`. This is not strictly
needed since the definition list is inside an open block. But
it is consistent with all other definition list in definition
lists I’ve seen. (The command option section is a long
definition list.)
• Rewrite part that I was unhappy about regarding
`--stdin-mode=text` and `-z`:
https://lore.kernel.org/git/c04d9cf9-e6a9-4e12-8025-9baededfdafc@app.fastmail.com/
• Replace `abcdef012...` with just “some object name”. The object
name is arbitrary and my AsciiDoc output seems to do some weird
things for `...` inside backticks for HTML.
• Replace “this problem is just another” with “this problem
*is solved* with...”
***
v4:
• Squash in “SQUASH???” commits from Junio’s branch:
• Fix -Werror=maybe-uninitialized at builtin/name-rev.c:893:25
• Add this new builtin to Meson build file as well
• Flush every output line/record with `maybe_flush_or_die(...)` (Phillip)
• Introduce `-z` and other NUL output options (Phillip)
• Tests:
• More tests: “name lookup failures” (This would have caught my
uninitialized mistake from v3)
• More tests: for NUL/LF termination
• Fix stale setup command which uses “name-rev” in the name
• Use `setup:` prefix for both (now two) setup tests
• Drop some `:/fifth` expressions in favor just `fifth` (the tag from
`test_commit`)
• Flesh out documentation
• --stdin-mode=revs: Replace “Could not get [sha1][object name]” since the
user could be using SHA256
• This error string was originally stolen from name-rev
• Remember to add this command to `.gitignore` this time
• Correct doc to `git [show][log] --no-walk -1` (next to `git show`)
• Plus some minor things!
v3:
• And don’t forget to document --notes this time
https://lore.kernel.org/git/CALnO6CB5WOTp_e7Kv3CrEbQ+3XE-gDxNVHf7qATBEbyKWfxpLg@mail.gmail.com/
.gitignore | 1 +
Documentation/git-format-rev.adoc | 215 ++++++++++++++++++++++++++++
Documentation/meson.build | 1 +
Makefile | 1 +
builtin.h | 1 +
builtin/name-rev.c | 223 ++++++++++++++++++++++++++++++
command-list.txt | 1 +
git.c | 1 +
t/t1517-outside-repo.sh | 3 +-
t/t6120-describe.sh | 194 ++++++++++++++++++++++++++
10 files changed, 640 insertions(+), 1 deletion(-)
create mode 100644 Documentation/git-format-rev.adoc
diff --git a/.gitignore b/.gitignore
index 24635cf2d6f..e406d3741cd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -71,6 +71,7 @@
/git-for-each-ref
/git-for-each-repo
/git-format-patch
+/git-format-rev
/git-fsck
/git-fsck-objects
/git-fsmonitor--daemon
diff --git a/Documentation/git-format-rev.adoc b/Documentation/git-format-rev.adoc
new file mode 100644
index 00000000000..c40d52e9f6d
--- /dev/null
+++ b/Documentation/git-format-rev.adoc
@@ -0,0 +1,215 @@
+git-format-rev(1)
+=================
+
+NAME
+----
+git-format-rev - EXPERIMENTAL: Pretty format revisions on demand
+
+
+SYNOPSIS
+--------
+[synopsis]
+(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> --format=<pretty> [--[no-]notes=<ref>] [-z] [--[no-]null-output] [--[no-]null-input]
+
+DESCRIPTION
+-----------
+
+Pretty format revisions from standard input.
+
+THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
+
+OPTIONS
+-------
+
+`--stdin-mode=<mode>`::
+ How to interpret standard input data:
++
+--
+`revs`;; Each line or record (see the <<io,INPUT AND OUTPUT FORMATS>>
+ section) is interpreted as a commit. Any kind of revision
+ expression can be used (see linkgit:gitrevisions[7]). Annotated
+ tags are peeled (see linkgit:gitglossary[7]).
++
+The argument `rev` is also accepted.
+
+`text`;; Formats all commit object names found in freeform text. These
+ must the full object names, i.e. abbreviated hexidecimal object
+ names will not be interpreted.
++
+Anything that is parsed as an object name but that is not found to be a
+commit object name is left alone (echoed).
+--
+
+`--format=<pretty>`::
+ Pretty format string.
+
+`--notes=<ref>`::
+`--no-notes`::
+ Custom notes ref. Notes are displayed when using the `%N`
+ atom. See linkgit:git-notes[1].
+
+`-z`::
+`--null`::
+ Use _NUL_ character to terminate both input and output instead
+ of newline. This option cannot be negated.
++
+This is useful if both the input and output could contain newlines or if
+the input to this command also uses _NUL_ character termination; see the
+<<io,INPUT AND OUTPUT FORMATS>> section below.
++
+The mode `--stdin-mode=text` can have use for this option when it needs
+to process input like for example `git last-modified -z`; see the
+<<examples,EXAMPLES>> section below.
+
+`--null-output`::
+`--no-null-output`::
+ Use _NUL_ character to terminate output instead of newline. The
+ default is `--no-null-output`.
++
+This is useful if the output could contain newlines, for example if the
+`%n` (newline) atom is used.
+
+`--null-input`::
+`--no-null-input`::
+ Use _NUL_ character to terminate input instead of newline. The
+ default is `--no-null-input`.
++
+This is useful if the input revision expressions could contain newlines.
+
+[[io]]
+INPUT AND OUTPUT FORMAT
+-----------------------
+
+The command uses newlines for both input and output termination by
+default. See the `-z`, `--null-output`, and `--null-input` options for
+using _NUL_ character as the terminator.
+
+The mode `--stdin-mode=revs` outputs one formatted commit followed by
+the terminator. This could either be called a _line_ or a _record_ in
+case "line" is too suggestive of newline termination.
+
+Note that this means that the terminator character (newline or _NUL_)
+acts as a _terminator_, not a _separator_. In other words, the final
+line or record is also terminated by the terminator character.
+
+The mode `--stdin-mode=text` replaces each object name with the
+formatted commit, i.e. the format `%s` would transform some commit
+object name to `<subject>` without any termination. Like this:
+
+----
+Did we not fix this in "<subject>"?
+----
+
+It is safe to interactively read and write from this command since each
+record is immediately flushed.
+
+[[examples]]
+EXAMPLES
+--------
+
+The command linkgit:git-last-modified[1] shows the commit that each file
+was last modified in.
+
+----
+$ git last-modified -- README.md Makefile
+7798034171030be0909c56377a4e0e10e6d2df93 Makefile
+c50fbb2dd225e7e82abba4380423ae105089f4d7 README.md
+----
+
+We can pipe the result to this command in order to replace the object
+name with the commit author.
+
+----
+$ git last-modified -- README.md Makefile |
+ git format-rev --stdin-mode=text --format=%an
+Junio C Hamano Makefile
+Todd Zullinger README.md
+----
+
+Another example is _formatting commits in commit messages_. Given this commit message:
+
+----
+Fix off-by-one error
+
+Fix off-by-one error introduced in
+e83c5163316f89bfbde7d9ab23ca2e25604af290.
+
+We thought we fixed this in 5569bf9bbedd63a00780fc5c110e0cfab3aa97b9 but
+that only covered 1/3 of the faulty cases.
+----
+
+We can format the commits and use par(1) to reflow the text, say in a
+`commit-msg` hook:
+
+----
+$ git config set hook.reference-commits.event commit-msg
+$ git config set hook.reference-commits.command reference-commits
+$ cat $(which reference-commits)
+#/bin/sh
+
+msg="$1"
+rewritten=$(mktemp)
+git format-rev --stdin-mode=text --format=reference <"$msg" |
+ par >"$rewritten"
+mv "$rewritten" "$msg"
+----
+
+Which will produce something like this:
+
+----
+Fix off-by-one error
+
+Fix off-by-one error introduced in e83c5163316 (Implement better memory
+allocator, 2005-04-07).
+
+We thought we fixed this in 5569bf9bbed (Fix memory allocator,
+2005-06-22) but that only covered 1/3 of the faulty cases.
+----
+
+DISCUSSION
+----------
+
+This command lets you format any number of revisions in any order
+through one command invocation. Consider the
+linkgit:git-last-modified[1] case from the <<examples,EXAMPLES>> section
+above:
+
+1. There might be hundreds of files
+2. Commits can be repeated, i.e. two or more files were last modified in
+ the same commit
+
+Two widely-used commands which pretty formats commits are
+linkgit:git-log[1] and linkgit:git-show[1]. It turns out that they are
+not a good fit for the above use case.
+
+- The output of linkgit:git-last-modified[1] would have to be processed
+ in stages since you need to transform the first column separately and
+ then link the author to the filename. But this is surmountable.
+- You can feed each commit to `git show` or `git log --no-walk -1`. But
+ that means that you need to create a process for each line.
+- Let’s say that you want to use one process, not one per line. So you
+ want to feed all the commits to the command. Now you face the problem
+ that you have to feed all the commits to the commands before you get
+ any output (this is also the case for the `--stdin` modes). In other
+ words, you cannot loop through each line, get the author for the
+ commit, and output the author and the filename. You need to feed all
+ the commits, get back all the output, and match the output with the
+ filename.
+- But the next problem is that commands will deduplicate the input and
+ only output one commit one single time only. Thus you cannot make the
+ output order match the input order, since a commit could have been
+ repeated in the original input.
+
+In short, it is straightforward to use these two commands if you use one
+process per line. It is much more work if you just want to use one
+process, but still doable. In contrast, this problem is solved with just
+another shell pipeline with this command.
+
+SEE ALSO
+--------
+linkgit:git-name-rev[1],
+linkgit:git-log[1].
+
+GIT
+---
+Part of the linkgit:git[1] suite
diff --git a/Documentation/meson.build b/Documentation/meson.build
index d6365b888bb..58e7c6a0b8a 100644
--- a/Documentation/meson.build
+++ b/Documentation/meson.build
@@ -55,6 +55,7 @@ manpages = {
'git-for-each-ref.adoc' : 1,
'git-for-each-repo.adoc' : 1,
'git-format-patch.adoc' : 1,
+ 'git-format-rev.adoc' : 1,
'git-fsck-objects.adoc' : 1,
'git-fsck.adoc' : 1,
'git-fsmonitor--daemon.adoc' : 1,
diff --git a/Makefile b/Makefile
index 15b1ded1a0b..cbaf91fd846 100644
--- a/Makefile
+++ b/Makefile
@@ -895,6 +895,7 @@ BUILT_INS += $(patsubst builtin/%.o,git-%$X,$(BUILTIN_OBJS))
BUILT_INS += git-cherry$X
BUILT_INS += git-cherry-pick$X
BUILT_INS += git-format-patch$X
+BUILT_INS += git-format-rev$X
BUILT_INS += git-fsck-objects$X
BUILT_INS += git-init$X
BUILT_INS += git-maintenance$X
diff --git a/builtin.h b/builtin.h
index 235c51f30e5..63813c90125 100644
--- a/builtin.h
+++ b/builtin.h
@@ -189,6 +189,7 @@ int cmd_fmt_merge_msg(int argc, const char **argv, const char *prefix, struct re
int cmd_for_each_ref(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_for_each_repo(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_format_patch(int argc, const char **argv, const char *prefix, struct repository *repo);
+int cmd_format_rev(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_fsck(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_fsmonitor__daemon(int argc, const char **argv, const char *prefix, struct repository *repo);
int cmd_gc(int argc, const char **argv, const char *prefix, struct repository *repo);
diff --git a/builtin/name-rev.c b/builtin/name-rev.c
index 475efb0b82b..5494b0424b3 100644
--- a/builtin/name-rev.c
+++ b/builtin/name-rev.c
@@ -18,6 +18,10 @@
#include "commit-graph.h"
#include "wildmatch.h"
#include "mem-pool.h"
+#include "pretty.h"
+#include "revision.h"
+#include "notes.h"
+#include "write-or-die.h"
/*
* One day. See the 'name a rev shortly after epoch' test in t6120 when
@@ -272,14 +276,26 @@ struct name_ref_data {
struct string_list exclude_filters;
};
+struct pretty_format {
+ struct pretty_print_context ctx;
+ struct userformat_want want;
+};
+
enum command_type {
NAME_REV = 1,
+ FORMAT_REV = 2,
+};
+
+enum stdin_mode {
+ TEXT = 1,
+ REVS = 2,
};
struct command {
enum command_type type;
union {
int name_only;
+ struct pretty_format *pretty_format;
} u;
};
@@ -290,6 +306,13 @@ static void init_name_rev_command(struct command *cmd,
cmd->u.name_only = name_only;
}
+static void init_format_rev_command(struct command *cmd,
+ struct pretty_format *pretty_format)
+{
+ cmd->type = FORMAT_REV;
+ cmd->u.pretty_format = pretty_format;
+}
+
static struct tip_table {
struct tip_table_entry {
struct object_id oid;
@@ -495,6 +518,27 @@ static const char *get_rev_name(const struct object *o, struct strbuf *buf)
}
}
+static const char *get_format_rev(const struct commit *c,
+ struct pretty_format *format_ctx,
+ struct strbuf *buf)
+{
+ strbuf_reset(buf);
+
+ if (format_ctx->want.notes) {
+ struct strbuf notebuf = STRBUF_INIT;
+
+ format_display_notes(&c->object.oid, ¬ebuf,
+ get_log_output_encoding(),
+ format_ctx->ctx.fmt == CMIT_FMT_USERFORMAT);
+ format_ctx->ctx.notes_message = strbuf_detach(¬ebuf, NULL);
+ }
+
+ pretty_print_commit(&format_ctx->ctx, c, buf);
+ FREE_AND_NULL(format_ctx->ctx.notes_message);
+
+ return buf->buf;
+}
+
static void show_name(const struct object *obj,
const char *caller_name,
int always, int allow_undefined, int name_only)
@@ -564,6 +608,18 @@ static void name_rev_line(char *p, struct command *cmd)
else
printf("%.*s (%s)", p_len, p_start, name);
break;
+ case FORMAT_REV:
+ if (!oid_ret)
+ o = parse_object(the_repository, &oid);
+ if (o && o->type == OBJ_COMMIT)
+ name = get_format_rev((const struct commit *)o,
+ cmd->u.pretty_format,
+ &buf);
+ if (name)
+ printf("%.*s%s", p_len - hexsz, p_start, name);
+ else
+ printf("%.*s", p_len, p_start);
+ break;
default:
BUG("uncovered case: %d", cmd->type);
}
@@ -717,3 +773,170 @@ int cmd_name_rev(int argc,
object_array_clear(&revs);
return 0;
}
+
+struct format_nul_data {
+ bool nul_input;
+ bool nul_output;
+};
+
+static int format_nul_cb(const struct option *option,
+ const char *arg,
+ int unset)
+{
+ struct format_nul_data *data = option->value;
+ data->nul_input = 1;
+ data->nul_output = 1;
+ BUG_ON_OPT_NEG(unset);
+ BUG_ON_OPT_ARG(arg);
+ return 0;
+}
+
+static enum stdin_mode parse_stdin_mode(const char *stdin_mode)
+{
+ if (!strcmp(stdin_mode, "text"))
+ return TEXT;
+ else if (!strcmp(stdin_mode, "revs") ||
+ !strcmp(stdin_mode, "rev"))
+ return REVS;
+ else
+ die(_("'%s' needs to be either text, revs, or rev"),
+ "--stdin-mode");
+}
+
+static char const *const format_rev_usage[] = {
+ N_("(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> "
+ "--format=<pretty> [--[no-]notes=<ref>] "
+ "[-z] [--[no-]null-output] [--[no-]null-input]"),
+ NULL
+};
+
+int cmd_format_rev(int argc,
+ const char **argv,
+ const char *prefix,
+ struct repository *repo UNUSED)
+{
+ const char *format = NULL;
+ enum stdin_mode stdin_mode;
+ const char *stdin_mode_arg = NULL;
+ struct format_nul_data nul_data = { 0, 0 };
+ char output_terminator;
+ strbuf_getline_fn getline_fn;
+ struct display_notes_opt format_notes_opt;
+ struct rev_info format_rev = REV_INFO_INIT;
+ struct pretty_format format_pp = { 0 };
+ struct string_list notes = STRING_LIST_INIT_NODUP;
+ struct strbuf scratch_buf = STRBUF_INIT;
+ struct command cmd;
+ struct option opts[] = {
+ OPT_STRING(0, "format", &format, N_("format"),
+ N_("pretty format to use")),
+ OPT_STRING(0, "stdin-mode", &stdin_mode_arg, N_("stdin-mode"),
+ N_("how revs are processed")),
+ OPT_STRING_LIST(0, "notes", ¬es, N_("notes"),
+ N_("display notes for pretty format")),
+ OPT_CALLBACK_F('z', "null", &nul_data, N_("z"),
+ N_("Use NUL for input and output termination"),
+ PARSE_OPT_NOARG | PARSE_OPT_NONEG, format_nul_cb),
+ OPT_BOOL(0, "null-input", &nul_data.nul_input,
+ N_("Use NUL for input termination")),
+ OPT_BOOL(0, "null-output", &nul_data.nul_output,
+ N_("Use NUL for output termination")),
+ OPT_END(),
+ };
+
+ argc = parse_options(argc, argv, prefix, opts, format_rev_usage, 0);
+
+ if (argc > 0) {
+ error(_("too many arguments"));
+ usage_with_options(format_rev_usage, opts);
+ }
+
+ if (!format)
+ die(_("'%s' is required"), "--format");
+ if (!stdin_mode_arg)
+ die(_("'%s' is required"), "--stdin-mode");
+
+ getline_fn = nul_data.nul_input ? strbuf_getline_nul : strbuf_getline_lf;
+ output_terminator = nul_data.nul_output ? '\0' : '\n';
+
+ init_display_notes(&format_notes_opt);
+ stdin_mode = parse_stdin_mode(stdin_mode_arg);
+
+ get_commit_format(format, &format_rev);
+ format_pp.ctx.rev = &format_rev;
+ format_pp.ctx.fmt = format_rev.commit_format;
+ format_pp.ctx.abbrev = format_rev.abbrev;
+ format_pp.ctx.date_mode_explicit = format_rev.date_mode_explicit;
+ format_pp.ctx.date_mode = format_rev.date_mode;
+ format_pp.ctx.color = GIT_COLOR_AUTO;
+
+ userformat_find_requirements(format,
+ &format_pp.want);
+ if (format_pp.want.notes) {
+ int ignore_show_notes = 0;
+ struct string_list_item *n;
+
+ for_each_string_list_item(n, ¬es)
+ enable_ref_display_notes(&format_notes_opt,
+ &ignore_show_notes,
+ n->string);
+ load_display_notes(&format_notes_opt);
+ }
+
+ init_format_rev_command(&cmd, &format_pp);
+
+ switch (stdin_mode) {
+ case TEXT:
+ while (getline_fn(&scratch_buf, stdin) != EOF) {
+ name_rev_line(scratch_buf.buf, &cmd);
+ /*
+ * We do not pass on the terminator to name_rev_line,
+ * unlike name-rev.
+ */
+ printf("%c", output_terminator);
+ maybe_flush_or_die(stdout, "stdout");
+ }
+ break;
+ case REVS:
+ while (getline_fn(&scratch_buf, stdin) != EOF) {
+ struct object_id oid;
+ struct object *object;
+ struct object *peeled;
+
+ if (repo_get_oid(the_repository, scratch_buf.buf, &oid)) {
+ fprintf(stderr, "Could not get object name for %s. Skipping.\n",
+ scratch_buf.buf);
+ continue;
+ }
+
+ object = parse_object(the_repository, &oid);
+ if (!object) {
+ fprintf(stderr, "Could not get object for %s. Skipping.\n",
+ scratch_buf.buf);
+ continue;
+ }
+
+ peeled = deref_tag(the_repository, object, scratch_buf.buf, 0);
+ if (!peeled || peeled->type != OBJ_COMMIT) {
+ fprintf(stderr,
+ "Could not get commit for %s. Skipping.\n",
+ scratch_buf.buf);
+ continue;
+ }
+
+ get_format_rev((struct commit *)peeled,
+ &format_pp, &scratch_buf);
+ printf("%s%c", scratch_buf.buf, output_terminator);
+ maybe_flush_or_die(stdout, "stdout");
+ strbuf_release(&scratch_buf);
+ }
+ break;
+ default:
+ BUG("uncovered case: %d", stdin_mode);
+ }
+
+ strbuf_release(&scratch_buf);
+ string_list_clear(¬es, 0);
+ release_display_notes(&format_notes_opt);
+ return 0;
+}
diff --git a/command-list.txt b/command-list.txt
index f9005cf4597..df729872dca 100644
--- a/command-list.txt
+++ b/command-list.txt
@@ -108,6 +108,7 @@ git-fmt-merge-msg purehelpers
git-for-each-ref plumbinginterrogators
git-for-each-repo plumbinginterrogators
git-format-patch mainporcelain
+git-format-rev plumbinginterrogators
git-fsck ancillaryinterrogators complete
git-gc mainporcelain
git-get-tar-commit-id plumbinginterrogators
diff --git a/git.c b/git.c
index 2b212e6675d..af5b0422b00 100644
--- a/git.c
+++ b/git.c
@@ -578,6 +578,7 @@ static struct cmd_struct commands[] = {
{ "for-each-ref", cmd_for_each_ref, RUN_SETUP },
{ "for-each-repo", cmd_for_each_repo, RUN_SETUP_GENTLY },
{ "format-patch", cmd_format_patch, RUN_SETUP },
+ { "format-rev", cmd_format_rev, RUN_SETUP },
{ "fsck", cmd_fsck, RUN_SETUP },
{ "fsck-objects", cmd_fsck, RUN_SETUP },
{ "fsmonitor--daemon", cmd_fsmonitor__daemon, RUN_SETUP },
diff --git a/t/t1517-outside-repo.sh b/t/t1517-outside-repo.sh
index c824c1a25cf..360a9323343 100755
--- a/t/t1517-outside-repo.sh
+++ b/t/t1517-outside-repo.sh
@@ -114,7 +114,8 @@ do
archimport | citool | credential-netrc | credential-libsecret | \
credential-osxkeychain | cvsexportcommit | cvsimport | cvsserver | \
daemon | \
- difftool--helper | filter-branch | fsck-objects | get-tar-commit-id | \
+ difftool--helper | filter-branch | format-rev | fsck-objects | \
+ get-tar-commit-id | \
gui | gui--askpass | \
http-backend | http-fetch | http-push | init-db | \
merge-octopus | merge-one-file | merge-resolve | mergetool | \
diff --git a/t/t6120-describe.sh b/t/t6120-describe.sh
index 62789f76381..8ee3d2c37d0 100755
--- a/t/t6120-describe.sh
+++ b/t/t6120-describe.sh
@@ -801,4 +801,198 @@ test_expect_success 'do not be fooled by invalid describe format ' '
test_must_fail git cat-file -t "refs/tags/super-invalid/./../...../ ~^:/?*[////\\\\\\&}/busted.lock-42-g"$(cat out)
'
+test_expect_success 'setup: format-rev' '
+ mkdir repo-format &&
+ git -C repo-format init &&
+ test_commit -C repo-format first &&
+ test_commit -C repo-format second &&
+ test_commit -C repo-format third &&
+ test_commit -C repo-format fourth &&
+ test_commit -C repo-format fifth &&
+ test_commit -C repo-format sixth &&
+ test_commit -C repo-format seventh &&
+ test_commit -C repo-format eighth
+'
+
+test_expect_success 'format-rev --stdin-mode=revs' '
+ cat >expect <<-\EOF &&
+ eighth
+ seventh
+ fifth
+ EOF
+ git -C repo-format format-rev --stdin-mode=revs \
+ --format=%s >actual <<-\EOF &&
+ HEAD
+ HEAD~
+ HEAD~3
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success 'format-rev --stdin-mode=text from rev-list same as log' '
+ git -C repo-format log --format=reference >expect &&
+ test_file_not_empty expect &&
+ git -C repo-format rev-list HEAD >list &&
+ git -C repo-format format-rev --stdin-mode=text \
+ --format=reference <list >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'format-rev --stdin-mode=text with running text and tree oid' '
+ cmit_oid=$(git -C repo-format rev-parse fifth) &&
+ reference=$(git -C repo-format log -n1 --format=reference fifth) &&
+ tree=$(git -C repo-format rev-parse HEAD^{tree}) &&
+ cat >expect <<-EOF &&
+ We thought we fixed this in ${reference}.
+ But look at this tree: ${tree}.
+ EOF
+ git -C repo-format format-rev --stdin-mode=text --format=reference \
+ >actual <<-EOF &&
+ We thought we fixed this in ${cmit_oid}.
+ But look at this tree: ${tree}.
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success 'format-rev with %N (note)' '
+ test_when_finished "git -C repo-format notes remove" &&
+ git -C repo-format notes add -m"Make a note" &&
+ printf "Make a note\n\n\n" >expect &&
+ git -C repo-format format-rev --stdin-mode=revs \
+ --format="tformat:%N" \
+ >actual <<-\EOF &&
+ HEAD
+ HEAD~
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success 'format-rev --notes<ref> (custom notes ref)' '
+ # One custom notes ref
+ test_when_finished "git -C repo-format notes remove" &&
+ test_when_finished "git -C repo-format notes --ref=word remove" &&
+ git -C repo-format notes add -m"default" &&
+ git -C repo-format notes --ref=word add -m"custom" &&
+ printf "custom\n\n" >expect &&
+ git -C repo-format format-rev --stdin-mode=revs \
+ --format="tformat:%N" \
+ --notes=word \
+ >actual <<-\EOF &&
+ HEAD
+ EOF
+ test_cmp expect actual &&
+ # Glob all
+ printf "default\ncustom\n\n" >expect &&
+ git -C repo-format format-rev --stdin-mode=revs \
+ --format="tformat:%N" \
+ --notes=* >actual <<-\EOF &&
+ HEAD
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success 'format-rev --stdin-mode=revs on annotated tag peels to commit' '
+ test_when_finished "git -C repo-format tag -d version" &&
+ git -C repo-format tag -a -m"new version" version &&
+ cat >expect <<-\EOF &&
+ eighth
+ EOF
+ git -C repo-format format-rev --stdin-mode=revs \
+ --format=%s \
+ >actual <<-\EOF &&
+ version
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success 'format-rev --stdin-mode=revs lookup failures' '
+ test_when_finished "git -C repo-format tag -d tag-to-tree" &&
+ invalid_syntax=not-valid &&
+ non_existing_oid=${EMPTY_BLOB} &&
+ tree=$(git -C repo-format rev-parse eighth^{tree}) &&
+ git -C repo-format tag -a -mmessage tag-to-tree "$tree" &&
+ tag_to_tree=$(git -C repo-format rev-parse tag-to-tree) &&
+ cat >expect <<-EOF &&
+ Could not get object name for ${invalid_syntax}. Skipping.
+ Could not get object for ${non_existing_oid}. Skipping.
+ Could not get commit for ${tree}. Skipping.
+ Could not get commit for ${tag_to_tree}. Skipping.
+ EOF
+ git -C repo-format format-rev --stdin-mode=revs \
+ --format=%s \
+ 2>actual >out <<-EOF &&
+ ${invalid_syntax}
+ ${non_existing_oid}
+ ${tree}
+ ${tag_to_tree}
+ EOF
+ test_line_count = 0 out &&
+ test_cmp expect actual
+'
+
+
+test_expect_success 'format-rev -z --stdin-mode=text with object name lookup failures' '
+ printf "%s\0" "$(git -C repo-format rev-parse HEAD)" >input &&
+ printf "%s\0" "$(git -C repo-format rev-parse HEAD^{tree})" >>input &&
+ printf "%s\0" "$EMPTY_BLOB" >>input &&
+ printf "%s\0" "$(git -C repo-format log --format=%s -1)" >expect &&
+ printf "%s\0" "$(git -C repo-format rev-parse HEAD^{tree})" >>expect &&
+ printf "%s\0" "$EMPTY_BLOB" >>expect &&
+ git -C repo-format format-rev --stdin-mode=text \
+ --format=%s -z <input >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'setup: format-rev input and output separators' '
+ git -C repo-format rev-list HEAD >input-lf &&
+ git -C repo-format rev-list -z HEAD >input-nul &&
+ git -C repo-format log --format=%s >output-lf &&
+ git -C repo-format log -z --format=%s >output-nul &&
+ echo revs >stdin-modes &&
+ echo text >>stdin-modes
+'
+
+while read mode
+do
+ test_expect_success "format-rev -z --stdin-mode=$mode" '
+ cat output-nul >expect &&
+ git -C repo-format format-rev --stdin-mode="$mode" \
+ --format=%s -z <input-nul >actual &&
+ test_cmp expect actual
+ '
+
+ test_expect_success "format-rev -z --no-null-input --no-null-output --stdin-mode=$mode" '
+ cat output-lf >expect &&
+ git -C repo-format format-rev --stdin-mode="$mode" \
+ --format=%s -z --no-null-input --no-null-output \
+ <input-lf >actual &&
+ test_cmp expect actual
+ '
+
+ test_expect_success "format-rev ---null-input --stdin-mode=$mode" '
+ cat output-lf >expect &&
+ git -C repo-format format-rev --stdin-mode="$mode" \
+ --format=%s --null-input \
+ <input-nul >actual &&
+ test_cmp expect actual
+ '
+
+ test_expect_success "format-rev --null-output --stdin-mode=$mode" '
+ cat output-nul >expect &&
+ git -C repo-format format-rev --stdin-mode="$mode" \
+ --format=%s --null-output \
+ <input-lf >actual &&
+ test_cmp expect actual
+ '
+
+ test_expect_success "format-rev -z --stdin-mode=$mode with multi-line output" '
+ format="%s%n%aI" &&
+ git -C repo-format log -z --format="$format" \
+ >expect &&
+ git -C repo-format format-rev --stdin-mode="$mode" \
+ --format="$format" -z <input-nul >actual &&
+ test_cmp expect actual
+ '
+done <stdin-modes
+
test_done
--
2.54.0.13.g9c7419e39f8
^ permalink raw reply related [flat|nested] 45+ messages in thread
end of thread, other threads:[~2026-05-11 15:47 UTC | newest]
Thread overview: 45+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-03-13 16:03 [PATCH 0/2] name-rev: learn --format=<pretty> kristofferhaugsbakk
2026-03-13 16:03 ` [PATCH 1/2] name-rev: wrap both blocks in braces kristofferhaugsbakk
2026-03-14 0:22 ` Junio C Hamano
2026-03-17 22:10 ` Kristoffer Haugsbakk
2026-03-13 16:03 ` [PATCH 2/2] name-rev: learn --format=<pretty> kristofferhaugsbakk
2026-03-14 0:22 ` Junio C Hamano
2026-03-17 22:07 ` Kristoffer Haugsbakk
2026-03-18 15:36 ` Kristoffer Haugsbakk
2026-03-20 13:09 ` [PATCH v2 0/2] " kristofferhaugsbakk
2026-03-20 13:09 ` [PATCH v2 1/2] name-rev: wrap both blocks in braces kristofferhaugsbakk
2026-03-20 13:09 ` [PATCH v2 2/2] name-rev: learn --format=<pretty> kristofferhaugsbakk
2026-03-20 15:25 ` D. Ben Knoble
2026-03-23 17:34 ` Kristoffer Haugsbakk
2026-04-28 22:25 ` [PATCH v3 0/5] format-rev: introduce builtin for on-demand pretty formatting kristofferhaugsbakk
2026-04-28 22:25 ` [PATCH v3 1/5] name-rev: wrap both blocks in braces kristofferhaugsbakk
2026-04-28 22:25 ` [PATCH v3 2/5] name-rev: run clang-format before factoring code kristofferhaugsbakk
2026-04-28 22:25 ` [PATCH v3 3/5] name-rev: factor code for sharing with a new command kristofferhaugsbakk
2026-04-30 13:54 ` Phillip Wood
2026-05-01 17:24 ` kristofferhaugsbakk
2026-05-02 10:00 ` Phillip Wood
2026-05-05 19:21 ` Kristoffer Haugsbakk
2026-04-28 22:25 ` [PATCH v3 4/5] name-rev: make dedicated --annotate-stdin --name-only test kristofferhaugsbakk
2026-04-28 22:25 ` [PATCH v3 5/5] format-rev: introduce builtin for on-demand pretty formatting kristofferhaugsbakk
2026-04-29 13:41 ` Kristoffer Haugsbakk
2026-04-30 6:23 ` Kristoffer Haugsbakk
2026-04-30 9:21 ` Kristoffer Haugsbakk
2026-05-01 10:16 ` Phillip Wood
2026-05-01 18:27 ` kristofferhaugsbakk
2026-05-02 10:00 ` Phillip Wood
2026-05-05 19:27 ` Kristoffer Haugsbakk
2026-05-03 19:19 ` Junio C Hamano
2026-05-07 19:34 ` [PATCH v4 0/5] " kristofferhaugsbakk
2026-05-07 19:34 ` [PATCH v4 1/5] name-rev: wrap both blocks in braces kristofferhaugsbakk
2026-05-07 19:34 ` [PATCH v4 2/5] name-rev: run clang-format before factoring code kristofferhaugsbakk
2026-05-07 19:34 ` [PATCH v4 3/5] name-rev: factor code for sharing with a new command kristofferhaugsbakk
2026-05-07 19:34 ` [PATCH v4 4/5] name-rev: make dedicated --annotate-stdin --name-only test kristofferhaugsbakk
2026-05-07 19:34 ` [PATCH v4 5/5] format-rev: introduce builtin for on-demand pretty formatting kristofferhaugsbakk
2026-05-08 13:25 ` Kristoffer Haugsbakk
2026-05-11 13:25 ` Kristoffer Haugsbakk
2026-05-11 15:45 ` [PATCH v5 0/5] " kristofferhaugsbakk
2026-05-11 15:45 ` [PATCH v5 1/5] name-rev: wrap both blocks in braces kristofferhaugsbakk
2026-05-11 15:45 ` [PATCH v5 2/5] name-rev: run clang-format before factoring code kristofferhaugsbakk
2026-05-11 15:45 ` [PATCH v5 3/5] name-rev: factor code for sharing with a new command kristofferhaugsbakk
2026-05-11 15:45 ` [PATCH v5 4/5] name-rev: make dedicated --annotate-stdin --name-only test kristofferhaugsbakk
2026-05-11 15:45 ` [PATCH v5 5/5] format-rev: introduce builtin for on-demand pretty formatting kristofferhaugsbakk
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox