diff mbox series

[v2] pretty: add %(decorate[:<options>]) format

Message ID 20230715160730.4046-1-andy.koppe@gmail.com (mailing list archive)
State New, archived
Headers show
Series [v2] pretty: add %(decorate[:<options>]) format | expand

Commit Message

Andy Koppe July 15, 2023, 4:07 p.m. UTC
This lists ref names in the same way as the %d decoration format, but
allows all the otherwise fixed strings printed around the ref names to
be customized, namely prefix, suffix, separator, the "tag:" annotation
and the arrow used to show where HEAD points.

Examples:
- %(decorate) is equivalent to %d.
- %(decorate:prefix=,suffix=) is equivalent to %D.
- %(decorate:prefix=,suffix=,separator= ,tag=,arrow=->) produces a
  space-separated list without wrapping, tag annotations or spaces
  around the arrow.
- %(decorate:prefix=[,suffix=],separator=%x2C,arrow=%x2C,tag=) produces
  a comma-separated list enclosed in square brackets where the arrow is
  replaced by a comma as well.

Add functions parse_decoration_option(), parse_decoration_options() and
free_decoration_options() to help implement the format. Test it in
t4205-log-pretty-formats.sh and document it in pretty-formats.txt.

Refactor format_decorations() to take a struct decoration_options
argument specifying those strings, whereby NULL entries select the
default. Avoid emitting color sequences for empty strings.

Wrap tag annotations in separate color sequences from tag names, because
otherwise tag names can end up uncolored when %w width formatting breaks
lines between annotation and name. Amend t4207-log-decoration-colors.sh
accordingly.

Signed-off-by: Andy Koppe <andy.koppe@gmail.com>
---
Corrected mistakes in commit description.

 Documentation/pretty-formats.txt | 19 ++++++++-
 log-tree.c                       | 69 ++++++++++++++++++++------------
 log-tree.h                       | 17 ++++----
 pretty.c                         | 62 +++++++++++++++++++++++++++-
 t/t4205-log-pretty-formats.sh    | 21 ++++++++++
 t/t4207-log-decoration-colors.sh | 32 +++++++++------
 6 files changed, 171 insertions(+), 49 deletions(-)

Comments

Junio C Hamano July 17, 2023, 11:10 p.m. UTC | #1
Andy Koppe <andy.koppe@gmail.com> writes:

> This lists ref names in the same way as the %d decoration format, but

Please replace "This" with "%(descorate[:<options>]", i.e. a more
concrete form, so that people do not have to go back to the title in
order to understand the body of the proposed log message.

> -'%(describe[:options])':: human-readable name, like
> +'%(describe[:<options>])':: human-readable name, like
> -'%(trailers[:options])':: display the trailers of the body as
> +'%(trailers[:<options>])':: display the trailers of the body as

It is a very good idea to signal that <options> is a placeholder by
enclosing it inside <angle bracket> like this patch wants to do with
%(decorate), and to make sure that other existing ones consistently
follow the same convention.

But the latter, being very small, can be buried in the noise.

It may be a good idea to have small "preliminary clean-up" patches
that do not add anything related to %(decorate) at the beginning of
the series.  [PATCH 1/3] can be %(token[:<options>]) clean-up,
[PATCH 2/3] can be "what is literal formatting code" clarification,
and [PATCH 3/3] can be the rest of this patch, for example.

> +'%(decorate[:<options>])':: ref names with custom decorations.
> +			  The `decorate` string may be followed by a colon
> +			  and zero or more comma-separated options.
> +			  Option values may contain literal formatting codes.
> +			  These must be used for commas (`%x2C`) and closing
> +			  parentheses (`%x29`), due to their role in the option
> +			  syntax.

OK.  I fear that "literal formatting codes" may not be understood by
readers without having any cross references here.  Perhaps something
like the patch attached at the end of this message would help.

> +** 'arrow=<value>': Shown between HEAD and the branch it points to, if any.
> +		    Defaults to " \-> ".

It feels a bit strange that this feature is limited only to "HEAD"
and other symbolic refs are not annotated with an arrow, but it is
not the fault of this patch.  We might want to see if it is worth to
extend this to other symbolic refs but not while this patch is being
discussed and polished.

