Merge branch 'kh/format-rev-more-options' into seen

The experimental 'git format-rev' has been taught a few more
formatting options.

* kh/format-rev-more-options:
  format-rev: learn --abbrev, --color, and --date
  doc: rev-list-options.adoc: factor out --date alts
  format-rev: factor option variables into a struct
  format-rev: place BUG calls first in callback
  format-rev: use lower case for opts description
seen
Junio C Hamano 2026-08-31 13:53:28 -07:00
commit c0d6a4d68a
5 changed files with 213 additions and 94 deletions

View File

@ -9,7 +9,7 @@ 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]
(EXPERIMENTAL!) git format-rev [<options>] --stdin-mode=<mode> --format=<pretty>

DESCRIPTION
-----------
@ -33,8 +33,8 @@ OPTIONS
The argument `rev` is also accepted.

`text`;; Formats all commit object names found in freeform text. These
must be full object names, i.e. abbreviated hexadecimal object
names will not be interpreted.
must be full object names, i.e. abbreviated hexadecimal (_hex_)
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).
@ -76,6 +76,44 @@ This is useful if the output could contain newlines, for example if the
+
This is useful if the input revision expressions could contain newlines.

`--color[=<when>]`::
`--no-color`::
Respect color formatting. The default color behavior is
`auto`. Bare `--color` is the same as `--color=always`.
+
Giving `--no-color` is the same as `--color=never`.
+
_<when>_ must be one of:
+
--
`always`;;
Always use color, even if the output is something like a file.
`never`;;
Never use color.
`auto`;;
Use color when the output is a terminal but not when the output
is something like a file.
--

`--abbrev[=<n>]`::
`--no-abbrev`::
Abbreviate the commit hex output. Without _<n>_ it will find the
minimum length which can describe the commit uniquely, with some
extra slack. Giving _<n>_ specifies the minimum length; a longer
length will be used if needed.
+
Giving `--no-abbrev` will turn off abbreviation, showing the full commit
hex output.
+
Note that some pretty formats use `--abbrev`. This behavior can be
controlled with these two options.

`--date=<format>`::
Date format for pretty formats. Note that date atoms like `%aI`
are not affected. This option cannot be negated.
+
include::rev-list-option-date-alternatives.adoc[]

[[io]]
INPUT AND OUTPUT FORMAT
-----------------------

View File

@ -0,0 +1,55 @@
--
`--date=relative` shows dates relative to the current time,
e.g. ``2 hours ago''. The `-local` option has no effect for
`--date=relative`.

`--date=local` is an alias for `--date=default-local`.

`--date=iso` (or `--date=iso8601`) shows timestamps in a ISO 8601-like format.
The differences to the strict ISO 8601 format are:

- a space instead of the `T` date/time delimiter
- a space between time and time zone
- no colon between hours and minutes of the time zone

`--date=iso-strict` (or `--date=iso8601-strict`) shows timestamps in strict
ISO 8601 format.

`--date=rfc` (or `--date=rfc2822`) shows timestamps in RFC 2822
format, often found in email messages.

`--date=short` shows only the date, but not the time, in `YYYY-MM-DD` format.

`--date=raw` shows the date as seconds since the epoch (1970-01-01
00:00:00 UTC), followed by a space, and then the timezone as an offset
from UTC (a `+` or `-` with four digits; the first two are hours, and
the second two are minutes). I.e., as if the timestamp were formatted
with `strftime("%s %z")`).
Note that the `-local` option does not affect the seconds-since-epoch
value (which is always measured in UTC), but does switch the accompanying
timezone value.

`--date=human` shows the timezone if the timezone does not match the
current time-zone, and doesn't print the whole date if that matches
(ie skip printing year for dates that are "this year", but also skip
the whole date itself if it's in the last few days and we can just say
what weekday it was). For older dates the hour and minute is also
omitted.

`--date=unix` shows the date as a Unix epoch timestamp (seconds since
1970). As with `--raw`, this is always in UTC and therefore `-local`
has no effect.

`--date=format:<format>` feeds the _<format>_ to your system `strftime`,
except for `%s`, `%z`, and `%Z`, which are handled internally.
Use `--date=format:%c` to show the date in your system locale's
preferred format. See the `strftime`(3) manual for a complete list of
format placeholders. When using `-local`, the correct syntax is
`--date=format-local:<format>`.

