* [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
* 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 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
* [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 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 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
* 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 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 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 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
* [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 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 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 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
* 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
* [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