format-rev: introduce builtin for on-demand pretty formatting

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> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Kristoffer Haugsbakk committed May 11, 2026 at 17:45 UTC 19e3106c4510bb50c370241c06e93f050f223d5c
10 files changed +640 -1
.gitignore
+1
@@ -71,6 +71,7 @@
71 /git-for-each-ref
72 /git-for-each-repo
73 /git-format-patch
74 +/git-format-rev
75 /git-fsck
76 /git-fsck-objects
77 /git-fsmonitor--daemon
Documentation/git-format-rev.adoc new
+215
@@ -0,0 +1,215 @@
1 +git-format-rev(1)
2 +=================
3 +
4 +NAME
5 +----
6 +git-format-rev - EXPERIMENTAL: Pretty format revisions on demand
7 +
8 +
9 +SYNOPSIS
10 +--------
11 +[synopsis]
12 +(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> --format=<pretty> [--[no-]notes=<ref>] [-z] [--[no-]null-output] [--[no-]null-input]
13 +
14 +DESCRIPTION
15 +-----------
16 +
17 +Pretty format revisions from standard input.
18 +
19 +THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
20 +
21 +OPTIONS
22 +-------
23 +
24 +`--stdin-mode=<mode>`::
25 + How to interpret standard input data:
26 ++
27 +--
28 +`revs`;; Each line or record (see the <<io,INPUT AND OUTPUT FORMATS>>
29 + section) is interpreted as a commit. Any kind of revision
30 + expression can be used (see linkgit:gitrevisions[7]). Annotated
31 + tags are peeled (see linkgit:gitglossary[7]).
32 ++
33 +The argument `rev` is also accepted.
34 +
35 +`text`;; Formats all commit object names found in freeform text. These
36 + must the full object names, i.e. abbreviated hexidecimal object
37 + names will not be interpreted.
38 ++
39 +Anything that is parsed as an object name but that is not found to be a
40 +commit object name is left alone (echoed).
41 +--
42 +
43 +`--format=<pretty>`::
44 + Pretty format string.
45 +
46 +`--notes=<ref>`::
47 +`--no-notes`::
48 + Custom notes ref. Notes are displayed when using the `%N`
49 + atom. See linkgit:git-notes[1].
50 +
51 +`-z`::
52 +`--null`::
53 + Use _NUL_ character to terminate both input and output instead
54 + of newline. This option cannot be negated.
55 ++
56 +This is useful if both the input and output could contain newlines or if
57 +the input to this command also uses _NUL_ character termination; see the
58 +<<io,INPUT AND OUTPUT FORMATS>> section below.
59 ++
60 +The mode `--stdin-mode=text` can have use for this option when it needs
61 +to process input like for example `git last-modified -z`; see the
62 +<<examples,EXAMPLES>> section below.
63 +
64 +`--null-output`::
65 +`--no-null-output`::
66 + Use _NUL_ character to terminate output instead of newline. The
67 + default is `--no-null-output`.
68 ++
69 +This is useful if the output could contain newlines, for example if the
70 +`%n` (newline) atom is used.
71 +
72 +`--null-input`::
73 +`--no-null-input`::
74 + Use _NUL_ character to terminate input instead of newline. The
75 + default is `--no-null-input`.
76 ++
77 +This is useful if the input revision expressions could contain newlines.
78 +
79 +[[io]]
80 +INPUT AND OUTPUT FORMAT
81 +-----------------------
82 +
83 +The command uses newlines for both input and output termination by
84 +default. See the `-z`, `--null-output`, and `--null-input` options for
85 +using _NUL_ character as the terminator.
86 +
87 +The mode `--stdin-mode=revs` outputs one formatted commit followed by
88 +the terminator. This could either be called a _line_ or a _record_ in
89 +case "line" is too suggestive of newline termination.
90 +
91 +Note that this means that the terminator character (newline or _NUL_)
92 +acts as a _terminator_, not a _separator_. In other words, the final
93 +line or record is also terminated by the terminator character.
94 +
95 +The mode `--stdin-mode=text` replaces each object name with the
96 +formatted commit, i.e. the format `%s` would transform some commit
97 +object name to `<subject>` without any termination. Like this:
98 +
99 +----
100 +Did we not fix this in "<subject>"?
101 +----
102 +
103 +It is safe to interactively read and write from this command since each
104 +record is immediately flushed.
105 +
106 +[[examples]]
107 +EXAMPLES
108 +--------
109 +
110 +The command linkgit:git-last-modified[1] shows the commit that each file
111 +was last modified in.
112 +
113 +----
114 +$ git last-modified -- README.md Makefile
115 +7798034171030be0909c56377a4e0e10e6d2df93 Makefile
116 +c50fbb2dd225e7e82abba4380423ae105089f4d7 README.md
117 +----
118 +
119 +We can pipe the result to this command in order to replace the object
120 +name with the commit author.
121 +
122 +----
123 +$ git last-modified -- README.md Makefile |
124 + git format-rev --stdin-mode=text --format=%an
125 +Junio C Hamano Makefile
126 +Todd Zullinger README.md
127 +----
128 +
129 +Another example is _formatting commits in commit messages_. Given this commit message:
130 +
131 +----
132 +Fix off-by-one error
133 +
134 +Fix off-by-one error introduced in
135 +e83c5163316f89bfbde7d9ab23ca2e25604af290.
136 +
137 +We thought we fixed this in 5569bf9bbedd63a00780fc5c110e0cfab3aa97b9 but
138 +that only covered 1/3 of the faulty cases.
139 +----
140 +
141 +We can format the commits and use par(1) to reflow the text, say in a
142 +`commit-msg` hook:
143 +
144 +----
145 +$ git config set hook.reference-commits.event commit-msg
146 +$ git config set hook.reference-commits.command reference-commits
147 +$ cat $(which reference-commits)
148 +#/bin/sh
149 +
150 +msg="$1"
151 +rewritten=$(mktemp)
152 +git format-rev --stdin-mode=text --format=reference <"$msg" |
153 + par >"$rewritten"
154 +mv "$rewritten" "$msg"
155 +----
156 +
157 +Which will produce something like this:
158 +
159 +----
160 +Fix off-by-one error
161 +
162 +Fix off-by-one error introduced in e83c5163316 (Implement better memory
163 +allocator, 2005-04-07).
164 +
165 +We thought we fixed this in 5569bf9bbed (Fix memory allocator,
166 +2005-06-22) but that only covered 1/3 of the faulty cases.
167 +----
168 +
169 +DISCUSSION
170 +----------
171 +
172 +This command lets you format any number of revisions in any order
173 +through one command invocation. Consider the
174 +linkgit:git-last-modified[1] case from the <<examples,EXAMPLES>> section
175 +above:
176 +
177 +1. There might be hundreds of files
178 +2. Commits can be repeated, i.e. two or more files were last modified in
179 + the same commit
180 +
181 +Two widely-used commands which pretty formats commits are
182 +linkgit:git-log[1] and linkgit:git-show[1]. It turns out that they are
183 +not a good fit for the above use case.
184 +
185 +- The output of linkgit:git-last-modified[1] would have to be processed
186 + in stages since you need to transform the first column separately and
187 + then link the author to the filename. But this is surmountable.
188 +- You can feed each commit to `git show` or `git log --no-walk -1`. But
189 + that means that you need to create a process for each line.
190 +- Let’s say that you want to use one process, not one per line. So you
191 + want to feed all the commits to the command. Now you face the problem
192 + that you have to feed all the commits to the commands before you get
193 + any output (this is also the case for the `--stdin` modes). In other
194 + words, you cannot loop through each line, get the author for the
195 + commit, and output the author and the filename. You need to feed all
196 + the commits, get back all the output, and match the output with the
197 + filename.
198 +- But the next problem is that commands will deduplicate the input and
199 + only output one commit one single time only. Thus you cannot make the
200 + output order match the input order, since a commit could have been
201 + repeated in the original input.
202 +
203 +In short, it is straightforward to use these two commands if you use one
204 +process per line. It is much more work if you just want to use one
205 +process, but still doable. In contrast, this problem is solved with just
206 +another shell pipeline with this command.
207 +
208 +SEE ALSO
209 +--------
210 +linkgit:git-name-rev[1],
211 +linkgit:git-log[1].
212 +
213 +GIT
214 +---
215 +Part of the linkgit:git[1] suite
Documentation/meson.build
+1
@@ -55,6 +55,7 @@ manpages = {
55 'git-for-each-ref.adoc' : 1,
56 'git-for-each-repo.adoc' : 1,
57 'git-format-patch.adoc' : 1,
58 + 'git-format-rev.adoc' : 1,
59 'git-fsck-objects.adoc' : 1,
60 'git-fsck.adoc' : 1,
61 'git-fsmonitor--daemon.adoc' : 1,
Makefile
+1
@@ -892,6 +892,7 @@ BUILT_INS += $(patsubst builtin/%.o,git-%$X,$(BUILTIN_OBJS))
892 BUILT_INS += git-cherry$X
893 BUILT_INS += git-cherry-pick$X
894 BUILT_INS += git-format-patch$X
895 +BUILT_INS += git-format-rev$X
896 BUILT_INS += git-fsck-objects$X
897 BUILT_INS += git-init$X
898 BUILT_INS += git-maintenance$X
builtin.h
+1
@@ -189,6 +189,7 @@ int cmd_fmt_merge_msg(int argc, const char **argv, const char *prefix, struct re
189 int cmd_for_each_ref(int argc, const char **argv, const char *prefix, struct repository *repo);
190 int cmd_for_each_repo(int argc, const char **argv, const char *prefix, struct repository *repo);
191 int cmd_format_patch(int argc, const char **argv, const char *prefix, struct repository *repo);
192 +int cmd_format_rev(int argc, const char **argv, const char *prefix, struct repository *repo);
193 int cmd_fsck(int argc, const char **argv, const char *prefix, struct repository *repo);
194 int cmd_fsmonitor__daemon(int argc, const char **argv, const char *prefix, struct repository *repo);
195 int cmd_gc(int argc, const char **argv, const char *prefix, struct repository *repo);
builtin/name-rev.c
+223
@@ -18,6 +18,10 @@
18 #include "commit-graph.h"
19 #include "wildmatch.h"
20 #include "mem-pool.h"
21 +#include "pretty.h"
22 +#include "revision.h"
23 +#include "notes.h"
24 +#include "write-or-die.h"
25
26 /*
27 * One day. See the 'name a rev shortly after epoch' test in t6120 when
@@ -272,14 +276,26 @@ struct name_ref_data {
276 struct string_list exclude_filters;
277 };
278
279 +struct pretty_format {
280 + struct pretty_print_context ctx;
281 + struct userformat_want want;
282 +};
283 +
284 enum command_type {
285 NAME_REV = 1,
286 + FORMAT_REV = 2,
287 +};
288 +
289 +enum stdin_mode {
290 + TEXT = 1,
291 + REVS = 2,
292 };
293
294 struct command {
295 enum command_type type;
296 union {
297 int name_only;
298 + struct pretty_format *pretty_format;
299 } u;
300 };
301
@@ -290,6 +306,13 @@ static void init_name_rev_command(struct command *cmd,
306 cmd->u.name_only = name_only;
307 }
308
309 +static void init_format_rev_command(struct command *cmd,
310 + struct pretty_format *pretty_format)
311 +{
312 + cmd->type = FORMAT_REV;
313 + cmd->u.pretty_format = pretty_format;
314 +}
315 +
316 static struct tip_table {
317 struct tip_table_entry {
318 struct object_id oid;
@@ -495,6 +518,27 @@ static const char *get_rev_name(const struct object *o, struct strbuf *buf)
518 }
519 }
520
521 +static const char *get_format_rev(const struct commit *c,
522 + struct pretty_format *format_ctx,
523 + struct strbuf *buf)
524 +{
525 + strbuf_reset(buf);
526 +
527 + if (format_ctx->want.notes) {
528 + struct strbuf notebuf = STRBUF_INIT;
529 +
530 + format_display_notes(&c->object.oid, &notebuf,
531 + get_log_output_encoding(),
532 + format_ctx->ctx.fmt == CMIT_FMT_USERFORMAT);
533 + format_ctx->ctx.notes_message = strbuf_detach(&notebuf, NULL);
534 + }
535 +
536 + pretty_print_commit(&format_ctx->ctx, c, buf);
537 + FREE_AND_NULL(format_ctx->ctx.notes_message);
538 +
539 + return buf->buf;
540 +}
541 +
542 static void show_name(const struct object *obj,
543 const char *caller_name,
544 int always, int allow_undefined, int name_only)
@@ -564,6 +608,18 @@ static void name_rev_line(char *p, struct command *cmd)
608 else
609 printf("%.*s (%s)", p_len, p_start, name);
610 break;
611 + case FORMAT_REV:
612 + if (!oid_ret)
613 + o = parse_object(the_repository, &oid);
614 + if (o && o->type == OBJ_COMMIT)
615 + name = get_format_rev((const struct commit *)o,
616 + cmd->u.pretty_format,
617 + &buf);
618 + if (name)
619 + printf("%.*s%s", p_len - hexsz, p_start, name);
620 + else
621 + printf("%.*s", p_len, p_start);
622 + break;
623 default:
624 BUG("uncovered case: %d", cmd->type);
625 }
@@ -717,3 +773,170 @@ int cmd_name_rev(int argc,
773 object_array_clear(&revs);
774 return 0;
775 }
776 +
777 +struct format_nul_data {
778 + bool nul_input;
779 + bool nul_output;
780 +};
781 +
782 +static int format_nul_cb(const struct option *option,
783 + const char *arg,
784 + int unset)
785 +{
786 + struct format_nul_data *data = option->value;
787 + data->nul_input = 1;
788 + data->nul_output = 1;
789 + BUG_ON_OPT_NEG(unset);
790 + BUG_ON_OPT_ARG(arg);
791 + return 0;
792 +}
793 +
794 +static enum stdin_mode parse_stdin_mode(const char *stdin_mode)
795 +{
796 + if (!strcmp(stdin_mode, "text"))
797 + return TEXT;
798 + else if (!strcmp(stdin_mode, "revs") ||
799 + !strcmp(stdin_mode, "rev"))
800 + return REVS;
801 + else
802 + die(_("'%s' needs to be either text, revs, or rev"),
803 + "--stdin-mode");
804 +}
805 +
806 +static char const *const format_rev_usage[] = {
807 + N_("(EXPERIMENTAL!) git format-rev --stdin-mode=<mode> "
808 + "--format=<pretty> [--[no-]notes=<ref>] "
809 + "[-z] [--[no-]null-output] [--[no-]null-input]"),
810 + NULL
811 +};
812 +
813 +int cmd_format_rev(int argc,
814 + const char **argv,
815 + const char *prefix,
816 + struct repository *repo UNUSED)
817 +{
818 + const char *format = NULL;
819 + enum stdin_mode stdin_mode;
820 + const char *stdin_mode_arg = NULL;
821 + struct format_nul_data nul_data = { 0, 0 };
822 + char output_terminator;
823 + strbuf_getline_fn getline_fn;
824 + struct display_notes_opt format_notes_opt;
825 + struct rev_info format_rev = REV_INFO_INIT;
826 + struct pretty_format format_pp = { 0 };
827 + struct string_list notes = STRING_LIST_INIT_NODUP;
828 + struct strbuf scratch_buf = STRBUF_INIT;
829 + struct command cmd;
830 + struct option opts[] = {
831 + OPT_STRING(0, "format", &format, N_("format"),
832 + N_("pretty format to use")),
833 + OPT_STRING(0, "stdin-mode", &stdin_mode_arg, N_("stdin-mode"),
834 + N_("how revs are processed")),
835 + OPT_STRING_LIST(0, "notes", &notes, N_("notes"),
836 + N_("display notes for pretty format")),
837 + OPT_CALLBACK_F('z', "null", &nul_data, N_("z"),
838 + N_("Use NUL for input and output termination"),
839 + PARSE_OPT_NOARG | PARSE_OPT_NONEG, format_nul_cb),
840 + OPT_BOOL(0, "null-input", &nul_data.nul_input,
841 + N_("Use NUL for input termination")),
842 + OPT_BOOL(0, "null-output", &nul_data.nul_output,
843 + N_("Use NUL for output termination")),
844 + OPT_END(),
845 + };
846 +
847 + argc = parse_options(argc, argv, prefix, opts, format_rev_usage, 0);
848 +
849 + if (argc > 0) {
850 + error(_("too many arguments"));
851 + usage_with_options(format_rev_usage, opts);
852 + }
853 +
854 + if (!format)
855 + die(_("'%s' is required"), "--format");
856 + if (!stdin_mode_arg)
857 + die(_("'%s' is required"), "--stdin-mode");
858 +
859 + getline_fn = nul_data.nul_input ? strbuf_getline_nul : strbuf_getline_lf;
860 + output_terminator = nul_data.nul_output ? '\0' : '\n';
861 +
862 + init_display_notes(&format_notes_opt);
863 + stdin_mode = parse_stdin_mode(stdin_mode_arg);
864 +
865 + get_commit_format(format, &format_rev);
866 + format_pp.ctx.rev = &format_rev;
867 + format_pp.ctx.fmt = format_rev.commit_format;
868 + format_pp.ctx.abbrev = format_rev.abbrev;
869 + format_pp.ctx.date_mode_explicit = format_rev.date_mode_explicit;
870 + format_pp.ctx.date_mode = format_rev.date_mode;
871 + format_pp.ctx.color = GIT_COLOR_AUTO;
872 +
873 + userformat_find_requirements(format,
874 + &format_pp.want);
875 + if (format_pp.want.notes) {
876 + int ignore_show_notes = 0;
877 + struct string_list_item *n;
878 +
879 + for_each_string_list_item(n, &notes)
880 + enable_ref_display_notes(&format_notes_opt,
881 + &ignore_show_notes,
882 + n->string);
883 + load_display_notes(&format_notes_opt);
884 + }
885 +
886 + init_format_rev_command(&cmd, &format_pp);
887 +
888 + switch (stdin_mode) {
889 + case TEXT:
890 + while (getline_fn(&scratch_buf, stdin) != EOF) {
891 + name_rev_line(scratch_buf.buf, &cmd);
892 + /*
893 + * We do not pass on the terminator to name_rev_line,
894 + * unlike name-rev.
895 + */
896 + printf("%c", output_terminator);
897 + maybe_flush_or_die(stdout, "stdout");
898 + }
899 + break;
900 + case REVS:
901 + while (getline_fn(&scratch_buf, stdin) != EOF) {
902 + struct object_id oid;
903 + struct object *object;
904 + struct object *peeled;
905 +
906 + if (repo_get_oid(the_repository, scratch_buf.buf, &oid)) {
907 + fprintf(stderr, "Could not get object name for %s. Skipping.\n",
908 + scratch_buf.buf);
909 + continue;
910 + }
911 +
912 + object = parse_object(the_repository, &oid);
913 + if (!object) {
914 + fprintf(stderr, "Could not get object for %s. Skipping.\n",
915 + scratch_buf.buf);
916 + continue;
917 + }
918 +
919 + peeled = deref_tag(the_repository, object, scratch_buf.buf, 0);
920 + if (!peeled || peeled->type != OBJ_COMMIT) {
921 + fprintf(stderr,
922 + "Could not get commit for %s. Skipping.\n",
923 + scratch_buf.buf);
924 + continue;
925 + }
926 +
927 + get_format_rev((struct commit *)peeled,
928 + &format_pp, &scratch_buf);
929 + printf("%s%c", scratch_buf.buf, output_terminator);
930 + maybe_flush_or_die(stdout, "stdout");
931 + strbuf_release(&scratch_buf);
932 + }
933 + break;
934 + default:
935 + BUG("uncovered case: %d", stdin_mode);
936 + }
937 +
938 + strbuf_release(&scratch_buf);
939 + string_list_clear(&notes, 0);
940 + release_display_notes(&format_notes_opt);
941 + return 0;
942 +}
command-list.txt
+1
@@ -108,6 +108,7 @@ git-fmt-merge-msg purehelpers
108 git-for-each-ref plumbinginterrogators
109 git-for-each-repo plumbinginterrogators
110 git-format-patch mainporcelain
111 +git-format-rev plumbinginterrogators
112 git-fsck ancillaryinterrogators complete
113 git-gc mainporcelain
114 git-get-tar-commit-id plumbinginterrogators
git.c
+1
@@ -578,6 +578,7 @@ static struct cmd_struct commands[] = {
578 { "for-each-ref", cmd_for_each_ref, RUN_SETUP },
579 { "for-each-repo", cmd_for_each_repo, RUN_SETUP_GENTLY },
580 { "format-patch", cmd_format_patch, RUN_SETUP },
581 + { "format-rev", cmd_format_rev, RUN_SETUP },
582 { "fsck", cmd_fsck, RUN_SETUP },
583 { "fsck-objects", cmd_fsck, RUN_SETUP },
584 { "fsmonitor--daemon", cmd_fsmonitor__daemon, RUN_SETUP },
t/t1517-outside-repo.sh
+2 -1
@@ -114,7 +114,8 @@ do
114 archimport | citool | credential-netrc | credential-libsecret | \
115 credential-osxkeychain | cvsexportcommit | cvsimport | cvsserver | \
116 daemon | \
117 - difftool--helper | filter-branch | fsck-objects | get-tar-commit-id | \
117 + difftool--helper | filter-branch | format-rev | fsck-objects | \
118 + get-tar-commit-id | \
119 gui | gui--askpass | \
120 http-backend | http-fetch | http-push | init-db | \
121 merge-octopus | merge-one-file | merge-resolve | mergetool | \
t/t6120-describe.sh
+194
@@ -801,4 +801,198 @@ test_expect_success 'do not be fooled by invalid describe format ' '
801 test_must_fail git cat-file -t "refs/tags/super-invalid/./../...../ ~^:/?*[////\\\\\\&}/busted.lock-42-g"$(cat out)
802 '
803
804 +test_expect_success 'setup: format-rev' '
805 + mkdir repo-format &&
806 + git -C repo-format init &&
807 + test_commit -C repo-format first &&
808 + test_commit -C repo-format second &&
809 + test_commit -C repo-format third &&
810 + test_commit -C repo-format fourth &&
811 + test_commit -C repo-format fifth &&
812 + test_commit -C repo-format sixth &&
813 + test_commit -C repo-format seventh &&
814 + test_commit -C repo-format eighth
815 +'
816 +
817 +test_expect_success 'format-rev --stdin-mode=revs' '
818 + cat >expect <<-\EOF &&
819 + eighth
820 + seventh
821 + fifth
822 + EOF
823 + git -C repo-format format-rev --stdin-mode=revs \
824 + --format=%s >actual <<-\EOF &&
825 + HEAD
826 + HEAD~
827 + HEAD~3
828 + EOF
829 + test_cmp expect actual
830 +'
831 +
832 +test_expect_success 'format-rev --stdin-mode=text from rev-list same as log' '
833 + git -C repo-format log --format=reference >expect &&
834 + test_file_not_empty expect &&
835 + git -C repo-format rev-list HEAD >list &&
836 + git -C repo-format format-rev --stdin-mode=text \
837 + --format=reference <list >actual &&
838 + test_cmp expect actual
839 +'
840 +
841 +test_expect_success 'format-rev --stdin-mode=text with running text and tree oid' '
842 + cmit_oid=$(git -C repo-format rev-parse fifth) &&
843 + reference=$(git -C repo-format log -n1 --format=reference fifth) &&
844 + tree=$(git -C repo-format rev-parse HEAD^{tree}) &&
845 + cat >expect <<-EOF &&
846 + We thought we fixed this in ${reference}.
847 + But look at this tree: ${tree}.
848 + EOF
849 + git -C repo-format format-rev --stdin-mode=text --format=reference \
850 + >actual <<-EOF &&
851 + We thought we fixed this in ${cmit_oid}.
852 + But look at this tree: ${tree}.
853 + EOF
854 + test_cmp expect actual
855 +'
856 +
857 +test_expect_success 'format-rev with %N (note)' '
858 + test_when_finished "git -C repo-format notes remove" &&
859 + git -C repo-format notes add -m"Make a note" &&
860 + printf "Make a note\n\n\n" >expect &&
861 + git -C repo-format format-rev --stdin-mode=revs \
862 + --format="tformat:%N" \
863 + >actual <<-\EOF &&
864 + HEAD
865 + HEAD~
866 + EOF
867 + test_cmp expect actual
868 +'
869 +
870 +test_expect_success 'format-rev --notes<ref> (custom notes ref)' '
871 + # One custom notes ref
872 + test_when_finished "git -C repo-format notes remove" &&
873 + test_when_finished "git -C repo-format notes --ref=word remove" &&
874 + git -C repo-format notes add -m"default" &&
875 + git -C repo-format notes --ref=word add -m"custom" &&
876 + printf "custom\n\n" >expect &&
877 + git -C repo-format format-rev --stdin-mode=revs \
878 + --format="tformat:%N" \
879 + --notes=word \
880 + >actual <<-\EOF &&
881 + HEAD
882 + EOF
883 + test_cmp expect actual &&
884 + # Glob all
885 + printf "default\ncustom\n\n" >expect &&
886 + git -C repo-format format-rev --stdin-mode=revs \
887 + --format="tformat:%N" \
888 + --notes=* >actual <<-\EOF &&
889 + HEAD
890 + EOF
891 + test_cmp expect actual
892 +'
893 +
894 +test_expect_success 'format-rev --stdin-mode=revs on annotated tag peels to commit' '
895 + test_when_finished "git -C repo-format tag -d version" &&
896 + git -C repo-format tag -a -m"new version" version &&
897 + cat >expect <<-\EOF &&
898 + eighth
899 + EOF
900 + git -C repo-format format-rev --stdin-mode=revs \
901 + --format=%s \
902 + >actual <<-\EOF &&
903 + version
904 + EOF
905 + test_cmp expect actual
906 +'
907 +
908 +test_expect_success 'format-rev --stdin-mode=revs lookup failures' '
909 + test_when_finished "git -C repo-format tag -d tag-to-tree" &&
910 + invalid_syntax=not-valid &&
911 + non_existing_oid=${EMPTY_BLOB} &&
912 + tree=$(git -C repo-format rev-parse eighth^{tree}) &&
913 + git -C repo-format tag -a -mmessage tag-to-tree "$tree" &&
914 + tag_to_tree=$(git -C repo-format rev-parse tag-to-tree) &&
915 + cat >expect <<-EOF &&
916 + Could not get object name for ${invalid_syntax}. Skipping.
917 + Could not get object for ${non_existing_oid}. Skipping.
918 + Could not get commit for ${tree}. Skipping.
919 + Could not get commit for ${tag_to_tree}. Skipping.
920 + EOF
921 + git -C repo-format format-rev --stdin-mode=revs \
922 + --format=%s \
923 + 2>actual >out <<-EOF &&
924 + ${invalid_syntax}
925 + ${non_existing_oid}
926 + ${tree}
927 + ${tag_to_tree}
928 + EOF
929 + test_line_count = 0 out &&
930 + test_cmp expect actual
931 +'
932 +
933 +
934 +test_expect_success 'format-rev -z --stdin-mode=text with object name lookup failures' '
935 + printf "%s\0" "$(git -C repo-format rev-parse HEAD)" >input &&
936 + printf "%s\0" "$(git -C repo-format rev-parse HEAD^{tree})" >>input &&
937 + printf "%s\0" "$EMPTY_BLOB" >>input &&
938 + printf "%s\0" "$(git -C repo-format log --format=%s -1)" >expect &&
939 + printf "%s\0" "$(git -C repo-format rev-parse HEAD^{tree})" >>expect &&
940 + printf "%s\0" "$EMPTY_BLOB" >>expect &&
941 + git -C repo-format format-rev --stdin-mode=text \
942 + --format=%s -z <input >actual &&
943 + test_cmp expect actual
944 +'
945 +
946 +test_expect_success 'setup: format-rev input and output separators' '
947 + git -C repo-format rev-list HEAD >input-lf &&
948 + git -C repo-format rev-list -z HEAD >input-nul &&
949 + git -C repo-format log --format=%s >output-lf &&
950 + git -C repo-format log -z --format=%s >output-nul &&
951 + echo revs >stdin-modes &&
952 + echo text >>stdin-modes
953 +'
954 +
955 +while read mode
956 +do
957 + test_expect_success "format-rev -z --stdin-mode=$mode" '
958 + cat output-nul >expect &&
959 + git -C repo-format format-rev --stdin-mode="$mode" \
960 + --format=%s -z <input-nul >actual &&
961 + test_cmp expect actual
962 + '
963 +
964 + test_expect_success "format-rev -z --no-null-input --no-null-output --stdin-mode=$mode" '
965 + cat output-lf >expect &&
966 + git -C repo-format format-rev --stdin-mode="$mode" \
967 + --format=%s -z --no-null-input --no-null-output \
968 + <input-lf >actual &&
969 + test_cmp expect actual
970 + '
971 +
972 + test_expect_success "format-rev ---null-input --stdin-mode=$mode" '
973 + cat output-lf >expect &&
974 + git -C repo-format format-rev --stdin-mode="$mode" \
975 + --format=%s --null-input \
976 + <input-nul >actual &&
977 + test_cmp expect actual
978 + '
979 +
980 + test_expect_success "format-rev --null-output --stdin-mode=$mode" '
981 + cat output-nul >expect &&
982 + git -C repo-format format-rev --stdin-mode="$mode" \
983 + --format=%s --null-output \
984 + <input-lf >actual &&
985 + test_cmp expect actual
986 + '
987 +
988 + test_expect_success "format-rev -z --stdin-mode=$mode with multi-line output" '
989 + format="%s%n%aI" &&
990 + git -C repo-format log -z --format="$format" \
991 + >expect &&
992 + git -C repo-format format-rev --stdin-mode="$mode" \
993 + --format="$format" -z <input-nul >actual &&
994 + test_cmp expect actual
995 + '
996 +done <stdin-modes
997 +
998 test_done