`--date=default` is the default format, and is based on ctime(3)
output. It shows a single line with three-letter day of the week,
three-letter month, day-of-month, hour-minute-seconds in "HH:MM:SS"
format, followed by 4-digit year, plus timezone information, unless
the local time zone is used, e.g. `Thu Jan 1 00:00:00 1970 +0000`.
--

View File

@ -1132,61 +1132,7 @@ include::pretty-options.adoc[]
author's). If `-local` is appended to the format (e.g.,
`iso-local`), the user's local time zone is used instead.
+
--
`--date=relative` shows dates relative to the current time,
e.g. ``2 hours ago''. The `-local` option has no effect for
`--date=relative`.

`--date=local` is an alias for `--date=default-local`.

`--date=iso` (or `--date=iso8601`) shows timestamps in a ISO 8601-like format.
The differences to the strict ISO 8601 format are:

- a space instead of the `T` date/time delimiter
- a space between time and time zone
- no colon between hours and minutes of the time zone

`--date=iso-strict` (or `--date=iso8601-strict`) shows timestamps in strict
ISO 8601 format.

`--date=rfc` (or `--date=rfc2822`) shows timestamps in RFC 2822
format, often found in email messages.

`--date=short` shows only the date, but not the time, in `YYYY-MM-DD` format.

`--date=raw` shows the date as seconds since the epoch (1970-01-01
00:00:00 UTC), followed by a space, and then the timezone as an offset
from UTC (a `+` or `-` with four digits; the first two are hours, and
the second two are minutes). I.e., as if the timestamp were formatted
with `strftime("%s %z")`).
Note that the `-local` option does not affect the seconds-since-epoch
value (which is always measured in UTC), but does switch the accompanying
timezone value.

`--date=human` shows the timezone if the timezone does not match the
current time-zone, and doesn't print the whole date if that matches
(ie skip printing year for dates that are "this year", but also skip
the whole date itself if it's in the last few days and we can just say
what weekday it was). For older dates the hour and minute is also
omitted.

`--date=unix` shows the date as a Unix epoch timestamp (seconds since
1970). As with `--raw`, this is always in UTC and therefore `-local`
has no effect.

`--date=format:<format>` feeds the _<format>_ to your system `strftime`,
except for `%s`, `%z`, and `%Z`, which are handled internally.
Use `--date=format:%c` to show the date in your system locale's
preferred format. See the `strftime`(3) manual for a complete list of
format placeholders. When using `-local`, the correct syntax is
`--date=format-local:<format>`.

`--date=default` is the default format, and is based on ctime(3)
output. It shows a single line with three-letter day of the week,
three-letter month, day-of-month, hour-minute-seconds in "HH:MM:SS"
format, followed by 4-digit year, plus timezone information, unless
the local time zone is used, e.g. `Thu Jan 1 00:00:00 1970 +0000`.
--
include::rev-list-option-date-alternatives.adoc[]

ifdef::git-rev-list[]
`--header`::

View File

@ -21,6 +21,7 @@
#include "revision.h"
#include "notes.h"
#include "write-or-die.h"
#include "date.h"

