From 7c78d24c52d65f4442e521c575ef887481a38439 Mon Sep 17 00:00:00 2001 From: Kristoffer Haugsbakk Date: Mon, 11 May 2026 17:45:45 +0200 Subject: [PATCH 1/5] name-rev: wrap both blocks in braces 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 Signed-off-by: Junio C Hamano --- 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 6188cf98ce..171e7bd0e9 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; From b9c1be43eb30084570781569920a92799afdceaf Mon Sep 17 00:00:00 2001 From: Kristoffer Haugsbakk Date: Mon, 11 May 2026 17:45:46 +0200 Subject: [PATCH 2/5] name-rev: run clang-format before factoring code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Signed-off-by: Junio C Hamano --- 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 171e7bd0e9..6357eaa76d 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 ")), + N_("only use refs matching ")), OPT_STRING_LIST(0, "exclude", &data.exclude_filters, N_("pattern"), - N_("ignore refs matching ")), + N_("ignore refs matching ")), 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(), }; From e2916329dbea62d033091a625340d3d79b686dc6 Mon Sep 17 00:00:00 2001 From: Kristoffer Haugsbakk Date: Mon, 11 May 2026 17:45:47 +0200 Subject: [PATCH 3/5] name-rev: factor code for sharing with a new command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Signed-off-by: Junio C Hamano --- builtin/name-rev.c | 55 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 41 insertions(+), 14 deletions(-) diff --git a/builtin/name-rev.c b/builtin/name-rev.c index 6357eaa76d..475efb0b82 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); - if (o) - name = get_rev_name(o, &buf); - } + oid_ret = repo_get_oid(the_repository, p - (hexsz - 1), &oid); *(p + 1) = c; - if (!name) - continue; + 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); + } - 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) { From ae34adcf035b0b9bed705727e8884082ff4ae7aa Mon Sep 17 00:00:00 2001 From: Kristoffer Haugsbakk Date: Mon, 11 May 2026 17:45:48 +0200 Subject: [PATCH 4/5] name-rev: make dedicated --annotate-stdin --name-only test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Signed-off-by: Junio C Hamano --- t/t6120-describe.sh | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/t/t6120-describe.sh b/t/t6120-describe.sh index 2c70cc561a..62789f7638 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 && + git name-rev --annotate-stdin --name-only \ + actual.unsorted && + sort actual && + test_cmp expect actual +' + test_expect_success 'name-rev --stdin deprecated' ' git rev-list --all >list && if ! test_have_prereq WITH_BREAKING_CHANGES From 19e3106c4510bb50c370241c06e93f050f223d5c Mon Sep 17 00:00:00 2001 From: Kristoffer Haugsbakk Date: Mon, 11 May 2026 17:45:49 +0200 Subject: [PATCH 5/5] format-rev: introduce builtin for on-demand pretty formatting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Helped-by: Ramsay Jones Helped-by: Junio C Hamano Signed-off-by: Kristoffer Haugsbakk Signed-off-by: Junio C Hamano --- .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 78a45cb5be..091600cb88 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 0000000000..c40d52e9f6 --- /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= --format= [--[no-]notes=] [-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=`:: + How to interpret standard input data: ++ +-- +`revs`;; Each line or record (see the <> + 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 format string. + +`--notes=`:: +`--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 +<> 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 +<> 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 `` without any termination. Like this: + +---- +Did we not fix this in ""? +---- + +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 <> 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 f02dbc20cb..3de4333b83 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 8aa489f3b6..f02f008a51 100644 --- a/Makefile +++ b/Makefile @@ -892,6 +892,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 e5e16ecaa6..d640a1c1e6 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 475efb0b82..5494b0424b 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= " + "--format= [--[no-]notes=] " + "[-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 accd3d0c4b..696484b92b 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 c5fad56813..68c6401698 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 c824c1a25c..360a932334 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 62789f7638..8ee3d2c37d 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 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 (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 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 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 \ + 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 \ + 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 \ + 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 actual && + test_cmp expect actual + ' +done