> diff --git a/log-tree.c b/log-tree.c
> index f4b22a60cc..4b46884ef6 100644
> --- a/log-tree.c
> +++ b/log-tree.c
> @@ -301,27 +301,34 @@ static void show_name(struct strbuf *sb, const struct name_decoration *decoratio
>  
>  /*
>   * The caller makes sure there is no funny color before calling.
> - * format_decorations_extended makes sure the same after return.
> + * format_decorations ensures the same after return.
>   */
> -void format_decorations_extended(struct strbuf *sb,
> +void format_decorations(struct strbuf *sb,
>  			const struct commit *commit,
>  			int use_color,
> -			const char *prefix,
> -			const char *separator,
> -			const char *suffix)
> +			const struct decoration_options *opts)

Hmph, presumably the idea is to collect these parameters in a struct
and pass it around, which would be easier to extend, and then teach
the hardcoded default to the callee, instead of the macro.  OK.

It may have made the change easier to review if such a change that
can cleanly separable into a single step into a single preliminary
clean-up patch before we start adding the customization, but let's
read on to see if I can keep everything in my head---I'll complain
at the end if I can't ;-).

>  {
> -	const struct name_decoration *decoration;
> -	const struct name_decoration *current_and_HEAD;
> -	const char *color_commit =
> -		diff_get_color(use_color, DIFF_COMMIT);
> -	const char *color_reset =
> -		decorate_get_color(use_color, DECORATION_NONE);

Getting rid of the computation of the initialization value from the
above decl block, ...


> +	const char *color_commit, *color_reset;
> +	const char *prefix, *suffix, *separator, *arrow, *tag;
> +
> +	const struct name_decoration *current_and_HEAD;

... like this, so we will return early without wasting extra cycles
whose result we will not use, is a very good idea.  But then, ...

> +	const struct name_decoration *decoration =
> +		get_name_decoration(&commit->object);
>  
> -	decoration = get_name_decoration(&commit->object);
>  	if (!decoration)
>  		return;

... I think the original is easier to follow than the updated form
for the "decoration" variable, simply because the declaration part
will become absolutely free, and it becomes easier to see that the
computation of "decoration" is the very first thing the cycles are
spent on.

> +	color_commit = diff_get_color(use_color, DIFF_COMMIT);
> +	color_reset = decorate_get_color(use_color, DECORATION_NONE);
> +
> +	prefix = (opts && opts->prefix) ? opts->prefix : " (";
> +	suffix = (opts && opts->suffix) ? opts->suffix : ")";
> +	separator = (opts && opts->separator) ? opts->separator : ", ";

Knowing these hardcoded values were the responsibility of the
format_decorations() C preprocessor macro; now it is written here.
It is a moral no-op change from the caller's point of view.

> +	arrow = (opts && opts->arrow) ? opts->arrow : " -> ";
> +	tag = (opts && opts->tag) ? opts->tag : "tag: ";

These two are new.  That is one thing why I wondered above if it is
a good idea to separate the "refactor to introduce
decoration_options structure that has three members and replace
three parameters to this function with it, so that we can get rid of
the format_decorations() macro" into a single preliminary step.
Then the reviewers can go on, after being convinced that such a
moral no-op refactoring is correct, to review the next step that
would presumably add these two members to the option struct and make
these customizable.

>  	current_and_HEAD = current_pointed_by_HEAD(decoration);
> +
>  	while (decoration) {
>  		/*
>  		 * When both current and HEAD are there, only

Unrelated noise.

> @@ -329,20 +336,29 @@ void format_decorations_extended(struct strbuf *sb,
>  		 * appeared, skipping the entry for current.
>  		 */
>  		if (decoration != current_and_HEAD) {
> -			strbuf_addstr(sb, color_commit);
> -			strbuf_addstr(sb, prefix);
> -			strbuf_addstr(sb, color_reset);
> -			strbuf_addstr(sb, decorate_get_color(use_color, decoration->type));
> -			if (decoration->type == DECORATION_REF_TAG)
> -				strbuf_addstr(sb, "tag: ");
> +			const char *color =
> +				decorate_get_color(use_color, decoration->type);
>  
> +			if (*prefix) {
> +				strbuf_addstr(sb, color_commit);
> +				strbuf_addstr(sb, prefix);
> +				strbuf_addstr(sb, color_reset);
> +			}
> +
> +			if (*tag && decoration->type == DECORATION_REF_TAG) {
> +				strbuf_addstr(sb, color);
> +				strbuf_addstr(sb, tag);
> +				strbuf_addstr(sb, color_reset);
> +			}
> +			strbuf_addstr(sb, color);
>  			show_name(sb, decoration);

Again, this mixes adding new things (i.e. customizeable "tag:"
string, and "->" we see below) and improving existing things
(i.e. "<color><reset>" that is presumably pointless when prefix is
an empty string is shown as an empty string).  Ideally, the step to
move the three existing parameters to three members of the new
struct should be done first WITHOUT the empty-string improvement,
then another step should do the empty-string improvement (at which
time, presumably existing test script may have to be adjusted), and
then new features to costumize "tag:" and "->" should be added on
top.

> -			if (current_and_HEAD &&
> +			if (*arrow && current_and_HEAD &&
>  			    decoration->type == DECORATION_REF_HEAD) {

Because arrow is never allowed to be NULL, remove the above change,
and ...

> -				strbuf_addstr(sb, " -> ");
> +				strbuf_addstr(sb, arrow);

... let the program crash to catch a future bug at runtime.

> @@ -351,9 +367,12 @@ void format_decorations_extended(struct strbuf *sb,
>  		}
>  		decoration = decoration->next;
>  	}
> -	strbuf_addstr(sb, color_commit);
> -	strbuf_addstr(sb, suffix);
> -	strbuf_addstr(sb, color_reset);
> +
> +	if (*suffix) {
> +		strbuf_addstr(sb, color_commit);
> +		strbuf_addstr(sb, suffix);
> +		strbuf_addstr(sb, color_reset);
> +	}

Ditto about "improving the <color><reset> empty sequence is a
separate change from making various fields customizable".

I'll stop here. After skimming the changes to the test, I think this
single patch should be split into separate steps.  Perhaps the split
should go like this:

 * documentation clean-up %(token[:options]) -> %(token[:<options>])
   plus clarification of what a "literal formatting code" is.

 * introduction of "struct decoration_option",
   removing the format_decorations() macro,
   renaming format_decorations_extended() to format_decorations(),
   replacing the three parameters with a single struct pointer.

 * improving <color><reset> string when the meat of the string is
   empty.  This step will be the FIRST step that changes the
   externally visible behaviour, and presumably will have adjustment
   to existing tests.

 * making "tag:" and "->" customizable, if values are passed in the
   struct.  This step does not have UI changes, and the existing
   tests should serve as a safety net to catch mistakes in this
   step.

 * read the %(describe[:<option>]) and fill "struct describe_option".
   This will be accompanied by additional tests for the new feature.

Thanks.

---------------------- >8 ----------------------
Subject: pretty-formats: define "literal formatting code"

The description for %(trailer) option already uses this term without
having definition anywhere in the document, and we are about to add
another one %(decorate) that uses it.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
diff --git c/Documentation/pretty-formats.txt w/Documentation/pretty-formats.txt
index c08aba15af..b7a3a150ae 100644
--- c/Documentation/pretty-formats.txt
+++ w/Documentation/pretty-formats.txt
@@ -122,7 +122,9 @@ The placeholders are:
 - Placeholders that expand to a single literal character:
 '%n':: newline
 '%%':: a raw '%'
-'%x00':: print a byte from a hex code
+'%x00':: '%x' followed by two hexadecimal digits is replaced with a
+	byte with the hexdecimal digits' value (we will call this
+	"literal formatting code" in the rest of this document).
 
 - Placeholders that affect formatting of later placeholders:
 '%Cred':: switch color to red
Junio C Hamano July 18, 2023, 1:05 a.m. UTC | #2
Junio C Hamano <gitster@pobox.com> writes:

> I'll stop here. After skimming the changes to the test, I think this
> single patch should be split into separate steps.  Perhaps the split
> should go like this:
> ...
> Thanks.

Oh, sorry that I forgot to add one thing.

Overall, the patch seems to be done very well when viewed as a
whole.  Thanks for working on it.

It is just I cannot be as confident as I would like to be in my
review when the single patch does several different things at once.
If it were split in steps, each step focusing on doing a single
thing well and describing well what it does and why, reviewers can
be more confident that they did not miss something important in the
patch(es).
Glen Choo July 19, 2023, 6:16 p.m. UTC | #3
Hi Andy!

We picked up this series at Review Club. Reviewers will leave their
thoughts on the mailing list, but if you like, you can find the notes
at:

  https://docs.google.com/document/d/14L8BAumGTpsXpjDY8VzZ4rRtpAjuGrFSRqn3stCuS_w/edit

Andy Koppe <andy.koppe@gmail.com> writes:

> This lists ref names in the same way as the %d decoration format, but
> allows all the otherwise fixed strings printed around the ref names to
> be customized, namely prefix, suffix, separator, the "tag:" annotation
> and the arrow used to show where HEAD points.
>
> Examples:
> - %(decorate) is equivalent to %d.
> - %(decorate:prefix=,suffix=) is equivalent to %D.
> - %(decorate:prefix=,suffix=,separator= ,tag=,arrow=->) produces a
>   space-separated list without wrapping, tag annotations or spaces
>   around the arrow.
> - %(decorate:prefix=[,suffix=],separator=%x2C,arrow=%x2C,tag=) produces
>   a comma-separated list enclosed in square brackets where the arrow is
>   replaced by a comma as well.

I think giving the user this level of customization makes sense,
especially since we do this for other format options. Importantly, this
design also fits the existing conventions we have, so this looks like a
good proposal.

As a micro-nit: there's some useful context behind your chosen design in
[1]. It would have been useful to link to it in the `---` context, or
perhaps send this series as v3 and v4 to [1].

[1] https://lore.kernel.org/git/20230712110732.8274-1-andy.koppe@gmail.com/

> Add functions parse_decoration_option(), parse_decoration_options() and
> free_decoration_options() to help implement the format. Test it in
> t4205-log-pretty-formats.sh and document it in pretty-formats.txt.

This commit adds the new feature...

> Refactor format_decorations() to take a struct decoration_options
> argument specifying those strings, whereby NULL entries select the
> default. Avoid emitting color sequences for empty strings.

does some refactoring to support the new feature + existing use cases...

> Wrap tag annotations in separate color sequences from tag names, because
> otherwise tag names can end up uncolored when %w width formatting breaks
> lines between annotation and name. Amend t4207-log-decoration-colors.sh
> accordingly.

and fixes a bug with coloring that is easier to run into as a result of
the new feature.

As others have mentioned, I think this would be easier to follow as
separate commits. This commit isn't so big that the refactor needs to be
its own commit, though I don't feel strongly either way.

> diff --git a/Documentation/pretty-formats.txt b/Documentation/pretty-formats.txt
> index 3b71334459..c08aba15af 100644
> --- a/Documentation/pretty-formats.txt
> +++ b/Documentation/pretty-formats.txt
> @@ -222,7 +222,22 @@ The placeholders are:
>  	linkgit:git-rev-list[1])
>  '%d':: ref names, like the --decorate option of linkgit:git-log[1]
>  '%D':: ref names without the " (", ")" wrapping.
> -'%(describe[:options])':: human-readable name, like
> +'%(decorate[:<options>])':: ref names with custom decorations.
> +			  The `decorate` string may be followed by a colon
> +			  and zero or more comma-separated options.
> +			  Option values may contain literal formatting codes.
> +			  These must be used for commas (`%x2C`) and closing
> +			  parentheses (`%x29`), due to their role in the option
> +			  syntax.

To make this easier to visualize, it would be useful to include the
examples from your commit message (%d, %D, etc.).

> +'%(describe[:<options>])':: human-readable name, like

Ah, adding the <> is a good fix. I think it doesn't warrant its own
patch, but it should be called out in the commit message.

>  /*
>   * The caller makes sure there is no funny color before calling.
> - * format_decorations_extended makes sure the same after return.
> + * format_decorations ensures the same after return.
>   */
> -void format_decorations_extended(struct strbuf *sb,
> +void format_decorations(struct strbuf *sb,
>  			const struct commit *commit,
>  			int use_color,
> -			const char *prefix,
> -			const char *separator,
> -			const char *suffix)
> +			const struct decoration_options *opts)
>  {
> -	const struct name_decoration *decoration;
> -	const struct name_decoration *current_and_HEAD;
> -	const char *color_commit =
> -		diff_get_color(use_color, DIFF_COMMIT);
> -	const char *color_reset =
> -		decorate_get_color(use_color, DECORATION_NONE);
> +	const char *color_commit, *color_reset;
> +	const char *prefix, *suffix, *separator, *arrow, *tag;
> +
> +	const struct name_decoration *current_and_HEAD;
> +	const struct name_decoration *decoration =
> +		get_name_decoration(&commit->object);
>  
> -	decoration = get_name_decoration(&commit->object);
>  	if (!decoration)
>  		return;
>  
> +	color_commit = diff_get_color(use_color, DIFF_COMMIT);
> +	color_reset = decorate_get_color(use_color, DECORATION_NONE);

I'm guessing that you shuffled these lines to make use of an early
return? If so, both versions are not different enough to warrant the
churn IMO. It would be worth pointing out the reshuffling in the commit
message, especially if you had another rationale in mind.

> +	prefix = (opts && opts->prefix) ? opts->prefix : " (";
> +	suffix = (opts && opts->suffix) ? opts->suffix : ")";
> +	separator = (opts && opts->separator) ? opts->separator : ", ";
> +	arrow = (opts && opts->arrow) ? opts->arrow : " -> ";
> +	tag = (opts && opts->tag) ? opts->tag : "tag: ";

So NULL means "use the default"...

> +struct decoration_options {
> +	char *prefix;
> +	char *suffix;
> +	char *separator;
> +	char *arrow;
> +	char *tag;
> +};
> +
>  int parse_decorate_color_config(const char *var, const char *slot_name, const char *value);
>  int log_tree_diff_flush(struct rev_info *);
>  int log_tree_commit(struct rev_info *, struct commit *);
>  void show_log(struct rev_info *opt);
> -void format_decorations_extended(struct strbuf *sb, const struct commit *commit,
> -			     int use_color,
> -			     const char *prefix,
> -			     const char *separator,
> -			     const char *suffix);
> -#define format_decorations(strbuf, commit, color) \
> -			     format_decorations_extended((strbuf), (commit), (color), " (", ", ", ")")
> +void format_decorations(struct strbuf *sb, const struct commit *commit,
> +			int use_color, const struct decoration_options *opts);

Which lets us unify these two functions. Makes sense.

> +static int parse_decoration_option(const char **arg,
> +				   const char *name,
> +				   char **opt)
> +{
> +	const char *argval;
> +	size_t arglen;
> +
> +	if (match_placeholder_arg_value(*arg, name, arg, &argval, &arglen)) {
> +		char *val = xstrndup(argval, arglen);
> +		struct strbuf sb = STRBUF_INIT;
> +
> +		strbuf_expand(&sb, val, strbuf_expand_literal_cb, NULL);

strbuf_expand() got removed in 'master' recently, so this should be
rebased.

>  static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */
>  				const char *placeholder,
>  				void *context)
> @@ -1526,10 +1566,11 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */
>  		strbuf_addstr(sb, get_revision_mark(NULL, commit));
>  		return 1;
>  	case 'd':
> -		format_decorations(sb, commit, c->auto_color);
> +		format_decorations(sb, commit, c->auto_color, NULL);
>  		return 1;
>  	case 'D':
> -		format_decorations_extended(sb, commit, c->auto_color, "", ", ", "");
> +		format_decorations(sb, commit, c->auto_color,
> +				   &(struct decoration_options){"", ""});

I don't remember if C99 lets you name .prefix and .suffix here, but if
so, it would be good to name them. Otherwise it's easy to get the order
wrong, e.g. if someone reorders the fields in struct decoration_options.

> +test_expect_success 'pretty format %decorate' '
> +	git checkout -b foo &&
> +	git commit --allow-empty -m "new commit" &&
> +	git tag bar &&
> +	git branch qux &&
> +	echo " (HEAD -> foo, tag: bar, qux)" >expect1 &&
> +	git log --format="%(decorate)" -1 >actual1 &&
> +	test_cmp expect1 actual1 &&
> +	echo "HEAD -> foo, tag: bar, qux" >expect2 &&
> +	git log --format="%(decorate:prefix=,suffix=)" -1 >actual2 &&
> +	test_cmp expect2 actual2 &&
> +	echo "HEAD->foo bar qux" >expect3 &&
> +	git log --format="%(decorate:prefix=,suffix=,separator= ,arrow=->,tag=)" \
> +		-1 >actual3 &&
> +	test_cmp expect3 actual3 &&
> +	echo "[HEAD,foo,bar,qux]" >expect4 &&
> +	git log --format="%(decorate:prefix=[,suffix=],separator=%x2C,arrow=%x2C,tag=)" \
> +		-1 >actual4 &&
> +	test_cmp expect4 actual4
> +'
> +

It would be useful to get some "bad" inputs to %(decorate:) to check
that we handle them correctly, especially since it's implemented with
while() loops.

Overall, I thought this patch looks really good. Thanks!
Phillip Wood July 23, 2023, 4:25 p.m. UTC | #4
Hi Andy

On 19/07/2023 19:16, Glen Choo wrote:
>>   	case 'D':
>> -		format_decorations_extended(sb, commit, c->auto_color, "", ", ", "");
>> +		format_decorations(sb, commit, c->auto_color,
>> +				   &(struct decoration_options){"", ""});
> 
> I don't remember if C99 lets you name .prefix and .suffix here, but if
> so, it would be good to name them. Otherwise it's easy to get the order
> wrong, e.g. if someone reorders the fields in struct decoration_options.

That's a good suggestion. I think this would be the first use of a 
compound literal in the code base so it would be helpful to mention that 
in the commit message.

We've been depending on C99 for a while now so I'd support adding this 
compound literal as a test balloon for compiler support. Ævar reported a 
while back that they are supported by IBM xlc, Oracle SunCC and HP/UX's 
aCC[1] and back then I looked at NonStop which seemed to offer support 
with the right compiler flag.

Overall this is a well written, well motivated patch with a good commit 
message.

Best Wishes

Phillip

[1] https://lore.kernel.org/git/87h7e61duk.fsf@evledraar.gmail.com/
Andy Koppe Aug. 11, 2023, 6:50 p.m. UTC | #5
Junio C Hamano <gitster@pobox.com> wrote:
> Overall, the patch seems to be done very well when viewed as a
> whole.  Thanks for working on it.
>
> It is just I cannot be as confident as I would like to be in my
> review when the single patch does several different things at once.
> If it were split in steps, each step focusing on doing a single
> thing well and describing well what it does and why, reviewers can
> be more confident that they did not miss something important in the
> patch(es).

Thanks to Junio, Glen, Phil and the Review Club for the helpful
reviews, especially the guidance on commit granularity.

Sorry for not getting back to this sooner, but the v3 patch series
addressing the review comments is now here:
https://lore.kernel.org/git/20230715160730.4046-1-andy.koppe@gmail.com/T/#m46ad3ebbe3163821f649f7122edcabd619fc5837

Kind regards,
Andy
Andy Koppe Aug. 11, 2023, 6:59 p.m. UTC | #6
On Wed, 19 Jul 2023 at 19:16, Glen Choo  wrote:

> As a micro-nit: there's some useful context behind your chosen design in
> [1]. It would have been useful to link to it in the `---` context, or
> perhaps send this series as v3 and v4 to [1].
>
> [1] https://lore.kernel.org/git/20230712110732.8274-1-andy.koppe@gmail.com/

Point taken.

> > +             strbuf_expand(&sb, val, strbuf_expand_literal_cb, NULL);
>
> strbuf_expand() got removed in 'master' recently, so this should be
> rebased.

Done. I think I had started off main, wrongly assuming that it's the
same as master.

Thanks again,
Andy
Andy Koppe Aug. 11, 2023, 7:04 p.m. UTC | #7
On Sun, 23 Jul 2023 at 17:26, Phillip Wood  wrote:
>
> On 19/07/2023 19:16, Glen Choo wrote:
> >>      case 'D':
> >> -            format_decorations_extended(sb, commit, c->auto_color, "", ", ", "");
> >> +            format_decorations(sb, commit, c->auto_color,
> >> +                               &(struct decoration_options){"", ""});
> >
> > I don't remember if C99 lets you name .prefix and .suffix here, but if
> > so, it would be good to name them. Otherwise it's easy to get the order
> > wrong, e.g. if someone reorders the fields in struct decoration_options.
>
> That's a good suggestion. I think this would be the first use of a
> compound literal in the code base so it would be helpful to mention that
> in the commit message.

I've taken the suggestion, but then forgot to mention it in the commit
message. Will do in the next round.

> We've been depending on C99 for a while now so I'd support adding this
> compound literal as a test balloon for compiler support. Ævar reported a
> while back that they are supported by IBM xlc, Oracle SunCC and HP/UX's
> aCC[1] and back then I looked at NonStop which seemed to offer support
> with the right compiler flag.

There are a number of uses of designated initializers already, so
hopefully compound literals aren't too much of an extra challenge.

Thanks,
Andy
Junio C Hamano Aug. 11, 2023, 8:38 p.m. UTC | #8
Andy Koppe <andy.koppe@gmail.com> writes:

> There are a number of uses of designated initializers already, so
> hopefully compound literals aren't too much of an extra challenge.

I do not see how one leads to the other here.  I'd prefer not to see
use of a new construct we do not currently use mixed in a new code,
even if it is mentioned in the proposed log message.

If we want to use compound literals in our codebase in the longer
term, we should first add a weatherballoon use to a very stable part
of the codebase that rarely changes, in a single patch that is
trivial to revert when a platform that matters is found to have
problem with the language construct, just like what we did when we
adopted the use of designated initializers.

Thanks.
Andy Koppe Aug. 11, 2023, 10:06 p.m. UTC | #9
On 11/08/2023 21:38, Junio C Hamano wrote:
 > Andy Koppe <andy.koppe@gmail.com> writes:
 >
 >> There are a number of uses of designated initializers already, so
 >> hopefully compound literals aren't too much of an extra challenge.
 >
 > I do not see how one leads to the other here.  I'd prefer not to see
 > use of a new construct we do not currently use mixed in a new code,
 > even if it is mentioned in the proposed log message.

Okay.

Would this style be acceptable to fulfil Glen's request to name the
fields?

	case 'D':
		{
			const struct decoration_options opts = {
				.prefix = "",
				.suffix = ""
			};

			format_decorations(sb, commit, c->auto_color, &opts);
		}
		return 1;

Andy
Junio C Hamano Aug. 12, 2023, 1:16 a.m. UTC | #10
Andy Koppe <andy.koppe@gmail.com> writes:

> On 11/08/2023 21:38, Junio C Hamano wrote:
>> Andy Koppe <andy.koppe@gmail.com> writes:
>>
>>> There are a number of uses of designated initializers already, so
>>> hopefully compound literals aren't too much of an extra challenge.
>>
>> I do not see how one leads to the other here.  I'd prefer not to see
>> use of a new construct we do not currently use mixed in a new code,
>> even if it is mentioned in the proposed log message.
>
> Okay.
>
> Would this style be acceptable to fulfil Glen's request to name the
> fields?
>
> 	case 'D':
> 		{
> 			const struct decoration_options opts = {
> 				.prefix = "",
> 				.suffix = ""
> 			};
>
> 			format_decorations(sb, commit, c->auto_color, &opts);
> 		}
> 		return 1;
>
> Andy

Sounds good to me.
Junio C Hamano Aug. 15, 2023, 6:13 p.m. UTC | #11
Andy Koppe <andy.koppe@gmail.com> writes:

> Done. I think I had started off main, wrongly assuming that it's the
> same as master.

If there is 'main' that is different from 'master', that sounds like
a problem to me.  This project predates the newer convention that
allows the primary branch to be named 'main', but many new folks of
course expect to see 'main', so while my primary working areas all
call the primary branch 'master', it is pushed out to both names.

Or at least I thought I arranged that to happen.

Thanks.
Andy Koppe Aug. 15, 2023, 6:28 p.m. UTC | #12
On 15/08/2023 19:13, Junio C Hamano wrote:
> If there is 'main' that is different from 'master', that sounds like
> a problem to me.  This project predates the newer convention that
> allows the primary branch to be named 'main', but many new folks of
> course expect to see 'main', so while my primary working areas all
> call the primary branch 'master', it is pushed out to both names.
> 
> Or at least I thought I arranged that to happen.

See [1], where main currently is at v2.41.0.

Regards,
Andy

[1] https://github.com/git/git/tree/main
Junio C Hamano Aug. 15, 2023, 7:01 p.m. UTC | #13
Andy Koppe <andy.koppe@gmail.com> writes:

> On 15/08/2023 19:13, Junio C Hamano wrote:
>> If there is 'main' that is different from 'master', that sounds like
>> a problem to me.  This project predates the newer convention that
>> allows the primary branch to be named 'main', but many new folks of
>> course expect to see 'main', so while my primary working areas all
>> call the primary branch 'master', it is pushed out to both names.
>> Or at least I thought I arranged that to happen.
>
> See [1], where main currently is at v2.41.0.
>
> Regards,
> Andy
>
> [1] https://github.com/git/git/tree/main

Ah, that one.  The CI job is unfortunately attached to that tree and
updating 'master' and 'main' with the same commit at the same time
wastes CI cycles, so I had to tentatively stop updating it.

It used to be that 'main' was set to lag behind 'master' by 24 hours
or so to prevent the problem---CI notices that the commit updated
'main' has been already dealt with 24 hours ago at 'master' and
refrains from wasting time on it.  But resurrecting it would still
make folks confused about how 'main' is different from 'master'.
Perhaps it is a good time to remove stale 'main' and keep only
'master' there?
Andy Koppe Aug. 15, 2023, 7:29 p.m. UTC | #14
On 15/08/2023 20:01, Junio C Hamano wrote:
>> See [1], where main currently is at v2.41.0.
>>
>> [1] https://github.com/git/git/tree/main
> 
> Ah, that one.  The CI job is unfortunately attached to that tree and
> updating 'master' and 'main' with the same commit at the same time
> wastes CI cycles, so I had to tentatively stop updating it.
> 
> It used to be that 'main' was set to lag behind 'master' by 24 hours
> or so to prevent the problem---CI notices that the commit updated
> 'main' has been already dealt with 24 hours ago at 'master' and
> refrains from wasting time on it.  But resurrecting it would still
> make folks confused about how 'main' is different from 'master'.
> Perhaps it is a good time to remove stale 'main' and keep only
> 'master' there?

An alternative might be to exclude one of the branches in the workflow 
file, as per [1].

Andy

[1] 
https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#example-excluding-branches
Taylor Blau Aug. 15, 2023, 10:16 p.m. UTC | #15
On Tue, Aug 15, 2023 at 08:29:43PM +0100, Andy Koppe wrote:
> On 15/08/2023 20:01, Junio C Hamano wrote:
> > > See [1], where main currently is at v2.41.0.
> > >
> > > [1] https://github.com/git/git/tree/main
> >
> > Ah, that one.  The CI job is unfortunately attached to that tree and
> > updating 'master' and 'main' with the same commit at the same time
> > wastes CI cycles, so I had to tentatively stop updating it.
> >
> > It used to be that 'main' was set to lag behind 'master' by 24 hours
> > or so to prevent the problem---CI notices that the commit updated
> > 'main' has been already dealt with 24 hours ago at 'master' and
> > refrains from wasting time on it.  But resurrecting it would still
> > make folks confused about how 'main' is different from 'master'.
> > Perhaps it is a good time to remove stale 'main' and keep only
> > 'master' there?
>
> An alternative might be to exclude one of the branches in the workflow file,
> as per [1].

I think that this should be relatively straightforward to do, and would
be preferable to dropping 'main'.

Here's an (untested) patch that should do the trick:

--- >8 ---
diff --git a/.github/workflows/check-whitespace.yml b/.github/workflows/check-whitespace.yml
index a58e2dc8ad..764f46b21f 100644
--- a/.github/workflows/check-whitespace.yml
+++ b/.github/workflows/check-whitespace.yml
@@ -8,6 +8,8 @@ name: check-whitespace
 on:
   pull_request:
     types: [opened, synchronize]
+    branches_ignore:
+      - main

 # Avoid unnecessary builds. Unlike the main CI jobs, these are not
 # ci-configurable (but could be).
diff --git a/.github/workflows/l10n.yml b/.github/workflows/l10n.yml
index 6c3849658a..f6767a73d2 100644
--- a/.github/workflows/l10n.yml
+++ b/.github/workflows/l10n.yml
@@ -1,6 +1,12 @@
 name: git-l10n

-on: [push, pull_request_target]
+on:
+  push:
+    branches_ignore:
+      - main
+  pull_request_target:
+    branches_ignore:
+      - main

 # Avoid unnecessary builds. Unlike the main CI jobs, these are not
 # ci-configurable (but could be).
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 079645b776..eaaf6a9151 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -1,6 +1,12 @@
 name: CI

-on: [push, pull_request]
+on:
+  push:
+    branches-ignore:
+      - main
+  pull_request:
+    branches-ignore:
+      - main

 env:
   DEVELOPER: 1
--- 8< ---

> [1] https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#example-excluding-branches

Thanks,
Taylor
Jeff King Aug. 16, 2023, 2:24 a.m. UTC | #16
On Tue, Aug 15, 2023 at 06:16:29PM -0400, Taylor Blau wrote:

> > An alternative might be to exclude one of the branches in the workflow file,
> > as per [1].
> 
> I think that this should be relatively straightforward to do, and would
> be preferable to dropping 'main'.

That was my inclination, too, though I wonder if that might cause
hassles for Git for Windows:

  $ git ls-remote --symref https://github.com/git-for-windows/git HEAD
  ref: refs/heads/main	HEAD
  a67b85bf88ddbccae96714edb64d741ddfc3a1c9	HEAD

I'm not sure how big a deal it would be in practice. Obviously they
carry patches that are not in upstream git and could adjust the file
themselves that way. But it might introduce extra friction, and in my
experience changes to "meta" files like this can be a hassle, because
you often want them independently on every branch (though in theory this
one only matters for the "main" branch itself).

So I won't say it's obviously a bad idea, but it might bear some
thinking on what the ramifications would be for downstream.

-Peff
Randall S. Becker Aug. 16, 2023, 1:30 p.m. UTC | #17
On Tuesday, August 15, 2023 10:24 PM, Jeff King wrote:
>On Tue, Aug 15, 2023 at 06:16:29PM -0400, Taylor Blau wrote:
>
>> > An alternative might be to exclude one of the branches in the
>> > workflow file, as per [1].
>>
>> I think that this should be relatively straightforward to do, and
>> would be preferable to dropping 'main'.
>
>That was my inclination, too, though I wonder if that might cause hassles for Git for
>Windows:
>
>  $ git ls-remote --symref https://github.com/git-for-windows/git HEAD
>  ref: refs/heads/main	HEAD
>  a67b85bf88ddbccae96714edb64d741ddfc3a1c9	HEAD
>
>I'm not sure how big a deal it would be in practice. Obviously they carry patches that
>are not in upstream git and could adjust the file themselves that way. But it might
>introduce extra friction, and in my experience changes to "meta" files like this can be
>a hassle, because you often want them independently on every branch (though in
>theory this one only matters for the "main" branch itself).
>
>So I won't say it's obviously a bad idea, but it might bear some thinking on what the
>ramifications would be for downstream.

Would it not be more convenient just to add a GitHub action that set main = master for each push?
Junio C Hamano Aug. 18, 2023, 12:35 a.m. UTC | #18
<rsbecker@nexbridge.com> writes:

> Would it not be more convenient just to add a GitHub action that
> set main = master for each push?

If "my private working area calls the primary integration branch
'master', but for publishing repositories, I have to push it twice,
once to 'master' and then to 'main'" were the problem, the solution
I would rather want to see implemented is to an ability for the
repository owners to set a symref that makes 'main' refer to
'master', so that I do not have to worry about the aliasing.  But it
is not a problem (the push refspec can be set up to send the same
commit to two different branches just fine).

In any case, I am not sure if it would solve the problem being
discussed: when CI runner sees branches updated to commit that
hasn't been worked on, a new job is created to work on that commit,
and updating two branches with the same commit at the same time
unfortunately means two independent CI jobs work on the same commit
in parallel.  The 'lagging behind by 24 hours' hack I mentioned
earlier was one way to work it around, but it would confuse folks.

I'd really prefer not to special case 'main' (or 'master' for that
matter), primarily because some downstreams rely more heavily on
'main' as Peff pointed out, but also because the problem is not
'master' vs 'main'.  If 'next' happens to become empty soon after a
new cycle starts and points at the same commit as 'master', we will
see the same waste of cycles between 'master' and 'next'.

Thanks.
Johannes Schindelin Aug. 21, 2023, 2:56 p.m. UTC | #19
Hi Junio,

On Thu, 17 Aug 2023, Junio C Hamano wrote:

> <rsbecker@nexbridge.com> writes:
>
> [...] when CI runner sees branches updated to commit that hasn't been
> worked on, a new job is created to work on that commit, and updating two
> branches with the same commit at the same time unfortunately means two
> independent CI jobs work on the same commit in parallel.

My understanding is that the recommended way to handle this via the
`concurrency` key [*1*]. That is, if we changed

    concurrency:
      group: windows-build-${{ github.ref }}
      cancel-in-progress: ${{ needs.ci-config.outputs.skip_concurrent == 'yes' }}

to

    concurrency:
      group: windows-build-${{ github.sha }}

then pushing both `master` and `next` pointing at the same commit would
start only one of the workflow runs immediately, keeping the second one
pending until the first run is done. If the first run succeeds, the second
run will pick up that status and avoid running everything all over again,
via `skip-if-redundant`.

Ciao,
Johannes

Footnote *1*:
https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#concurrency
Junio C Hamano Aug. 21, 2023, 4:17 p.m. UTC | #20
Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:

> My understanding is that the recommended way to handle this via the
> `concurrency` key [*1*]. That is, if we changed
>
>     concurrency:
>       group: windows-build-${{ github.ref }}
>       cancel-in-progress: ${{ needs.ci-config.outputs.skip_concurrent == 'yes' }}
>
> to
>
>     concurrency:
>       group: windows-build-${{ github.sha }}
>
> then pushing both `master` and `next` pointing at the same commit would
> start only one of the workflow runs immediately, keeping the second one
> pending until the first run is done.

Perfect.  It is much better than pushing 'master@{24.hours.ago} to
'main' which was what I used to do avoid the problem between these
two, which I stopped doing because it did not work well.

> If the first run succeeds, the second
> run will pick up that status and avoid running everything all over again,
> via `skip-if-redundant`.

Nice.  I understand that this would kick in regardless, but the
right use of the concurrency key would make it far more effective.

Very nice.
diff mbox series

Patch

diff --git a/Documentation/pretty-formats.txt b/Documentation/pretty-formats.txt
index 3b71334459..c08aba15af 100644
--- a/Documentation/pretty-formats.txt
+++ b/Documentation/pretty-formats.txt
@@ -222,7 +222,22 @@  The placeholders are:
 	linkgit:git-rev-list[1])
 '%d':: ref names, like the --decorate option of linkgit:git-log[1]
 '%D':: ref names without the " (", ")" wrapping.
-'%(describe[:options])':: human-readable name, like
+'%(decorate[:<options>])':: ref names with custom decorations.
+			  The `decorate` string may be followed by a colon
+			  and zero or more comma-separated options.
+			  Option values may contain literal formatting codes.
+			  These must be used for commas (`%x2C`) and closing
+			  parentheses (`%x29`), due to their role in the option
+			  syntax.
++
+** 'prefix=<value>': Shown before the list of ref names.  Defaults to " (".
+** 'suffix=<value>': Shown after the list of ref names.  Defaults to ")".
+** 'separator=<value>': Shown between ref names.  Defaults to ", ".
+** 'arrow=<value>': Shown between HEAD and the branch it points to, if any.
+		    Defaults to " \-> ".
+** 'tag=<value>': Shown before tag names. Defaults to "tag: ".
+
+'%(describe[:<options>])':: human-readable name, like
 			  linkgit:git-describe[1]; empty string for
 			  undescribable commits.  The `describe` string
 			  may be followed by a colon and zero or more
@@ -281,7 +296,7 @@  endif::git-rev-list[]
 '%gE':: reflog identity email (respecting .mailmap, see
 	linkgit:git-shortlog[1] or linkgit:git-blame[1])
 '%gs':: reflog subject
-'%(trailers[:options])':: display the trailers of the body as
+'%(trailers[:<options>])':: display the trailers of the body as
 			  interpreted by
 			  linkgit:git-interpret-trailers[1]. The
 			  `trailers` string may be followed by a colon
diff --git a/log-tree.c b/log-tree.c
index f4b22a60cc..4b46884ef6 100644
--- a/log-tree.c
+++ b/log-tree.c
@@ -301,27 +301,34 @@  static void show_name(struct strbuf *sb, const struct name_decoration *decoratio
 
 /*
  * The caller makes sure there is no funny color before calling.
- * format_decorations_extended makes sure the same after return.
+ * format_decorations ensures the same after return.
  */
-void format_decorations_extended(struct strbuf *sb,
+void format_decorations(struct strbuf *sb,
 			const struct commit *commit,
 			int use_color,
-			const char *prefix,
-			const char *separator,
-			const char *suffix)
+			const struct decoration_options *opts)
 {
-	const struct name_decoration *decoration;
-	const struct name_decoration *current_and_HEAD;
-	const char *color_commit =
-		diff_get_color(use_color, DIFF_COMMIT);
-	const char *color_reset =
-		decorate_get_color(use_color, DECORATION_NONE);
+	const char *color_commit, *color_reset;
+	const char *prefix, *suffix, *separator, *arrow, *tag;
+
+	const struct name_decoration *current_and_HEAD;
+	const struct name_decoration *decoration =
+		get_name_decoration(&commit->object);
 
-	decoration = get_name_decoration(&commit->object);
 	if (!decoration)
 		return;
 
+	color_commit = diff_get_color(use_color, DIFF_COMMIT);
+	color_reset = decorate_get_color(use_color, DECORATION_NONE);
+
+	prefix = (opts && opts->prefix) ? opts->prefix : " (";
+	suffix = (opts && opts->suffix) ? opts->suffix : ")";
+	separator = (opts && opts->separator) ? opts->separator : ", ";
+	arrow = (opts && opts->arrow) ? opts->arrow : " -> ";
+	tag = (opts && opts->tag) ? opts->tag : "tag: ";
+
 	current_and_HEAD = current_pointed_by_HEAD(decoration);
+
 	while (decoration) {
 		/*
 		 * When both current and HEAD are there, only
@@ -329,20 +336,29 @@  void format_decorations_extended(struct strbuf *sb,
 		 * appeared, skipping the entry for current.
 		 */
 		if (decoration != current_and_HEAD) {
-			strbuf_addstr(sb, color_commit);
-			strbuf_addstr(sb, prefix);
-			strbuf_addstr(sb, color_reset);
-			strbuf_addstr(sb, decorate_get_color(use_color, decoration->type));
-			if (decoration->type == DECORATION_REF_TAG)
-				strbuf_addstr(sb, "tag: ");
+			const char *color =
+				decorate_get_color(use_color, decoration->type);
 
+			if (*prefix) {
+				strbuf_addstr(sb, color_commit);
+				strbuf_addstr(sb, prefix);
+				strbuf_addstr(sb, color_reset);
+			}
+
+			if (*tag && decoration->type == DECORATION_REF_TAG) {
+				strbuf_addstr(sb, color);
+				strbuf_addstr(sb, tag);
+				strbuf_addstr(sb, color_reset);
+			}
+			strbuf_addstr(sb, color);
 			show_name(sb, decoration);
 
-			if (current_and_HEAD &&
+			if (*arrow && current_and_HEAD &&
 			    decoration->type == DECORATION_REF_HEAD) {
-				strbuf_addstr(sb, " -> ");
+				strbuf_addstr(sb, arrow);
 				strbuf_addstr(sb, color_reset);
-				strbuf_addstr(sb, decorate_get_color(use_color, current_and_HEAD->type));
+				strbuf_addstr(sb, decorate_get_color(
+					use_color, current_and_HEAD->type));
 				show_name(sb, current_and_HEAD);
 			}
 			strbuf_addstr(sb, color_reset);
@@ -351,9 +367,12 @@  void format_decorations_extended(struct strbuf *sb,
 		}
 		decoration = decoration->next;
 	}
-	strbuf_addstr(sb, color_commit);
-	strbuf_addstr(sb, suffix);
-	strbuf_addstr(sb, color_reset);
+
+	if (*suffix) {
+		strbuf_addstr(sb, color_commit);
+		strbuf_addstr(sb, suffix);
+		strbuf_addstr(sb, color_reset);
+	}
 }
 
 void show_decorations(struct rev_info *opt, struct commit *commit)
@@ -368,7 +387,7 @@  void show_decorations(struct rev_info *opt, struct commit *commit)
 	}
 	if (!opt->show_decorations)
 		return;
-	format_decorations(&sb, commit, opt->diffopt.use_color);
+	format_decorations(&sb, commit, opt->diffopt.use_color, NULL);
 	fputs(sb.buf, opt->diffopt.file);
 	strbuf_release(&sb);
 }
diff --git a/log-tree.h b/log-tree.h
index e7e4641cf8..39ab06a3ca 100644
--- a/log-tree.h
+++ b/log-tree.h
@@ -13,17 +13,20 @@  struct decoration_filter {
 	struct string_list *exclude_ref_config_pattern;
 };
 
+struct decoration_options {
+	char *prefix;
+	char *suffix;
+	char *separator;
+	char *arrow;
+	char *tag;
+};
+
 int parse_decorate_color_config(const char *var, const char *slot_name, const char *value);
 int log_tree_diff_flush(struct rev_info *);
 int log_tree_commit(struct rev_info *, struct commit *);
 void show_log(struct rev_info *opt);
-void format_decorations_extended(struct strbuf *sb, const struct commit *commit,
-			     int use_color,
-			     const char *prefix,
-			     const char *separator,
-			     const char *suffix);
-#define format_decorations(strbuf, commit, color) \
-			     format_decorations_extended((strbuf), (commit), (color), " (", ", ", ")")
+void format_decorations(struct strbuf *sb, const struct commit *commit,
+			int use_color, const struct decoration_options *opts);
 void show_decorations(struct rev_info *opt, struct commit *commit);
 void log_write_email_headers(struct rev_info *opt, struct commit *commit,
 			     const char **extra_headers_p,
diff --git a/pretty.c b/pretty.c
index 0bb938021b..a59b7f0dbc 100644
--- a/pretty.c
+++ b/pretty.c
@@ -1373,6 +1373,46 @@  static size_t parse_describe_args(const char *start, struct strvec *args)
 	return arg - start;
 }
 
+
+static int parse_decoration_option(const char **arg,
+				   const char *name,
+				   char **opt)
+{
+	const char *argval;
+	size_t arglen;
+
+	if (match_placeholder_arg_value(*arg, name, arg, &argval, &arglen)) {
+		char *val = xstrndup(argval, arglen);
+		struct strbuf sb = STRBUF_INIT;
+
+		strbuf_expand(&sb, val, strbuf_expand_literal_cb, NULL);
+		free(val);
+		*opt = strbuf_detach(&sb, NULL);
+		return 1;
+	}
+	return 0;
+}
+
+static void parse_decoration_options(const char **arg,
+				     struct decoration_options *opts)
+{
+	while (parse_decoration_option(arg, "prefix", &opts->prefix) ||
+	       parse_decoration_option(arg, "suffix", &opts->suffix) ||
+	       parse_decoration_option(arg, "separator", &opts->separator) ||
+	       parse_decoration_option(arg, "arrow", &opts->arrow) ||
+	       parse_decoration_option(arg, "tag", &opts->tag))
+		;
+}
+
+static void free_decoration_options(const struct decoration_options *opts)
+{
+	free(opts->prefix);
+	free(opts->suffix);
+	free(opts->separator);
+	free(opts->arrow);
+	free(opts->tag);
+}
+
 static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */
 				const char *placeholder,
 				void *context)
@@ -1526,10 +1566,11 @@  static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */
 		strbuf_addstr(sb, get_revision_mark(NULL, commit));
 		return 1;
 	case 'd':
-		format_decorations(sb, commit, c->auto_color);
+		format_decorations(sb, commit, c->auto_color, NULL);
 		return 1;
 	case 'D':
-		format_decorations_extended(sb, commit, c->auto_color, "", ", ", "");
+		format_decorations(sb, commit, c->auto_color,
+				   &(struct decoration_options){"", ""});
 		return 1;
 	case 'S':		/* tag/branch like --source */
 		if (!(c->pretty_ctx->rev && c->pretty_ctx->rev->sources))
@@ -1627,6 +1668,23 @@  static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */
 		return 2;
 	}
 
+	if (skip_prefix(placeholder, "(decorate", &arg)) {
+		struct decoration_options opts = { NULL };
+		size_t ret = 0;
+
+		if (*arg == ':') {
+			arg++;
+			parse_decoration_options(&arg, &opts);
+		}
+		if (*arg == ')') {
+			format_decorations(sb, commit, c->auto_color, &opts);
+			ret = arg - placeholder + 1;
+		}
+
+		free_decoration_options(&opts);
+		return ret;
+	}
+
 	/* For the rest we have to parse the commit header. */
 	if (!c->commit_header_parsed) {
 		msg = c->message =
diff --git a/t/t4205-log-pretty-formats.sh b/t/t4205-log-pretty-formats.sh
index 4cf8a77667..5ea937648a 100755
--- a/t/t4205-log-pretty-formats.sh
+++ b/t/t4205-log-pretty-formats.sh
@@ -576,6 +576,27 @@  test_expect_success 'clean log decoration' '
 	test_cmp expected actual1
 '
 
+test_expect_success 'pretty format %decorate' '
+	git checkout -b foo &&
+	git commit --allow-empty -m "new commit" &&
+	git tag bar &&
+	git branch qux &&
+	echo " (HEAD -> foo, tag: bar, qux)" >expect1 &&
+	git log --format="%(decorate)" -1 >actual1 &&
+	test_cmp expect1 actual1 &&
+	echo "HEAD -> foo, tag: bar, qux" >expect2 &&
+	git log --format="%(decorate:prefix=,suffix=)" -1 >actual2 &&
+	test_cmp expect2 actual2 &&
+	echo "HEAD->foo bar qux" >expect3 &&
+	git log --format="%(decorate:prefix=,suffix=,separator= ,arrow=->,tag=)" \
+		-1 >actual3 &&
+	test_cmp expect3 actual3 &&
+	echo "[HEAD,foo,bar,qux]" >expect4 &&
+	git log --format="%(decorate:prefix=[,suffix=],separator=%x2C,arrow=%x2C,tag=)" \
+		-1 >actual4 &&
+	test_cmp expect4 actual4
+'
+
 cat >trailers <<EOF
 Signed-off-by: A U Thor <author@example.com>
 Acked-by: A U Thor <author@example.com>
diff --git a/t/t4207-log-decoration-colors.sh b/t/t4207-log-decoration-colors.sh
index ded33a82e2..3a4eedc494 100755
--- a/t/t4207-log-decoration-colors.sh
+++ b/t/t4207-log-decoration-colors.sh
@@ -55,13 +55,15 @@  test_expect_success 'commit decorations colored correctly' '
 	cat >expect <<-EOF &&
 	${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_HEAD}HEAD -> \
 ${c_reset}${c_branch}main${c_reset}${c_commit}, \
-${c_reset}${c_tag}tag: v1.0${c_reset}${c_commit}, \
-${c_reset}${c_tag}tag: B${c_reset}${c_commit})${c_reset} B
-${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_tag}tag: A1${c_reset}${c_commit}, \
+${c_reset}${c_tag}tag: ${c_reset}${c_tag}v1.0${c_reset}${c_commit}, \
+${c_reset}${c_tag}tag: ${c_reset}${c_tag}B${c_reset}${c_commit})${c_reset} B
+${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\
+${c_tag}tag: ${c_reset}${c_tag}A1${c_reset}${c_commit}, \
 ${c_reset}${c_remoteBranch}other/main${c_reset}${c_commit})${c_reset} A1
-	${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_stash}refs/stash${c_reset}${c_commit})${c_reset} \
-On main: Changes to A.t
-	${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_tag}tag: A${c_reset}${c_commit})${c_reset} A
+	${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\
+${c_stash}refs/stash${c_reset}${c_commit})${c_reset} On main: Changes to A.t
+	${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\
+${c_tag}tag: ${c_reset}${c_tag}A${c_reset}${c_commit})${c_reset} A
 	EOF
 
 	git log --first-parent --no-abbrev --decorate --oneline --color=always --all >actual &&