/*
* One day. See the 'name a rev shortly after epoch' test in t6120 when
@ -772,20 +773,36 @@ int cmd_name_rev(int argc,
return 0;
}

struct format_nul_data {
struct format_rev_data {
const char *format;
const char *stdin_mode;
bool nul_input;
bool nul_output;
struct string_list notes;
struct rev_info rev;
int color;
};

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;
struct format_rev_data *data = option->value;
BUG_ON_OPT_NEG(unset);
BUG_ON_OPT_ARG(arg);
data->nul_input = 1;
data->nul_output = 1;
return 0;
}

static int date_cb(const struct option *option,
const char *arg,
int unset)
{
struct rev_info *data = option->value;
BUG_ON_OPT_NEG(unset);
parse_date_format(arg, &data->date_mode);
data->date_mode_explicit = 1;
return 0;
}

@ -802,9 +819,8 @@ 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> [--[no-]notes=<ref>] "
"[-z] [--[no-]null-output] [--[no-]null-input]"),
N_("(EXPERIMENTAL!) git format-rev [<options>] "
"--stdin-mode=<mode> --format=<pretty>"),
NULL
};

@ -813,32 +829,37 @@ int cmd_format_rev(int argc,
const char *prefix,
struct repository *repo UNUSED)
{
const char *format = NULL;
struct format_rev_data data = {
.notes = STRING_LIST_INIT_NODUP,
.rev = REV_INFO_INIT,
.color = GIT_COLOR_AUTO,
};
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"),
OPT_STRING(0, "format", &data.format, N_("format"),
N_("pretty format to use")),
OPT_STRING(0, "stdin-mode", &stdin_mode_arg, N_("stdin-mode"),
OPT_STRING(0, "stdin-mode", &data.stdin_mode, N_("stdin-mode"),
N_("how revs are processed")),
OPT_STRING_LIST(0, "notes", &notes, N_("notes"),
OPT_STRING_LIST(0, "notes", &data.notes, 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"),
OPT__ABBREV(&data.rev.abbrev),
OPT__COLOR(&data.color, N_("use colored output")),
OPT_CALLBACK_F(0, "date", &data.rev, N_("date"),
N_("date format"),
PARSE_OPT_NONEG, date_cb),
OPT_CALLBACK_F('z', "null", &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_BOOL(0, "null-input", &data.nul_input,
N_("use NUL for input termination")),
OPT_BOOL(0, "null-output", &data.nul_output,
N_("use NUL for output termination")),
OPT_END(),
};

@ -849,32 +870,32 @@ int cmd_format_rev(int argc,
usage_with_options(format_rev_usage, opts);
}

if (!format)
if (!data.format)
die(_("'%s' is required"), "--format");
if (!stdin_mode_arg)
if (!data.stdin_mode)
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';
getline_fn = data.nul_input ? strbuf_getline_nul : strbuf_getline_lf;
output_terminator = data.nul_output ? '\0' : '\n';

init_display_notes(&format_notes_opt);
stdin_mode = parse_stdin_mode(stdin_mode_arg);
stdin_mode = parse_stdin_mode(data.stdin_mode);

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;
get_commit_format(data.format, &data.rev);
format_pp.ctx.rev = &data.rev;
format_pp.ctx.fmt = data.rev.commit_format;
format_pp.ctx.abbrev = data.rev.abbrev;
format_pp.ctx.date_mode_explicit = data.rev.date_mode_explicit;
format_pp.ctx.date_mode = data.rev.date_mode;
format_pp.ctx.color = data.color;

userformat_find_requirements(format,
userformat_find_requirements(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, &notes)
for_each_string_list_item(n, &data.notes)
enable_ref_display_notes(&format_notes_opt,
&ignore_show_notes,
n->string);
@ -933,8 +954,9 @@ int cmd_format_rev(int argc,
BUG("uncovered case: %d", stdin_mode);
}

date_mode_release(&data.rev.date_mode);
strbuf_release(&scratch_buf);
string_list_clear(&notes, 0);
string_list_clear(&data.notes, 0);
release_display_notes(&format_notes_opt);
return 0;
}

View File

@ -1017,4 +1017,62 @@ do
'
done <stdin-modes

format_rev_cmp_log () {
opts="$1"
format=reference
cat >input <<-\EOF &&
third
second
first
EOF
git -C repo-format log --stdin --no-walk \
--format="$format" "$opts" >expect <input &&
git -C repo-format format-rev --stdin-mode=revs \
--format="$format" "$opts" >actual <input &&
test_cmp expect actual
}

format_rev_err_cmp_log () {
opts="$1"
format=reference
# No input since we ought to fail while parsing options
test_must_fail git -C repo-format log --stdin --no-walk \
--format="$format" "$opts" 2>expect &&
test_must_fail git -C repo-format format-rev \
--stdin-mode=revs --format="$format" "$opts" 2>actual &&
test_cmp expect actual
}

test_expect_success 'format-rev --color' '
format_rev_cmp_log --color=always &&
format_rev_cmp_log --color &&
format_rev_cmp_log --no-color &&
format_rev_err_cmp_log --color=not-valid
'

test_expect_success 'format-rev --abbrev' '
format_rev_cmp_log --abbrev &&
format_rev_cmp_log --abbrev=31 &&
format_rev_cmp_log --no-abbrev
'

test_expect_success 'format-rev --date' '
format_rev_cmp_log --date=relative &&
format_rev_cmp_log --date=iso-strict &&
# This also tests the only case where we need to release
# the data for the parsed format
format_rev_cmp_log --date="format:%c" &&
format_rev_err_cmp_log --date=not-valid &&
# Test --date (no arg) next
# We cannot compare the output to git-log(1)
# because that command uses a slightly different
# error message (different library)
cat >expect <<-EOF &&
error: option \`date${SQ} requires a value
EOF
test_must_fail git -C repo-format format-rev \
--stdin-mode=revs --format="$format" --date 2>actual &&
test_cmp expect actual
'

test_done