@@ -78,10 +80,12 @@  test_expect_success 'test coloring with replace-objects' '
 	cat >expect <<-EOF &&
 	${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_HEAD}HEAD -> \
 ${c_reset}${c_branch}main${c_reset}${c_commit}, \
-${c_reset}${c_tag}tag: D${c_reset}${c_commit})${c_reset} D
-	${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_tag}tag: C${c_reset}${c_commit}, \
+${c_reset}${c_tag}tag: ${c_reset}${c_tag}D${c_reset}${c_commit})${c_reset} D
+	${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\
+${c_tag}tag: ${c_reset}${c_tag}C${c_reset}${c_commit}, \
 ${c_reset}${c_grafted}replaced${c_reset}${c_commit})${c_reset} B
-	${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_tag}tag: A${c_reset}${c_commit})${c_reset} A
+	${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\
+${c_tag}tag: ${c_reset}${c_tag}A${c_reset}${c_commit})${c_reset} A
 EOF
 
 	git log --first-parent --no-abbrev --decorate --oneline --color=always HEAD >actual &&
@@ -102,11 +106,13 @@  test_expect_success 'test coloring with grafted commit' '
 	cat >expect <<-EOF &&
 	${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_HEAD}HEAD -> \
 ${c_reset}${c_branch}main${c_reset}${c_commit}, \
-${c_reset}${c_tag}tag: D${c_reset}${c_commit}, \
+${c_reset}${c_tag}tag: ${c_reset}${c_tag}D${c_reset}${c_commit}, \
 ${c_reset}${c_grafted}replaced${c_reset}${c_commit})${c_reset} D
-	${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_tag}tag: v1.0${c_reset}${c_commit}, \
-${c_reset}${c_tag}tag: B${c_reset}${c_commit})${c_reset} B
-	${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_tag}tag: A${c_reset}${c_commit})${c_reset} A
+	${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\
+${c_tag}tag: ${c_reset}${c_tag}v1.0${c_reset}${c_commit}, \
+${c_reset}${c_tag}tag: ${c_reset}${c_tag}B${c_reset}${c_commit})${c_reset} B
+	${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\
+${c_tag}tag: ${c_reset}${c_tag}A${c_reset}${c_commit})${c_reset} A
 	EOF
 
 	git log --first-parent --no-abbrev --decorate --oneline --color=always HEAD >actual &&