{"thread":{"id":"59990","subject":"[PATCH] pretty: add %(decorate[:<options>]) format","startedAt":"2023-07-15T10:39:02Z","lastAt":"2023-09-01T21:50:07Z","messageCount":59,"participants":["Andy Koppe","Junio C Hamano","Glen Choo","Phillip Wood","Taylor Blau","Jeff King","rsbecker@nexbridge.com","Johannes Schindelin"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"479544","messageId":"20230715103758.3862-1-andy.koppe@gmail.com","threadId":"59990","inReplyTo":null,"subject":"[PATCH] pretty: add %(decorate[:<options>]) format","fromName":"Andy Koppe","fromEmail":"andy.koppe@gmail.com","sentAt":"2023-07-15T10:37:56Z","receivedAt":"2023-07-15T10:39:02Z","isPatch":true,"sender":{"key":"andy.koppe@gmail.com","avatar":"https://avatars.githubusercontent.com/u/223411?v=4"},"body":"This lists ref names in the same way as the %d decoration format, but\nallows all the otherwise fixed strings printed around the ref names to\nbe customized, namely prefix, suffix, separator, the \"tag:\" annotation\nand the arrow used to show where HEAD points.\n\nExamples:\n- %decorate(prefix=,suffix=) removes the enclosing parentheses, like %D.\n- %decorate(prefix=,suffix=,separator=,tag=,arrow=->) produces a\n  space-separated list without wrapping, tag annotations or spaces\n  around the arrow.\n- %(decorate:prefix=[,suffix=],separator=%x2C,arrow=%x2C,tag=) produces\n  a comma-separated list enclosed in square brackets where the arrow is\n  replaced by a comma as well.\n\nAdd functions parse_decoration_option(), parse_decoration_options() and\nfree_decoration_options() to help implement the format. Test it in\nt4205-log-pretty-formats.sh and document it in pretty-formats.txt.\n\nRefactor format_decorations() to take a struct decoration_options\nargument specifying those strings, whereby NULL entries select the\ndefault. Avoid emitting color sequences for empty strings.\n\nWrap tag annotations in separate color sequences from tag names, because\notherwise tag names can end up uncolored when %w width formatting breaks\nlines between annotation and name. Amend t4207-log-decoration-colors.sh\naccordingly.\n\nSigned-off-by: Andy Koppe <andy.koppe@gmail.com>\n---\nCI: https://github.com/ak2/git/actions/runs/5561647197\n\n Documentation/pretty-formats.txt | 19 ++++++++-\n log-tree.c                       | 69 ++++++++++++++++++++------------\n log-tree.h                       | 17 ++++----\n pretty.c                         | 62 +++++++++++++++++++++++++++-\n t/t4205-log-pretty-formats.sh    | 21 ++++++++++\n t/t4207-log-decoration-colors.sh | 32 +++++++++------\n 6 files changed, 171 insertions(+), 49 deletions(-)\n\ndiff --git a/Documentation/pretty-formats.txt b/Documentation/pretty-formats.txt\nindex 3b71334459..c08aba15af 100644\n--- a/Documentation/pretty-formats.txt\n+++ b/Documentation/pretty-formats.txt\n@@ -222,7 +222,22 @@ The placeholders are:\n \tlinkgit:git-rev-list[1])\n '%d':: ref names, like the --decorate option of linkgit:git-log[1]\n '%D':: ref names without the \" (\", \")\" wrapping.\n-'%(describe[:options])':: human-readable name, like\n+'%(decorate[:<options>])':: ref names with custom decorations.\n+\t\t\t  The `decorate` string may be followed by a colon\n+\t\t\t  and zero or more comma-separated options.\n+\t\t\t  Option values may contain literal formatting codes.\n+\t\t\t  These must be used for commas (`%x2C`) and closing\n+\t\t\t  parentheses (`%x29`), due to their role in the option\n+\t\t\t  syntax.\n++\n+** 'prefix=<value>': Shown before the list of ref names.  Defaults to \" (\".\n+** 'suffix=<value>': Shown after the list of ref names.  Defaults to \")\".\n+** 'separator=<value>': Shown between ref names.  Defaults to \", \".\n+** 'arrow=<value>': Shown between HEAD and the branch it points to, if any.\n+\t\t    Defaults to \" \\-> \".\n+** 'tag=<value>': Shown before tag names. Defaults to \"tag: \".\n+\n+'%(describe[:<options>])':: human-readable name, like\n \t\t\t  linkgit:git-describe[1]; empty string for\n \t\t\t  undescribable commits.  The `describe` string\n \t\t\t  may be followed by a colon and zero or more\n@@ -281,7 +296,7 @@ endif::git-rev-list[]\n '%gE':: reflog identity email (respecting .mailmap, see\n \tlinkgit:git-shortlog[1] or linkgit:git-blame[1])\n '%gs':: reflog subject\n-'%(trailers[:options])':: display the trailers of the body as\n+'%(trailers[:<options>])':: display the trailers of the body as\n \t\t\t  interpreted by\n \t\t\t  linkgit:git-interpret-trailers[1]. The\n \t\t\t  `trailers` string may be followed by a colon\ndiff --git a/log-tree.c b/log-tree.c\nindex f4b22a60cc..4b46884ef6 100644\n--- a/log-tree.c\n+++ b/log-tree.c\n@@ -301,27 +301,34 @@ static void show_name(struct strbuf *sb, const struct name_decoration *decoratio\n \n /*\n  * The caller makes sure there is no funny color before calling.\n- * format_decorations_extended makes sure the same after return.\n+ * format_decorations ensures the same after return.\n  */\n-void format_decorations_extended(struct strbuf *sb,\n+void format_decorations(struct strbuf *sb,\n \t\t\tconst struct commit *commit,\n \t\t\tint use_color,\n-\t\t\tconst char *prefix,\n-\t\t\tconst char *separator,\n-\t\t\tconst char *suffix)\n+\t\t\tconst struct decoration_options *opts)\n {\n-\tconst struct name_decoration *decoration;\n-\tconst struct name_decoration *current_and_HEAD;\n-\tconst char *color_commit =\n-\t\tdiff_get_color(use_color, DIFF_COMMIT);\n-\tconst char *color_reset =\n-\t\tdecorate_get_color(use_color, DECORATION_NONE);\n+\tconst char *color_commit, *color_reset;\n+\tconst char *prefix, *suffix, *separator, *arrow, *tag;\n+\n+\tconst struct name_decoration *current_and_HEAD;\n+\tconst struct name_decoration *decoration =\n+\t\tget_name_decoration(&commit->object);\n \n-\tdecoration = get_name_decoration(&commit->object);\n \tif (!decoration)\n \t\treturn;\n \n+\tcolor_commit = diff_get_color(use_color, DIFF_COMMIT);\n+\tcolor_reset = decorate_get_color(use_color, DECORATION_NONE);\n+\n+\tprefix = (opts && opts->prefix) ? opts->prefix : \" (\";\n+\tsuffix = (opts && opts->suffix) ? opts->suffix : \")\";\n+\tseparator = (opts && opts->separator) ? opts->separator : \", \";\n+\tarrow = (opts && opts->arrow) ? opts->arrow : \" -> \";\n+\ttag = (opts && opts->tag) ? opts->tag : \"tag: \";\n+\n \tcurrent_and_HEAD = current_pointed_by_HEAD(decoration);\n+\n \twhile (decoration) {\n \t\t/*\n \t\t * When both current and HEAD are there, only\n@@ -329,20 +336,29 @@ void format_decorations_extended(struct strbuf *sb,\n \t\t * appeared, skipping the entry for current.\n \t\t */\n \t\tif (decoration != current_and_HEAD) {\n-\t\t\tstrbuf_addstr(sb, color_commit);\n-\t\t\tstrbuf_addstr(sb, prefix);\n-\t\t\tstrbuf_addstr(sb, color_reset);\n-\t\t\tstrbuf_addstr(sb, decorate_get_color(use_color, decoration->type));\n-\t\t\tif (decoration->type == DECORATION_REF_TAG)\n-\t\t\t\tstrbuf_addstr(sb, \"tag: \");\n+\t\t\tconst char *color =\n+\t\t\t\tdecorate_get_color(use_color, decoration->type);\n \n+\t\t\tif (*prefix) {\n+\t\t\t\tstrbuf_addstr(sb, color_commit);\n+\t\t\t\tstrbuf_addstr(sb, prefix);\n+\t\t\t\tstrbuf_addstr(sb, color_reset);\n+\t\t\t}\n+\n+\t\t\tif (*tag && decoration->type == DECORATION_REF_TAG) {\n+\t\t\t\tstrbuf_addstr(sb, color);\n+\t\t\t\tstrbuf_addstr(sb, tag);\n+\t\t\t\tstrbuf_addstr(sb, color_reset);\n+\t\t\t}\n+\t\t\tstrbuf_addstr(sb, color);\n \t\t\tshow_name(sb, decoration);\n \n-\t\t\tif (current_and_HEAD &&\n+\t\t\tif (*arrow && current_and_HEAD &&\n \t\t\t    decoration->type == DECORATION_REF_HEAD) {\n-\t\t\t\tstrbuf_addstr(sb, \" -> \");\n+\t\t\t\tstrbuf_addstr(sb, arrow);\n \t\t\t\tstrbuf_addstr(sb, color_reset);\n-\t\t\t\tstrbuf_addstr(sb, decorate_get_color(use_color, current_and_HEAD->type));\n+\t\t\t\tstrbuf_addstr(sb, decorate_get_color(\n+\t\t\t\t\tuse_color, current_and_HEAD->type));\n \t\t\t\tshow_name(sb, current_and_HEAD);\n \t\t\t}\n \t\t\tstrbuf_addstr(sb, color_reset);\n@@ -351,9 +367,12 @@ void format_decorations_extended(struct strbuf *sb,\n \t\t}\n \t\tdecoration = decoration->next;\n \t}\n-\tstrbuf_addstr(sb, color_commit);\n-\tstrbuf_addstr(sb, suffix);\n-\tstrbuf_addstr(sb, color_reset);\n+\n+\tif (*suffix) {\n+\t\tstrbuf_addstr(sb, color_commit);\n+\t\tstrbuf_addstr(sb, suffix);\n+\t\tstrbuf_addstr(sb, color_reset);\n+\t}\n }\n \n void show_decorations(struct rev_info *opt, struct commit *commit)\n@@ -368,7 +387,7 @@ void show_decorations(struct rev_info *opt, struct commit *commit)\n \t}\n \tif (!opt->show_decorations)\n \t\treturn;\n-\tformat_decorations(&sb, commit, opt->diffopt.use_color);\n+\tformat_decorations(&sb, commit, opt->diffopt.use_color, NULL);\n \tfputs(sb.buf, opt->diffopt.file);\n \tstrbuf_release(&sb);\n }\ndiff --git a/log-tree.h b/log-tree.h\nindex e7e4641cf8..39ab06a3ca 100644\n--- a/log-tree.h\n+++ b/log-tree.h\n@@ -13,17 +13,20 @@ struct decoration_filter {\n \tstruct string_list *exclude_ref_config_pattern;\n };\n \n+struct decoration_options {\n+\tchar *prefix;\n+\tchar *suffix;\n+\tchar *separator;\n+\tchar *arrow;\n+\tchar *tag;\n+};\n+\n int parse_decorate_color_config(const char *var, const char *slot_name, const char *value);\n int log_tree_diff_flush(struct rev_info *);\n int log_tree_commit(struct rev_info *, struct commit *);\n void show_log(struct rev_info *opt);\n-void format_decorations_extended(struct strbuf *sb, const struct commit *commit,\n-\t\t\t     int use_color,\n-\t\t\t     const char *prefix,\n-\t\t\t     const char *separator,\n-\t\t\t     const char *suffix);\n-#define format_decorations(strbuf, commit, color) \\\n-\t\t\t     format_decorations_extended((strbuf), (commit), (color), \" (\", \", \", \")\")\n+void format_decorations(struct strbuf *sb, const struct commit *commit,\n+\t\t\tint use_color, const struct decoration_options *opts);\n void show_decorations(struct rev_info *opt, struct commit *commit);\n void log_write_email_headers(struct rev_info *opt, struct commit *commit,\n \t\t\t     const char **extra_headers_p,\ndiff --git a/pretty.c b/pretty.c\nindex 0bb938021b..a59b7f0dbc 100644\n--- a/pretty.c\n+++ b/pretty.c\n@@ -1373,6 +1373,46 @@ static size_t parse_describe_args(const char *start, struct strvec *args)\n \treturn arg - start;\n }\n \n+\n+static int parse_decoration_option(const char **arg,\n+\t\t\t\t   const char *name,\n+\t\t\t\t   char **opt)\n+{\n+\tconst char *argval;\n+\tsize_t arglen;\n+\n+\tif (match_placeholder_arg_value(*arg, name, arg, &argval, &arglen)) {\n+\t\tchar *val = xstrndup(argval, arglen);\n+\t\tstruct strbuf sb = STRBUF_INIT;\n+\n+\t\tstrbuf_expand(&sb, val, strbuf_expand_literal_cb, NULL);\n+\t\tfree(val);\n+\t\t*opt = strbuf_detach(&sb, NULL);\n+\t\treturn 1;\n+\t}\n+\treturn 0;\n+}\n+\n+static void parse_decoration_options(const char **arg,\n+\t\t\t\t     struct decoration_options *opts)\n+{\n+\twhile (parse_decoration_option(arg, \"prefix\", &opts->prefix) ||\n+\t       parse_decoration_option(arg, \"suffix\", &opts->suffix) ||\n+\t       parse_decoration_option(arg, \"separator\", &opts->separator) ||\n+\t       parse_decoration_option(arg, \"arrow\", &opts->arrow) ||\n+\t       parse_decoration_option(arg, \"tag\", &opts->tag))\n+\t\t;\n+}\n+\n+static void free_decoration_options(const struct decoration_options *opts)\n+{\n+\tfree(opts->prefix);\n+\tfree(opts->suffix);\n+\tfree(opts->separator);\n+\tfree(opts->arrow);\n+\tfree(opts->tag);\n+}\n+\n static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */\n \t\t\t\tconst char *placeholder,\n \t\t\t\tvoid *context)\n@@ -1526,10 +1566,11 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */\n \t\tstrbuf_addstr(sb, get_revision_mark(NULL, commit));\n \t\treturn 1;\n \tcase 'd':\n-\t\tformat_decorations(sb, commit, c->auto_color);\n+\t\tformat_decorations(sb, commit, c->auto_color, NULL);\n \t\treturn 1;\n \tcase 'D':\n-\t\tformat_decorations_extended(sb, commit, c->auto_color, \"\", \", \", \"\");\n+\t\tformat_decorations(sb, commit, c->auto_color,\n+\t\t\t\t   &(struct decoration_options){\"\", \"\"});\n \t\treturn 1;\n \tcase 'S':\t\t/* tag/branch like --source */\n \t\tif (!(c->pretty_ctx->rev && c->pretty_ctx->rev->sources))\n@@ -1627,6 +1668,23 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */\n \t\treturn 2;\n \t}\n \n+\tif (skip_prefix(placeholder, \"(decorate\", &arg)) {\n+\t\tstruct decoration_options opts = { NULL };\n+\t\tsize_t ret = 0;\n+\n+\t\tif (*arg == ':') {\n+\t\t\targ++;\n+\t\t\tparse_decoration_options(&arg, &opts);\n+\t\t}\n+\t\tif (*arg == ')') {\n+\t\t\tformat_decorations(sb, commit, c->auto_color, &opts);\n+\t\t\tret = arg - placeholder + 1;\n+\t\t}\n+\n+\t\tfree_decoration_options(&opts);\n+\t\treturn ret;\n+\t}\n+\n \t/* For the rest we have to parse the commit header. */\n \tif (!c->commit_header_parsed) {\n \t\tmsg = c->message =\ndiff --git a/t/t4205-log-pretty-formats.sh b/t/t4205-log-pretty-formats.sh\nindex 4cf8a77667..5ea937648a 100755\n--- a/t/t4205-log-pretty-formats.sh\n+++ b/t/t4205-log-pretty-formats.sh\n@@ -576,6 +576,27 @@ test_expect_success 'clean log decoration' '\n \ttest_cmp expected actual1\n '\n \n+test_expect_success 'pretty format %decorate' '\n+\tgit checkout -b foo &&\n+\tgit commit --allow-empty -m \"new commit\" &&\n+\tgit tag bar &&\n+\tgit branch qux &&\n+\techo \" (HEAD -> foo, tag: bar, qux)\" >expect1 &&\n+\tgit log --format=\"%(decorate)\" -1 >actual1 &&\n+\ttest_cmp expect1 actual1 &&\n+\techo \"HEAD -> foo, tag: bar, qux\" >expect2 &&\n+\tgit log --format=\"%(decorate:prefix=,suffix=)\" -1 >actual2 &&\n+\ttest_cmp expect2 actual2 &&\n+\techo \"HEAD->foo bar qux\" >expect3 &&\n+\tgit log --format=\"%(decorate:prefix=,suffix=,separator= ,arrow=->,tag=)\" \\\n+\t\t-1 >actual3 &&\n+\ttest_cmp expect3 actual3 &&\n+\techo \"[HEAD,foo,bar,qux]\" >expect4 &&\n+\tgit log --format=\"%(decorate:prefix=[,suffix=],separator=%x2C,arrow=%x2C,tag=)\" \\\n+\t\t-1 >actual4 &&\n+\ttest_cmp expect4 actual4\n+'\n+\n cat >trailers <<EOF\n Signed-off-by: A U Thor <author@example.com>\n Acked-by: A U Thor <author@example.com>\ndiff --git a/t/t4207-log-decoration-colors.sh b/t/t4207-log-decoration-colors.sh\nindex ded33a82e2..3a4eedc494 100755\n--- a/t/t4207-log-decoration-colors.sh\n+++ b/t/t4207-log-decoration-colors.sh\n@@ -55,13 +55,15 @@ test_expect_success 'commit decorations colored correctly' '\n \tcat >expect <<-EOF &&\n \t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_HEAD}HEAD -> \\\n ${c_reset}${c_branch}main${c_reset}${c_commit}, \\\n-${c_reset}${c_tag}tag: v1.0${c_reset}${c_commit}, \\\n-${c_reset}${c_tag}tag: B${c_reset}${c_commit})${c_reset} B\n-${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_tag}tag: A1${c_reset}${c_commit}, \\\n+${c_reset}${c_tag}tag: ${c_reset}${c_tag}v1.0${c_reset}${c_commit}, \\\n+${c_reset}${c_tag}tag: ${c_reset}${c_tag}B${c_reset}${c_commit})${c_reset} B\n+${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\\\n+${c_tag}tag: ${c_reset}${c_tag}A1${c_reset}${c_commit}, \\\n ${c_reset}${c_remoteBranch}other/main${c_reset}${c_commit})${c_reset} A1\n-\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_stash}refs/stash${c_reset}${c_commit})${c_reset} \\\n-On main: Changes to A.t\n-\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_tag}tag: A${c_reset}${c_commit})${c_reset} A\n+\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\\\n+${c_stash}refs/stash${c_reset}${c_commit})${c_reset} On main: Changes to A.t\n+\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\\\n+${c_tag}tag: ${c_reset}${c_tag}A${c_reset}${c_commit})${c_reset} A\n \tEOF\n \n \tgit log --first-parent --no-abbrev --decorate --oneline --color=always --all >actual &&\n@@ -78,10 +80,12 @@ test_expect_success 'test coloring with replace-objects' '\n \tcat >expect <<-EOF &&\n \t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_HEAD}HEAD -> \\\n ${c_reset}${c_branch}main${c_reset}${c_commit}, \\\n-${c_reset}${c_tag}tag: D${c_reset}${c_commit})${c_reset} D\n-\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_tag}tag: C${c_reset}${c_commit}, \\\n+${c_reset}${c_tag}tag: ${c_reset}${c_tag}D${c_reset}${c_commit})${c_reset} D\n+\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\\\n+${c_tag}tag: ${c_reset}${c_tag}C${c_reset}${c_commit}, \\\n ${c_reset}${c_grafted}replaced${c_reset}${c_commit})${c_reset} B\n-\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_tag}tag: A${c_reset}${c_commit})${c_reset} A\n+\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\\\n+${c_tag}tag: ${c_reset}${c_tag}A${c_reset}${c_commit})${c_reset} A\n EOF\n \n \tgit log --first-parent --no-abbrev --decorate --oneline --color=always HEAD >actual &&\n@@ -102,11 +106,13 @@ test_expect_success 'test coloring with grafted commit' '\n \tcat >expect <<-EOF &&\n \t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_HEAD}HEAD -> \\\n ${c_reset}${c_branch}main${c_reset}${c_commit}, \\\n-${c_reset}${c_tag}tag: D${c_reset}${c_commit}, \\\n+${c_reset}${c_tag}tag: ${c_reset}${c_tag}D${c_reset}${c_commit}, \\\n ${c_reset}${c_grafted}replaced${c_reset}${c_commit})${c_reset} D\n-\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_tag}tag: v1.0${c_reset}${c_commit}, \\\n-${c_reset}${c_tag}tag: B${c_reset}${c_commit})${c_reset} B\n-\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_tag}tag: A${c_reset}${c_commit})${c_reset} A\n+\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\\\n+${c_tag}tag: ${c_reset}${c_tag}v1.0${c_reset}${c_commit}, \\\n+${c_reset}${c_tag}tag: ${c_reset}${c_tag}B${c_reset}${c_commit})${c_reset} B\n+\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\\\n+${c_tag}tag: ${c_reset}${c_tag}A${c_reset}${c_commit})${c_reset} A\n \tEOF\n \n \tgit log --first-parent --no-abbrev --decorate --oneline --color=always HEAD >actual &&\n-- \n2.41.0\n\n"},{"id":"479545","messageId":"20230715160730.4046-1-andy.koppe@gmail.com","threadId":"59990","inReplyTo":"20230715103758.3862-1-andy.koppe@gmail.com","subject":"[PATCH v2] pretty: add %(decorate[:<options>]) format","fromName":"Andy Koppe","fromEmail":"andy.koppe@gmail.com","sentAt":"2023-07-15T16:07:29Z","receivedAt":"2023-07-15T16:07:51Z","isPatch":true,"sender":{"key":"andy.koppe@gmail.com","avatar":"https://avatars.githubusercontent.com/u/223411?v=4"},"body":"This lists ref names in the same way as the %d decoration format, but\nallows all the otherwise fixed strings printed around the ref names to\nbe customized, namely prefix, suffix, separator, the \"tag:\" annotation\nand the arrow used to show where HEAD points.\n\nExamples:\n- %(decorate) is equivalent to %d.\n- %(decorate:prefix=,suffix=) is equivalent to %D.\n- %(decorate:prefix=,suffix=,separator= ,tag=,arrow=->) produces a\n  space-separated list without wrapping, tag annotations or spaces\n  around the arrow.\n- %(decorate:prefix=[,suffix=],separator=%x2C,arrow=%x2C,tag=) produces\n  a comma-separated list enclosed in square brackets where the arrow is\n  replaced by a comma as well.\n\nAdd functions parse_decoration_option(), parse_decoration_options() and\nfree_decoration_options() to help implement the format. Test it in\nt4205-log-pretty-formats.sh and document it in pretty-formats.txt.\n\nRefactor format_decorations() to take a struct decoration_options\nargument specifying those strings, whereby NULL entries select the\ndefault. Avoid emitting color sequences for empty strings.\n\nWrap tag annotations in separate color sequences from tag names, because\notherwise tag names can end up uncolored when %w width formatting breaks\nlines between annotation and name. Amend t4207-log-decoration-colors.sh\naccordingly.\n\nSigned-off-by: Andy Koppe <andy.koppe@gmail.com>\n---\nCorrected mistakes in commit description.\n\n Documentation/pretty-formats.txt | 19 ++++++++-\n log-tree.c                       | 69 ++++++++++++++++++++------------\n log-tree.h                       | 17 ++++----\n pretty.c                         | 62 +++++++++++++++++++++++++++-\n t/t4205-log-pretty-formats.sh    | 21 ++++++++++\n t/t4207-log-decoration-colors.sh | 32 +++++++++------\n 6 files changed, 171 insertions(+), 49 deletions(-)\n\ndiff --git a/Documentation/pretty-formats.txt b/Documentation/pretty-formats.txt\nindex 3b71334459..c08aba15af 100644\n--- a/Documentation/pretty-formats.txt\n+++ b/Documentation/pretty-formats.txt\n@@ -222,7 +222,22 @@ The placeholders are:\n \tlinkgit:git-rev-list[1])\n '%d':: ref names, like the --decorate option of linkgit:git-log[1]\n '%D':: ref names without the \" (\", \")\" wrapping.\n-'%(describe[:options])':: human-readable name, like\n+'%(decorate[:<options>])':: ref names with custom decorations.\n+\t\t\t  The `decorate` string may be followed by a colon\n+\t\t\t  and zero or more comma-separated options.\n+\t\t\t  Option values may contain literal formatting codes.\n+\t\t\t  These must be used for commas (`%x2C`) and closing\n+\t\t\t  parentheses (`%x29`), due to their role in the option\n+\t\t\t  syntax.\n++\n+** 'prefix=<value>': Shown before the list of ref names.  Defaults to \" (\".\n+** 'suffix=<value>': Shown after the list of ref names.  Defaults to \")\".\n+** 'separator=<value>': Shown between ref names.  Defaults to \", \".\n+** 'arrow=<value>': Shown between HEAD and the branch it points to, if any.\n+\t\t    Defaults to \" \\-> \".\n+** 'tag=<value>': Shown before tag names. Defaults to \"tag: \".\n+\n+'%(describe[:<options>])':: human-readable name, like\n \t\t\t  linkgit:git-describe[1]; empty string for\n \t\t\t  undescribable commits.  The `describe` string\n \t\t\t  may be followed by a colon and zero or more\n@@ -281,7 +296,7 @@ endif::git-rev-list[]\n '%gE':: reflog identity email (respecting .mailmap, see\n \tlinkgit:git-shortlog[1] or linkgit:git-blame[1])\n '%gs':: reflog subject\n-'%(trailers[:options])':: display the trailers of the body as\n+'%(trailers[:<options>])':: display the trailers of the body as\n \t\t\t  interpreted by\n \t\t\t  linkgit:git-interpret-trailers[1]. The\n \t\t\t  `trailers` string may be followed by a colon\ndiff --git a/log-tree.c b/log-tree.c\nindex f4b22a60cc..4b46884ef6 100644\n--- a/log-tree.c\n+++ b/log-tree.c\n@@ -301,27 +301,34 @@ static void show_name(struct strbuf *sb, const struct name_decoration *decoratio\n \n /*\n  * The caller makes sure there is no funny color before calling.\n- * format_decorations_extended makes sure the same after return.\n+ * format_decorations ensures the same after return.\n  */\n-void format_decorations_extended(struct strbuf *sb,\n+void format_decorations(struct strbuf *sb,\n \t\t\tconst struct commit *commit,\n \t\t\tint use_color,\n-\t\t\tconst char *prefix,\n-\t\t\tconst char *separator,\n-\t\t\tconst char *suffix)\n+\t\t\tconst struct decoration_options *opts)\n {\n-\tconst struct name_decoration *decoration;\n-\tconst struct name_decoration *current_and_HEAD;\n-\tconst char *color_commit =\n-\t\tdiff_get_color(use_color, DIFF_COMMIT);\n-\tconst char *color_reset =\n-\t\tdecorate_get_color(use_color, DECORATION_NONE);\n+\tconst char *color_commit, *color_reset;\n+\tconst char *prefix, *suffix, *separator, *arrow, *tag;\n+\n+\tconst struct name_decoration *current_and_HEAD;\n+\tconst struct name_decoration *decoration =\n+\t\tget_name_decoration(&commit->object);\n \n-\tdecoration = get_name_decoration(&commit->object);\n \tif (!decoration)\n \t\treturn;\n \n+\tcolor_commit = diff_get_color(use_color, DIFF_COMMIT);\n+\tcolor_reset = decorate_get_color(use_color, DECORATION_NONE);\n+\n+\tprefix = (opts && opts->prefix) ? opts->prefix : \" (\";\n+\tsuffix = (opts && opts->suffix) ? opts->suffix : \")\";\n+\tseparator = (opts && opts->separator) ? opts->separator : \", \";\n+\tarrow = (opts && opts->arrow) ? opts->arrow : \" -> \";\n+\ttag = (opts && opts->tag) ? opts->tag : \"tag: \";\n+\n \tcurrent_and_HEAD = current_pointed_by_HEAD(decoration);\n+\n \twhile (decoration) {\n \t\t/*\n \t\t * When both current and HEAD are there, only\n@@ -329,20 +336,29 @@ void format_decorations_extended(struct strbuf *sb,\n \t\t * appeared, skipping the entry for current.\n \t\t */\n \t\tif (decoration != current_and_HEAD) {\n-\t\t\tstrbuf_addstr(sb, color_commit);\n-\t\t\tstrbuf_addstr(sb, prefix);\n-\t\t\tstrbuf_addstr(sb, color_reset);\n-\t\t\tstrbuf_addstr(sb, decorate_get_color(use_color, decoration->type));\n-\t\t\tif (decoration->type == DECORATION_REF_TAG)\n-\t\t\t\tstrbuf_addstr(sb, \"tag: \");\n+\t\t\tconst char *color =\n+\t\t\t\tdecorate_get_color(use_color, decoration->type);\n \n+\t\t\tif (*prefix) {\n+\t\t\t\tstrbuf_addstr(sb, color_commit);\n+\t\t\t\tstrbuf_addstr(sb, prefix);\n+\t\t\t\tstrbuf_addstr(sb, color_reset);\n+\t\t\t}\n+\n+\t\t\tif (*tag && decoration->type == DECORATION_REF_TAG) {\n+\t\t\t\tstrbuf_addstr(sb, color);\n+\t\t\t\tstrbuf_addstr(sb, tag);\n+\t\t\t\tstrbuf_addstr(sb, color_reset);\n+\t\t\t}\n+\t\t\tstrbuf_addstr(sb, color);\n \t\t\tshow_name(sb, decoration);\n \n-\t\t\tif (current_and_HEAD &&\n+\t\t\tif (*arrow && current_and_HEAD &&\n \t\t\t    decoration->type == DECORATION_REF_HEAD) {\n-\t\t\t\tstrbuf_addstr(sb, \" -> \");\n+\t\t\t\tstrbuf_addstr(sb, arrow);\n \t\t\t\tstrbuf_addstr(sb, color_reset);\n-\t\t\t\tstrbuf_addstr(sb, decorate_get_color(use_color, current_and_HEAD->type));\n+\t\t\t\tstrbuf_addstr(sb, decorate_get_color(\n+\t\t\t\t\tuse_color, current_and_HEAD->type));\n \t\t\t\tshow_name(sb, current_and_HEAD);\n \t\t\t}\n \t\t\tstrbuf_addstr(sb, color_reset);\n@@ -351,9 +367,12 @@ void format_decorations_extended(struct strbuf *sb,\n \t\t}\n \t\tdecoration = decoration->next;\n \t}\n-\tstrbuf_addstr(sb, color_commit);\n-\tstrbuf_addstr(sb, suffix);\n-\tstrbuf_addstr(sb, color_reset);\n+\n+\tif (*suffix) {\n+\t\tstrbuf_addstr(sb, color_commit);\n+\t\tstrbuf_addstr(sb, suffix);\n+\t\tstrbuf_addstr(sb, color_reset);\n+\t}\n }\n \n void show_decorations(struct rev_info *opt, struct commit *commit)\n@@ -368,7 +387,7 @@ void show_decorations(struct rev_info *opt, struct commit *commit)\n \t}\n \tif (!opt->show_decorations)\n \t\treturn;\n-\tformat_decorations(&sb, commit, opt->diffopt.use_color);\n+\tformat_decorations(&sb, commit, opt->diffopt.use_color, NULL);\n \tfputs(sb.buf, opt->diffopt.file);\n \tstrbuf_release(&sb);\n }\ndiff --git a/log-tree.h b/log-tree.h\nindex e7e4641cf8..39ab06a3ca 100644\n--- a/log-tree.h\n+++ b/log-tree.h\n@@ -13,17 +13,20 @@ struct decoration_filter {\n \tstruct string_list *exclude_ref_config_pattern;\n };\n \n+struct decoration_options {\n+\tchar *prefix;\n+\tchar *suffix;\n+\tchar *separator;\n+\tchar *arrow;\n+\tchar *tag;\n+};\n+\n int parse_decorate_color_config(const char *var, const char *slot_name, const char *value);\n int log_tree_diff_flush(struct rev_info *);\n int log_tree_commit(struct rev_info *, struct commit *);\n void show_log(struct rev_info *opt);\n-void format_decorations_extended(struct strbuf *sb, const struct commit *commit,\n-\t\t\t     int use_color,\n-\t\t\t     const char *prefix,\n-\t\t\t     const char *separator,\n-\t\t\t     const char *suffix);\n-#define format_decorations(strbuf, commit, color) \\\n-\t\t\t     format_decorations_extended((strbuf), (commit), (color), \" (\", \", \", \")\")\n+void format_decorations(struct strbuf *sb, const struct commit *commit,\n+\t\t\tint use_color, const struct decoration_options *opts);\n void show_decorations(struct rev_info *opt, struct commit *commit);\n void log_write_email_headers(struct rev_info *opt, struct commit *commit,\n \t\t\t     const char **extra_headers_p,\ndiff --git a/pretty.c b/pretty.c\nindex 0bb938021b..a59b7f0dbc 100644\n--- a/pretty.c\n+++ b/pretty.c\n@@ -1373,6 +1373,46 @@ static size_t parse_describe_args(const char *start, struct strvec *args)\n \treturn arg - start;\n }\n \n+\n+static int parse_decoration_option(const char **arg,\n+\t\t\t\t   const char *name,\n+\t\t\t\t   char **opt)\n+{\n+\tconst char *argval;\n+\tsize_t arglen;\n+\n+\tif (match_placeholder_arg_value(*arg, name, arg, &argval, &arglen)) {\n+\t\tchar *val = xstrndup(argval, arglen);\n+\t\tstruct strbuf sb = STRBUF_INIT;\n+\n+\t\tstrbuf_expand(&sb, val, strbuf_expand_literal_cb, NULL);\n+\t\tfree(val);\n+\t\t*opt = strbuf_detach(&sb, NULL);\n+\t\treturn 1;\n+\t}\n+\treturn 0;\n+}\n+\n+static void parse_decoration_options(const char **arg,\n+\t\t\t\t     struct decoration_options *opts)\n+{\n+\twhile (parse_decoration_option(arg, \"prefix\", &opts->prefix) ||\n+\t       parse_decoration_option(arg, \"suffix\", &opts->suffix) ||\n+\t       parse_decoration_option(arg, \"separator\", &opts->separator) ||\n+\t       parse_decoration_option(arg, \"arrow\", &opts->arrow) ||\n+\t       parse_decoration_option(arg, \"tag\", &opts->tag))\n+\t\t;\n+}\n+\n+static void free_decoration_options(const struct decoration_options *opts)\n+{\n+\tfree(opts->prefix);\n+\tfree(opts->suffix);\n+\tfree(opts->separator);\n+\tfree(opts->arrow);\n+\tfree(opts->tag);\n+}\n+\n static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */\n \t\t\t\tconst char *placeholder,\n \t\t\t\tvoid *context)\n@@ -1526,10 +1566,11 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */\n \t\tstrbuf_addstr(sb, get_revision_mark(NULL, commit));\n \t\treturn 1;\n \tcase 'd':\n-\t\tformat_decorations(sb, commit, c->auto_color);\n+\t\tformat_decorations(sb, commit, c->auto_color, NULL);\n \t\treturn 1;\n \tcase 'D':\n-\t\tformat_decorations_extended(sb, commit, c->auto_color, \"\", \", \", \"\");\n+\t\tformat_decorations(sb, commit, c->auto_color,\n+\t\t\t\t   &(struct decoration_options){\"\", \"\"});\n \t\treturn 1;\n \tcase 'S':\t\t/* tag/branch like --source */\n \t\tif (!(c->pretty_ctx->rev && c->pretty_ctx->rev->sources))\n@@ -1627,6 +1668,23 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */\n \t\treturn 2;\n \t}\n \n+\tif (skip_prefix(placeholder, \"(decorate\", &arg)) {\n+\t\tstruct decoration_options opts = { NULL };\n+\t\tsize_t ret = 0;\n+\n+\t\tif (*arg == ':') {\n+\t\t\targ++;\n+\t\t\tparse_decoration_options(&arg, &opts);\n+\t\t}\n+\t\tif (*arg == ')') {\n+\t\t\tformat_decorations(sb, commit, c->auto_color, &opts);\n+\t\t\tret = arg - placeholder + 1;\n+\t\t}\n+\n+\t\tfree_decoration_options(&opts);\n+\t\treturn ret;\n+\t}\n+\n \t/* For the rest we have to parse the commit header. */\n \tif (!c->commit_header_parsed) {\n \t\tmsg = c->message =\ndiff --git a/t/t4205-log-pretty-formats.sh b/t/t4205-log-pretty-formats.sh\nindex 4cf8a77667..5ea937648a 100755\n--- a/t/t4205-log-pretty-formats.sh\n+++ b/t/t4205-log-pretty-formats.sh\n@@ -576,6 +576,27 @@ test_expect_success 'clean log decoration' '\n \ttest_cmp expected actual1\n '\n \n+test_expect_success 'pretty format %decorate' '\n+\tgit checkout -b foo &&\n+\tgit commit --allow-empty -m \"new commit\" &&\n+\tgit tag bar &&\n+\tgit branch qux &&\n+\techo \" (HEAD -> foo, tag: bar, qux)\" >expect1 &&\n+\tgit log --format=\"%(decorate)\" -1 >actual1 &&\n+\ttest_cmp expect1 actual1 &&\n+\techo \"HEAD -> foo, tag: bar, qux\" >expect2 &&\n+\tgit log --format=\"%(decorate:prefix=,suffix=)\" -1 >actual2 &&\n+\ttest_cmp expect2 actual2 &&\n+\techo \"HEAD->foo bar qux\" >expect3 &&\n+\tgit log --format=\"%(decorate:prefix=,suffix=,separator= ,arrow=->,tag=)\" \\\n+\t\t-1 >actual3 &&\n+\ttest_cmp expect3 actual3 &&\n+\techo \"[HEAD,foo,bar,qux]\" >expect4 &&\n+\tgit log --format=\"%(decorate:prefix=[,suffix=],separator=%x2C,arrow=%x2C,tag=)\" \\\n+\t\t-1 >actual4 &&\n+\ttest_cmp expect4 actual4\n+'\n+\n cat >trailers <<EOF\n Signed-off-by: A U Thor <author@example.com>\n Acked-by: A U Thor <author@example.com>\ndiff --git a/t/t4207-log-decoration-colors.sh b/t/t4207-log-decoration-colors.sh\nindex ded33a82e2..3a4eedc494 100755\n--- a/t/t4207-log-decoration-colors.sh\n+++ b/t/t4207-log-decoration-colors.sh\n@@ -55,13 +55,15 @@ test_expect_success 'commit decorations colored correctly' '\n \tcat >expect <<-EOF &&\n \t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_HEAD}HEAD -> \\\n ${c_reset}${c_branch}main${c_reset}${c_commit}, \\\n-${c_reset}${c_tag}tag: v1.0${c_reset}${c_commit}, \\\n-${c_reset}${c_tag}tag: B${c_reset}${c_commit})${c_reset} B\n-${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_tag}tag: A1${c_reset}${c_commit}, \\\n+${c_reset}${c_tag}tag: ${c_reset}${c_tag}v1.0${c_reset}${c_commit}, \\\n+${c_reset}${c_tag}tag: ${c_reset}${c_tag}B${c_reset}${c_commit})${c_reset} B\n+${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\\\n+${c_tag}tag: ${c_reset}${c_tag}A1${c_reset}${c_commit}, \\\n ${c_reset}${c_remoteBranch}other/main${c_reset}${c_commit})${c_reset} A1\n-\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_stash}refs/stash${c_reset}${c_commit})${c_reset} \\\n-On main: Changes to A.t\n-\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_tag}tag: A${c_reset}${c_commit})${c_reset} A\n+\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\\\n+${c_stash}refs/stash${c_reset}${c_commit})${c_reset} On main: Changes to A.t\n+\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\\\n+${c_tag}tag: ${c_reset}${c_tag}A${c_reset}${c_commit})${c_reset} A\n \tEOF\n \n \tgit log --first-parent --no-abbrev --decorate --oneline --color=always --all >actual &&\n@@ -78,10 +80,12 @@ test_expect_success 'test coloring with replace-objects' '\n \tcat >expect <<-EOF &&\n \t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_HEAD}HEAD -> \\\n ${c_reset}${c_branch}main${c_reset}${c_commit}, \\\n-${c_reset}${c_tag}tag: D${c_reset}${c_commit})${c_reset} D\n-\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_tag}tag: C${c_reset}${c_commit}, \\\n+${c_reset}${c_tag}tag: ${c_reset}${c_tag}D${c_reset}${c_commit})${c_reset} D\n+\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\\\n+${c_tag}tag: ${c_reset}${c_tag}C${c_reset}${c_commit}, \\\n ${c_reset}${c_grafted}replaced${c_reset}${c_commit})${c_reset} B\n-\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_tag}tag: A${c_reset}${c_commit})${c_reset} A\n+\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\\\n+${c_tag}tag: ${c_reset}${c_tag}A${c_reset}${c_commit})${c_reset} A\n EOF\n \n \tgit log --first-parent --no-abbrev --decorate --oneline --color=always HEAD >actual &&\n@@ -102,11 +106,13 @@ test_expect_success 'test coloring with grafted commit' '\n \tcat >expect <<-EOF &&\n \t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_HEAD}HEAD -> \\\n ${c_reset}${c_branch}main${c_reset}${c_commit}, \\\n-${c_reset}${c_tag}tag: D${c_reset}${c_commit}, \\\n+${c_reset}${c_tag}tag: ${c_reset}${c_tag}D${c_reset}${c_commit}, \\\n ${c_reset}${c_grafted}replaced${c_reset}${c_commit})${c_reset} D\n-\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_tag}tag: v1.0${c_reset}${c_commit}, \\\n-${c_reset}${c_tag}tag: B${c_reset}${c_commit})${c_reset} B\n-\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_tag}tag: A${c_reset}${c_commit})${c_reset} A\n+\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\\\n+${c_tag}tag: ${c_reset}${c_tag}v1.0${c_reset}${c_commit}, \\\n+${c_reset}${c_tag}tag: ${c_reset}${c_tag}B${c_reset}${c_commit})${c_reset} B\n+\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\\\n+${c_tag}tag: ${c_reset}${c_tag}A${c_reset}${c_commit})${c_reset} A\n \tEOF\n \n \tgit log --first-parent --no-abbrev --decorate --oneline --color=always HEAD >actual &&\n-- \n2.41.0\n\n"},{"id":"479590","messageId":"xmqqmszudtih.fsf@gitster.g","threadId":"59990","inReplyTo":"20230715160730.4046-1-andy.koppe@gmail.com","subject":"Re: [PATCH v2] pretty: add %(decorate[:<options>]) format","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2023-07-17T23:10:14Z","receivedAt":"2023-07-17T23:11:27Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Andy Koppe <andy.koppe@gmail.com> writes:\n\n> This lists ref names in the same way as the %d decoration format, but\n\nPlease replace \"This\" with \"%(descorate[:<options>]\", i.e. a more\nconcrete form, so that people do not have to go back to the title in\norder to understand the body of the proposed log message.\n\n> -'%(describe[:options])':: human-readable name, like\n> +'%(describe[:<options>])':: human-readable name, like\n> -'%(trailers[:options])':: display the trailers of the body as\n> +'%(trailers[:<options>])':: display the trailers of the body as\n\nIt is a very good idea to signal that <options> is a placeholder by\nenclosing it inside <angle bracket> like this patch wants to do with\n%(decorate), and to make sure that other existing ones consistently\nfollow the same convention.\n\nBut the latter, being very small, can be buried in the noise.\n\nIt may be a good idea to have small \"preliminary clean-up\" patches\nthat do not add anything related to %(decorate) at the beginning of\nthe series.  [PATCH 1/3] can be %(token[:<options>]) clean-up,\n[PATCH 2/3] can be \"what is literal formatting code\" clarification,\nand [PATCH 3/3] can be the rest of this patch, for example.\n\n> +'%(decorate[:<options>])':: ref names with custom decorations.\n> +\t\t\t  The `decorate` string may be followed by a colon\n> +\t\t\t  and zero or more comma-separated options.\n> +\t\t\t  Option values may contain literal formatting codes.\n> +\t\t\t  These must be used for commas (`%x2C`) and closing\n> +\t\t\t  parentheses (`%x29`), due to their role in the option\n> +\t\t\t  syntax.\n\nOK.  I fear that \"literal formatting codes\" may not be understood by\nreaders without having any cross references here.  Perhaps something\nlike the patch attached at the end of this message would help.\n\n> +** 'arrow=<value>': Shown between HEAD and the branch it points to, if any.\n> +\t\t    Defaults to \" \\-> \".\n\nIt feels a bit strange that this feature is limited only to \"HEAD\"\nand other symbolic refs are not annotated with an arrow, but it is\nnot the fault of this patch.  We might want to see if it is worth to\nextend this to other symbolic refs but not while this patch is being\ndiscussed and polished.\n\n> diff --git a/log-tree.c b/log-tree.c\n> index f4b22a60cc..4b46884ef6 100644\n> --- a/log-tree.c\n> +++ b/log-tree.c\n> @@ -301,27 +301,34 @@ static void show_name(struct strbuf *sb, const struct name_decoration *decoratio\n>  \n>  /*\n>   * The caller makes sure there is no funny color before calling.\n> - * format_decorations_extended makes sure the same after return.\n> + * format_decorations ensures the same after return.\n>   */\n> -void format_decorations_extended(struct strbuf *sb,\n> +void format_decorations(struct strbuf *sb,\n>  \t\t\tconst struct commit *commit,\n>  \t\t\tint use_color,\n> -\t\t\tconst char *prefix,\n> -\t\t\tconst char *separator,\n> -\t\t\tconst char *suffix)\n> +\t\t\tconst struct decoration_options *opts)\n\nHmph, presumably the idea is to collect these parameters in a struct\nand pass it around, which would be easier to extend, and then teach\nthe hardcoded default to the callee, instead of the macro.  OK.\n\nIt may have made the change easier to review if such a change that\ncan cleanly separable into a single step into a single preliminary\nclean-up patch before we start adding the customization, but let's\nread on to see if I can keep everything in my head---I'll complain\nat the end if I can't ;-).\n\n>  {\n> -\tconst struct name_decoration *decoration;\n> -\tconst struct name_decoration *current_and_HEAD;\n> -\tconst char *color_commit =\n> -\t\tdiff_get_color(use_color, DIFF_COMMIT);\n> -\tconst char *color_reset =\n> -\t\tdecorate_get_color(use_color, DECORATION_NONE);\n\nGetting rid of the computation of the initialization value from the\nabove decl block, ...\n\n\n> +\tconst char *color_commit, *color_reset;\n> +\tconst char *prefix, *suffix, *separator, *arrow, *tag;\n> +\n> +\tconst struct name_decoration *current_and_HEAD;\n\n... like this, so we will return early without wasting extra cycles\nwhose result we will not use, is a very good idea.  But then, ...\n\n> +\tconst struct name_decoration *decoration =\n> +\t\tget_name_decoration(&commit->object);\n>  \n> -\tdecoration = get_name_decoration(&commit->object);\n>  \tif (!decoration)\n>  \t\treturn;\n\n... I think the original is easier to follow than the updated form\nfor the \"decoration\" variable, simply because the declaration part\nwill become absolutely free, and it becomes easier to see that the\ncomputation of \"decoration\" is the very first thing the cycles are\nspent on.\n\n> +\tcolor_commit = diff_get_color(use_color, DIFF_COMMIT);\n> +\tcolor_reset = decorate_get_color(use_color, DECORATION_NONE);\n> +\n> +\tprefix = (opts && opts->prefix) ? opts->prefix : \" (\";\n> +\tsuffix = (opts && opts->suffix) ? opts->suffix : \")\";\n> +\tseparator = (opts && opts->separator) ? opts->separator : \", \";\n\nKnowing these hardcoded values were the responsibility of the\nformat_decorations() C preprocessor macro; now it is written here.\nIt is a moral no-op change from the caller's point of view.\n\n> +\tarrow = (opts && opts->arrow) ? opts->arrow : \" -> \";\n> +\ttag = (opts && opts->tag) ? opts->tag : \"tag: \";\n\nThese two are new.  That is one thing why I wondered above if it is\na good idea to separate the \"refactor to introduce\ndecoration_options structure that has three members and replace\nthree parameters to this function with it, so that we can get rid of\nthe format_decorations() macro\" into a single preliminary step.\nThen the reviewers can go on, after being convinced that such a\nmoral no-op refactoring is correct, to review the next step that\nwould presumably add these two members to the option struct and make\nthese customizable.\n\n>  \tcurrent_and_HEAD = current_pointed_by_HEAD(decoration);\n> +\n>  \twhile (decoration) {\n>  \t\t/*\n>  \t\t * When both current and HEAD are there, only\n\nUnrelated noise.\n\n> @@ -329,20 +336,29 @@ void format_decorations_extended(struct strbuf *sb,\n>  \t\t * appeared, skipping the entry for current.\n>  \t\t */\n>  \t\tif (decoration != current_and_HEAD) {\n> -\t\t\tstrbuf_addstr(sb, color_commit);\n> -\t\t\tstrbuf_addstr(sb, prefix);\n> -\t\t\tstrbuf_addstr(sb, color_reset);\n> -\t\t\tstrbuf_addstr(sb, decorate_get_color(use_color, decoration->type));\n> -\t\t\tif (decoration->type == DECORATION_REF_TAG)\n> -\t\t\t\tstrbuf_addstr(sb, \"tag: \");\n> +\t\t\tconst char *color =\n> +\t\t\t\tdecorate_get_color(use_color, decoration->type);\n>  \n> +\t\t\tif (*prefix) {\n> +\t\t\t\tstrbuf_addstr(sb, color_commit);\n> +\t\t\t\tstrbuf_addstr(sb, prefix);\n> +\t\t\t\tstrbuf_addstr(sb, color_reset);\n> +\t\t\t}\n> +\n> +\t\t\tif (*tag && decoration->type == DECORATION_REF_TAG) {\n> +\t\t\t\tstrbuf_addstr(sb, color);\n> +\t\t\t\tstrbuf_addstr(sb, tag);\n> +\t\t\t\tstrbuf_addstr(sb, color_reset);\n> +\t\t\t}\n> +\t\t\tstrbuf_addstr(sb, color);\n>  \t\t\tshow_name(sb, decoration);\n\nAgain, this mixes adding new things (i.e. customizeable \"tag:\"\nstring, and \"->\" we see below) and improving existing things\n(i.e. \"<color><reset>\" that is presumably pointless when prefix is\nan empty string is shown as an empty string).  Ideally, the step to\nmove the three existing parameters to three members of the new\nstruct should be done first WITHOUT the empty-string improvement,\nthen another step should do the empty-string improvement (at which\ntime, presumably existing test script may have to be adjusted), and\nthen new features to costumize \"tag:\" and \"->\" should be added on\ntop.\n\n> -\t\t\tif (current_and_HEAD &&\n> +\t\t\tif (*arrow && current_and_HEAD &&\n>  \t\t\t    decoration->type == DECORATION_REF_HEAD) {\n\nBecause arrow is never allowed to be NULL, remove the above change,\nand ...\n\n> -\t\t\t\tstrbuf_addstr(sb, \" -> \");\n> +\t\t\t\tstrbuf_addstr(sb, arrow);\n\n... let the program crash to catch a future bug at runtime.\n\n> @@ -351,9 +367,12 @@ void format_decorations_extended(struct strbuf *sb,\n>  \t\t}\n>  \t\tdecoration = decoration->next;\n>  \t}\n> -\tstrbuf_addstr(sb, color_commit);\n> -\tstrbuf_addstr(sb, suffix);\n> -\tstrbuf_addstr(sb, color_reset);\n> +\n> +\tif (*suffix) {\n> +\t\tstrbuf_addstr(sb, color_commit);\n> +\t\tstrbuf_addstr(sb, suffix);\n> +\t\tstrbuf_addstr(sb, color_reset);\n> +\t}\n\nDitto about \"improving the <color><reset> empty sequence is a\nseparate change from making various fields customizable\".\n\nI'll stop here. After skimming the changes to the test, I think this\nsingle patch should be split into separate steps.  Perhaps the split\nshould go like this:\n\n * documentation clean-up %(token[:options]) -> %(token[:<options>])\n   plus clarification of what a \"literal formatting code\" is.\n\n * introduction of \"struct decoration_option\",\n   removing the format_decorations() macro,\n   renaming format_decorations_extended() to format_decorations(),\n   replacing the three parameters with a single struct pointer.\n\n * improving <color><reset> string when the meat of the string is\n   empty.  This step will be the FIRST step that changes the\n   externally visible behaviour, and presumably will have adjustment\n   to existing tests.\n\n * making \"tag:\" and \"->\" customizable, if values are passed in the\n   struct.  This step does not have UI changes, and the existing\n   tests should serve as a safety net to catch mistakes in this\n   step.\n\n * read the %(describe[:<option>]) and fill \"struct describe_option\".\n   This will be accompanied by additional tests for the new feature.\n\nThanks.\n\n---------------------- >8 ----------------------\nSubject: pretty-formats: define \"literal formatting code\"\n\nThe description for %(trailer) option already uses this term without\nhaving definition anywhere in the document, and we are about to add\nanother one %(decorate) that uses it.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\ndiff --git c/Documentation/pretty-formats.txt w/Documentation/pretty-formats.txt\nindex c08aba15af..b7a3a150ae 100644\n--- c/Documentation/pretty-formats.txt\n+++ w/Documentation/pretty-formats.txt\n@@ -122,7 +122,9 @@ The placeholders are:\n - Placeholders that expand to a single literal character:\n '%n':: newline\n '%%':: a raw '%'\n-'%x00':: print a byte from a hex code\n+'%x00':: '%x' followed by two hexadecimal digits is replaced with a\n+\tbyte with the hexdecimal digits' value (we will call this\n+\t\"literal formatting code\" in the rest of this document).\n \n - Placeholders that affect formatting of later placeholders:\n '%Cred':: switch color to red\n\n\n"},{"id":"479594","messageId":"xmqq7cqydo5v.fsf@gitster.g","threadId":"59990","inReplyTo":"xmqqmszudtih.fsf@gitster.g","subject":"Re: [PATCH v2] pretty: add %(decorate[:<options>]) format","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2023-07-18T01:05:48Z","receivedAt":"2023-07-18T01:05:55Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> I'll stop here. After skimming the changes to the test, I think this\n> single patch should be split into separate steps.  Perhaps the split\n> should go like this:\n> ...\n> Thanks.\n\nOh, sorry that I forgot to add one thing.\n\nOverall, the patch seems to be done very well when viewed as a\nwhole.  Thanks for working on it.\n\nIt is just I cannot be as confident as I would like to be in my\nreview when the single patch does several different things at once.\nIf it were split in steps, each step focusing on doing a single\nthing well and describing well what it does and why, reviewers can\nbe more confident that they did not miss something important in the\npatch(es).\n"},{"id":"479654","messageId":"kl6l351j22dr.fsf@chooglen-macbookpro.roam.corp.google.com","threadId":"59990","inReplyTo":"20230715160730.4046-1-andy.koppe@gmail.com","subject":"Re: [PATCH v2] pretty: add %(decorate[:<options>]) format","fromName":"Glen Choo","fromEmail":"chooglen@google.com","sentAt":"2023-07-19T18:16:16Z","receivedAt":"2023-07-19T18:16:22Z","isPatch":true,"sender":{"key":"glencbz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/58092771?v=4"},"body":"Hi Andy!\n\nWe picked up this series at Review Club. Reviewers will leave their\nthoughts on the mailing list, but if you like, you can find the notes\nat:\n\n  https://docs.google.com/document/d/14L8BAumGTpsXpjDY8VzZ4rRtpAjuGrFSRqn3stCuS_w/edit\n\nAndy Koppe <andy.koppe@gmail.com> writes:\n\n> This lists ref names in the same way as the %d decoration format, but\n> allows all the otherwise fixed strings printed around the ref names to\n> be customized, namely prefix, suffix, separator, the \"tag:\" annotation\n> and the arrow used to show where HEAD points.\n>\n> Examples:\n> - %(decorate) is equivalent to %d.\n> - %(decorate:prefix=,suffix=) is equivalent to %D.\n> - %(decorate:prefix=,suffix=,separator= ,tag=,arrow=->) produces a\n>   space-separated list without wrapping, tag annotations or spaces\n>   around the arrow.\n> - %(decorate:prefix=[,suffix=],separator=%x2C,arrow=%x2C,tag=) produces\n>   a comma-separated list enclosed in square brackets where the arrow is\n>   replaced by a comma as well.\n\nI think giving the user this level of customization makes sense,\nespecially since we do this for other format options. Importantly, this\ndesign also fits the existing conventions we have, so this looks like a\ngood proposal.\n\nAs a micro-nit: there's some useful context behind your chosen design in\n[1]. It would have been useful to link to it in the `---` context, or\nperhaps send this series as v3 and v4 to [1].\n\n[1] https://lore.kernel.org/git/20230712110732.8274-1-andy.koppe@gmail.com/\n\n> Add functions parse_decoration_option(), parse_decoration_options() and\n> free_decoration_options() to help implement the format. Test it in\n> t4205-log-pretty-formats.sh and document it in pretty-formats.txt.\n\nThis commit adds the new feature...\n\n> Refactor format_decorations() to take a struct decoration_options\n> argument specifying those strings, whereby NULL entries select the\n> default. Avoid emitting color sequences for empty strings.\n\ndoes some refactoring to support the new feature + existing use cases...\n\n> Wrap tag annotations in separate color sequences from tag names, because\n> otherwise tag names can end up uncolored when %w width formatting breaks\n> lines between annotation and name. Amend t4207-log-decoration-colors.sh\n> accordingly.\n\nand fixes a bug with coloring that is easier to run into as a result of\nthe new feature.\n\nAs others have mentioned, I think this would be easier to follow as\nseparate commits. This commit isn't so big that the refactor needs to be\nits own commit, though I don't feel strongly either way.\n\n> diff --git a/Documentation/pretty-formats.txt b/Documentation/pretty-formats.txt\n> index 3b71334459..c08aba15af 100644\n> --- a/Documentation/pretty-formats.txt\n> +++ b/Documentation/pretty-formats.txt\n> @@ -222,7 +222,22 @@ The placeholders are:\n>  \tlinkgit:git-rev-list[1])\n>  '%d':: ref names, like the --decorate option of linkgit:git-log[1]\n>  '%D':: ref names without the \" (\", \")\" wrapping.\n> -'%(describe[:options])':: human-readable name, like\n> +'%(decorate[:<options>])':: ref names with custom decorations.\n> +\t\t\t  The `decorate` string may be followed by a colon\n> +\t\t\t  and zero or more comma-separated options.\n> +\t\t\t  Option values may contain literal formatting codes.\n> +\t\t\t  These must be used for commas (`%x2C`) and closing\n> +\t\t\t  parentheses (`%x29`), due to their role in the option\n> +\t\t\t  syntax.\n\nTo make this easier to visualize, it would be useful to include the\nexamples from your commit message (%d, %D, etc.).\n\n> +'%(describe[:<options>])':: human-readable name, like\n\nAh, adding the <> is a good fix. I think it doesn't warrant its own\npatch, but it should be called out in the commit message.\n\n>  /*\n>   * The caller makes sure there is no funny color before calling.\n> - * format_decorations_extended makes sure the same after return.\n> + * format_decorations ensures the same after return.\n>   */\n> -void format_decorations_extended(struct strbuf *sb,\n> +void format_decorations(struct strbuf *sb,\n>  \t\t\tconst struct commit *commit,\n>  \t\t\tint use_color,\n> -\t\t\tconst char *prefix,\n> -\t\t\tconst char *separator,\n> -\t\t\tconst char *suffix)\n> +\t\t\tconst struct decoration_options *opts)\n>  {\n> -\tconst struct name_decoration *decoration;\n> -\tconst struct name_decoration *current_and_HEAD;\n> -\tconst char *color_commit =\n> -\t\tdiff_get_color(use_color, DIFF_COMMIT);\n> -\tconst char *color_reset =\n> -\t\tdecorate_get_color(use_color, DECORATION_NONE);\n> +\tconst char *color_commit, *color_reset;\n> +\tconst char *prefix, *suffix, *separator, *arrow, *tag;\n> +\n> +\tconst struct name_decoration *current_and_HEAD;\n> +\tconst struct name_decoration *decoration =\n> +\t\tget_name_decoration(&commit->object);\n>  \n> -\tdecoration = get_name_decoration(&commit->object);\n>  \tif (!decoration)\n>  \t\treturn;\n>  \n> +\tcolor_commit = diff_get_color(use_color, DIFF_COMMIT);\n> +\tcolor_reset = decorate_get_color(use_color, DECORATION_NONE);\n\nI'm guessing that you shuffled these lines to make use of an early\nreturn? If so, both versions are not different enough to warrant the\nchurn IMO. It would be worth pointing out the reshuffling in the commit\nmessage, especially if you had another rationale in mind.\n\n> +\tprefix = (opts && opts->prefix) ? opts->prefix : \" (\";\n> +\tsuffix = (opts && opts->suffix) ? opts->suffix : \")\";\n> +\tseparator = (opts && opts->separator) ? opts->separator : \", \";\n> +\tarrow = (opts && opts->arrow) ? opts->arrow : \" -> \";\n> +\ttag = (opts && opts->tag) ? opts->tag : \"tag: \";\n\nSo NULL means \"use the default\"...\n\n> +struct decoration_options {\n> +\tchar *prefix;\n> +\tchar *suffix;\n> +\tchar *separator;\n> +\tchar *arrow;\n> +\tchar *tag;\n> +};\n> +\n>  int parse_decorate_color_config(const char *var, const char *slot_name, const char *value);\n>  int log_tree_diff_flush(struct rev_info *);\n>  int log_tree_commit(struct rev_info *, struct commit *);\n>  void show_log(struct rev_info *opt);\n> -void format_decorations_extended(struct strbuf *sb, const struct commit *commit,\n> -\t\t\t     int use_color,\n> -\t\t\t     const char *prefix,\n> -\t\t\t     const char *separator,\n> -\t\t\t     const char *suffix);\n> -#define format_decorations(strbuf, commit, color) \\\n> -\t\t\t     format_decorations_extended((strbuf), (commit), (color), \" (\", \", \", \")\")\n> +void format_decorations(struct strbuf *sb, const struct commit *commit,\n> +\t\t\tint use_color, const struct decoration_options *opts);\n\nWhich lets us unify these two functions. Makes sense.\n\n> +static int parse_decoration_option(const char **arg,\n> +\t\t\t\t   const char *name,\n> +\t\t\t\t   char **opt)\n> +{\n> +\tconst char *argval;\n> +\tsize_t arglen;\n> +\n> +\tif (match_placeholder_arg_value(*arg, name, arg, &argval, &arglen)) {\n> +\t\tchar *val = xstrndup(argval, arglen);\n> +\t\tstruct strbuf sb = STRBUF_INIT;\n> +\n> +\t\tstrbuf_expand(&sb, val, strbuf_expand_literal_cb, NULL);\n\nstrbuf_expand() got removed in 'master' recently, so this should be\nrebased.\n\n>  static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */\n>  \t\t\t\tconst char *placeholder,\n>  \t\t\t\tvoid *context)\n> @@ -1526,10 +1566,11 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */\n>  \t\tstrbuf_addstr(sb, get_revision_mark(NULL, commit));\n>  \t\treturn 1;\n>  \tcase 'd':\n> -\t\tformat_decorations(sb, commit, c->auto_color);\n> +\t\tformat_decorations(sb, commit, c->auto_color, NULL);\n>  \t\treturn 1;\n>  \tcase 'D':\n> -\t\tformat_decorations_extended(sb, commit, c->auto_color, \"\", \", \", \"\");\n> +\t\tformat_decorations(sb, commit, c->auto_color,\n> +\t\t\t\t   &(struct decoration_options){\"\", \"\"});\n\nI don't remember if C99 lets you name .prefix and .suffix here, but if\nso, it would be good to name them. Otherwise it's easy to get the order\nwrong, e.g. if someone reorders the fields in struct decoration_options.\n\n> +test_expect_success 'pretty format %decorate' '\n> +\tgit checkout -b foo &&\n> +\tgit commit --allow-empty -m \"new commit\" &&\n> +\tgit tag bar &&\n> +\tgit branch qux &&\n> +\techo \" (HEAD -> foo, tag: bar, qux)\" >expect1 &&\n> +\tgit log --format=\"%(decorate)\" -1 >actual1 &&\n> +\ttest_cmp expect1 actual1 &&\n> +\techo \"HEAD -> foo, tag: bar, qux\" >expect2 &&\n> +\tgit log --format=\"%(decorate:prefix=,suffix=)\" -1 >actual2 &&\n> +\ttest_cmp expect2 actual2 &&\n> +\techo \"HEAD->foo bar qux\" >expect3 &&\n> +\tgit log --format=\"%(decorate:prefix=,suffix=,separator= ,arrow=->,tag=)\" \\\n> +\t\t-1 >actual3 &&\n> +\ttest_cmp expect3 actual3 &&\n> +\techo \"[HEAD,foo,bar,qux]\" >expect4 &&\n> +\tgit log --format=\"%(decorate:prefix=[,suffix=],separator=%x2C,arrow=%x2C,tag=)\" \\\n> +\t\t-1 >actual4 &&\n> +\ttest_cmp expect4 actual4\n> +'\n> +\n\nIt would be useful to get some \"bad\" inputs to %(decorate:) to check\nthat we handle them correctly, especially since it's implemented with\nwhile() loops.\n\nOverall, I thought this patch looks really good. Thanks!\n"},{"id":"479783","messageId":"783011d8-53ea-15cb-a9c7-6cb0c15bd5aa@gmail.com","threadId":"59990","inReplyTo":"kl6l351j22dr.fsf@chooglen-macbookpro.roam.corp.google.com","subject":"Re: [PATCH v2] pretty: add %(decorate[:<options>]) format","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2023-07-23T16:25:58Z","receivedAt":"2023-07-23T16:26:48Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Andy\n\nOn 19/07/2023 19:16, Glen Choo wrote:\n>>   \tcase 'D':\n>> -\t\tformat_decorations_extended(sb, commit, c->auto_color, \"\", \", \", \"\");\n>> +\t\tformat_decorations(sb, commit, c->auto_color,\n>> +\t\t\t\t   &(struct decoration_options){\"\", \"\"});\n> \n> I don't remember if C99 lets you name .prefix and .suffix here, but if\n> so, it would be good to name them. Otherwise it's easy to get the order\n> wrong, e.g. if someone reorders the fields in struct decoration_options.\n\nThat's a good suggestion. I think this would be the first use of a \ncompound literal in the code base so it would be helpful to mention that \nin the commit message.\n\nWe've been depending on C99 for a while now so I'd support adding this \ncompound literal as a test balloon for compiler support. Ævar reported a \nwhile back that they are supported by IBM xlc, Oracle SunCC and HP/UX's \naCC[1] and back then I looked at NonStop which seemed to offer support \nwith the right compiler flag.\n\nOverall this is a well written, well motivated patch with a good commit \nmessage.\n\nBest Wishes\n\nPhillip\n\n[1] https://lore.kernel.org/git/87h7e61duk.fsf@evledraar.gmail.com/\n"},{"id":"480482","messageId":"20230810211619.19055-2-andy.koppe@gmail.com","threadId":"59990","inReplyTo":"20230810211619.19055-1-andy.koppe@gmail.com","subject":"[PATCH v3 2/7] pretty-formats: enclose options in angle brackets","fromName":"Andy Koppe","fromEmail":"andy.koppe@gmail.com","sentAt":"2023-08-10T21:16:14Z","receivedAt":"2023-08-10T21:17:08Z","isPatch":true,"sender":{"key":"andy.koppe@gmail.com","avatar":"https://avatars.githubusercontent.com/u/223411?v=4"},"body":"Enclose the 'options' placeholders in the %(describe) and %(trailers)\nformat specifiers in angle brackets to clarify that they are\nplaceholders rather than keywords.\n\nAlso remove the indentation from their descriptions, instead of\nincreasing it to account for the extra two angle bracket in the\nheadings. The indentation isn't required by asciidoc, it doesn't reflect\nhow the output text is formatted, and it's inconsistent with the\nfollowing bullet points that are at the same level in the output.\n\nSigned-off-by: Andy Koppe <andy.koppe@gmail.com>\n---\n Documentation/pretty-formats.txt | 24 ++++++++++--------------\n 1 file changed, 10 insertions(+), 14 deletions(-)\n\ndiff --git a/Documentation/pretty-formats.txt b/Documentation/pretty-formats.txt\nindex 5e1432951b..851a9878e6 100644\n--- a/Documentation/pretty-formats.txt\n+++ b/Documentation/pretty-formats.txt\n@@ -224,13 +224,11 @@ The placeholders are:\n \tlinkgit:git-rev-list[1])\n '%d':: ref names, like the --decorate option of linkgit:git-log[1]\n '%D':: ref names without the \" (\", \")\" wrapping.\n-'%(describe[:options])':: human-readable name, like\n-\t\t\t  linkgit:git-describe[1]; empty string for\n-\t\t\t  undescribable commits.  The `describe` string\n-\t\t\t  may be followed by a colon and zero or more\n-\t\t\t  comma-separated options.  Descriptions can be\n-\t\t\t  inconsistent when tags are added or removed at\n-\t\t\t  the same time.\n+'%(describe[:<options>])'::\n+human-readable name, like linkgit:git-describe[1]; empty string for\n+undescribable commits.  The `describe` string may be followed by a colon and\n+zero or more comma-separated options.  Descriptions can be inconsistent when\n+tags are added or removed at the same time.\n +\n ** 'tags[=<bool-value>]': Instead of only considering annotated tags,\n    consider lightweight tags as well.\n@@ -283,13 +281,11 @@ endif::git-rev-list[]\n '%gE':: reflog identity email (respecting .mailmap, see\n \tlinkgit:git-shortlog[1] or linkgit:git-blame[1])\n '%gs':: reflog subject\n-'%(trailers[:options])':: display the trailers of the body as\n-\t\t\t  interpreted by\n-\t\t\t  linkgit:git-interpret-trailers[1]. The\n-\t\t\t  `trailers` string may be followed by a colon\n-\t\t\t  and zero or more comma-separated options.\n-\t\t\t  If any option is provided multiple times the\n-\t\t\t  last occurrence wins.\n+'%(trailers[:<options>])'::\n+display the trailers of the body as interpreted by\n+linkgit:git-interpret-trailers[1]. The `trailers` string may be followed by\n+a colon and zero or more comma-separated options. If any option is provided\n+multiple times, the last occurrence wins.\n +\n ** 'key=<key>': only show trailers with specified <key>. Matching is done\n    case-insensitively and trailing colon is optional. If option is\n-- \n2.42.0-rc1\n\n"},{"id":"480483","messageId":"20230810211619.19055-4-andy.koppe@gmail.com","threadId":"59990","inReplyTo":"20230810211619.19055-1-andy.koppe@gmail.com","subject":"[PATCH v3 4/7] decorate: avoid some unnecessary color overhead","fromName":"Andy Koppe","fromEmail":"andy.koppe@gmail.com","sentAt":"2023-08-10T21:16:16Z","receivedAt":"2023-08-10T21:17:10Z","isPatch":true,"sender":{"key":"andy.koppe@gmail.com","avatar":"https://avatars.githubusercontent.com/u/223411?v=4"},"body":"In format_decorations(), don't obtain color sequences if there are no\ndecorations, and don't emit color sequences around empty strings.\n\nSigned-off-by: Andy Koppe <andy.koppe@gmail.com>\n---\n log-tree.c | 25 +++++++++++++++----------\n 1 file changed, 15 insertions(+), 10 deletions(-)\n\ndiff --git a/log-tree.c b/log-tree.c\nindex cd12c26c29..7c6d3f1ac3 100644\n--- a/log-tree.c\n+++ b/log-tree.c\n@@ -312,10 +312,7 @@ void format_decorations(struct strbuf *sb,\n {\n \tconst struct name_decoration *decoration;\n \tconst struct name_decoration *current_and_HEAD;\n-\tconst char *color_commit =\n-\t\tdiff_get_color(use_color, DIFF_COMMIT);\n-\tconst char *color_reset =\n-\t\tdecorate_get_color(use_color, DECORATION_NONE);\n+\tconst char *color_commit, *color_reset;\n \n \tconst char *prefix = \" (\";\n \tconst char *suffix = \")\";\n@@ -334,6 +331,9 @@ void format_decorations(struct strbuf *sb,\n \t\t\tseparator = opts->separator;\n \t}\n \n+\tcolor_commit = diff_get_color(use_color, DIFF_COMMIT);\n+\tcolor_reset = decorate_get_color(use_color, DECORATION_NONE);\n+\n \tcurrent_and_HEAD = current_pointed_by_HEAD(decoration);\n \twhile (decoration) {\n \t\t/*\n@@ -342,9 +342,12 @@ void format_decorations(struct strbuf *sb,\n \t\t * appeared, skipping the entry for current.\n \t\t */\n \t\tif (decoration != current_and_HEAD) {\n-\t\t\tstrbuf_addstr(sb, color_commit);\n-\t\t\tstrbuf_addstr(sb, prefix);\n-\t\t\tstrbuf_addstr(sb, color_reset);\n+\t\t\tif (*prefix) {\n+\t\t\t\tstrbuf_addstr(sb, color_commit);\n+\t\t\t\tstrbuf_addstr(sb, prefix);\n+\t\t\t\tstrbuf_addstr(sb, color_reset);\n+\t\t\t}\n+\n \t\t\tstrbuf_addstr(sb, decorate_get_color(use_color, decoration->type));\n \t\t\tif (decoration->type == DECORATION_REF_TAG)\n \t\t\t\tstrbuf_addstr(sb, \"tag: \");\n@@ -364,9 +367,11 @@ void format_decorations(struct strbuf *sb,\n \t\t}\n \t\tdecoration = decoration->next;\n \t}\n-\tstrbuf_addstr(sb, color_commit);\n-\tstrbuf_addstr(sb, suffix);\n-\tstrbuf_addstr(sb, color_reset);\n+\tif (*suffix) {\n+\t\tstrbuf_addstr(sb, color_commit);\n+\t\tstrbuf_addstr(sb, suffix);\n+\t\tstrbuf_addstr(sb, color_reset);\n+\t}\n }\n \n void show_decorations(struct rev_info *opt, struct commit *commit)\n-- \n2.42.0-rc1\n\n"},{"id":"480484","messageId":"20230810211619.19055-1-andy.koppe@gmail.com","threadId":"59990","inReplyTo":"20230715160730.4046-1-andy.koppe@gmail.com","subject":"[PATCH v3 1/7] pretty-formats: define \"literal formatting code\"","fromName":"Andy Koppe","fromEmail":"andy.koppe@gmail.com","sentAt":"2023-08-10T21:16:13Z","receivedAt":"2023-08-10T21:17:10Z","isPatch":true,"sender":{"key":"andy.koppe@gmail.com","avatar":"https://avatars.githubusercontent.com/u/223411?v=4"},"body":"From: Junio C Hamano <gitster@pobox.com>\n\nThe description for a %(trailer) option already uses this term without\nhaving a definition anywhere in the document, and we are about to add\nanother one in %(decorate) that uses it.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\nSigned-off-by: Andy Koppe <andy.koppe@gmail.com>\n---\n Documentation/pretty-formats.txt | 4 +++-\n 1 file changed, 3 insertions(+), 1 deletion(-)\n\ndiff --git a/Documentation/pretty-formats.txt b/Documentation/pretty-formats.txt\nindex 3b71334459..5e1432951b 100644\n--- a/Documentation/pretty-formats.txt\n+++ b/Documentation/pretty-formats.txt\n@@ -122,7 +122,9 @@ The placeholders are:\n - Placeholders that expand to a single literal character:\n '%n':: newline\n '%%':: a raw '%'\n-'%x00':: print a byte from a hex code\n+'%x00':: '%x' followed by two hexadecimal digits is replaced with a\n+\t byte with the hexadecimal digits' value (we will call this\n+\t \"literal formatting code\" in the rest of this document).\n \n - Placeholders that affect formatting of later placeholders:\n '%Cred':: switch color to red\n-- \n2.42.0-rc1\n\n"},{"id":"480485","messageId":"20230810211619.19055-3-andy.koppe@gmail.com","threadId":"59990","inReplyTo":"20230810211619.19055-1-andy.koppe@gmail.com","subject":"[PATCH v3 3/7] decorate: refactor format_decorations()","fromName":"Andy Koppe","fromEmail":"andy.koppe@gmail.com","sentAt":"2023-08-10T21:16:15Z","receivedAt":"2023-08-10T21:17:12Z","isPatch":true,"sender":{"key":"andy.koppe@gmail.com","avatar":"https://avatars.githubusercontent.com/u/223411?v=4"},"body":"Rename the format_decorations_extended function to format_decorations\nand drop the format_decorations wrapper macro. Pass the prefix, suffix\nand separator strings as a single 'struct format_decorations' pointer\nargument instead of separate arguments. Use default values defined in\nthe function when either the struct pointer or any of the struct fields\nare NULL. This is to ease extension with additional options.\n\nSigned-off-by: Andy Koppe <andy.koppe@gmail.com>\n---\n log-tree.c | 23 +++++++++++++++++------\n log-tree.h | 15 ++++++++-------\n pretty.c   |  6 ++++--\n 3 files changed, 29 insertions(+), 15 deletions(-)\n\ndiff --git a/log-tree.c b/log-tree.c\nindex 208c69cbb7..cd12c26c29 100644\n--- a/log-tree.c\n+++ b/log-tree.c\n@@ -303,14 +303,12 @@ static void show_name(struct strbuf *sb, const struct name_decoration *decoratio\n \n /*\n  * The caller makes sure there is no funny color before calling.\n- * format_decorations_extended makes sure the same after return.\n+ * format_decorations ensures the same after return.\n  */\n-void format_decorations_extended(struct strbuf *sb,\n+void format_decorations(struct strbuf *sb,\n \t\t\tconst struct commit *commit,\n \t\t\tint use_color,\n-\t\t\tconst char *prefix,\n-\t\t\tconst char *separator,\n-\t\t\tconst char *suffix)\n+\t\t\tconst struct decoration_options *opts)\n {\n \tconst struct name_decoration *decoration;\n \tconst struct name_decoration *current_and_HEAD;\n@@ -319,10 +317,23 @@ void format_decorations_extended(struct strbuf *sb,\n \tconst char *color_reset =\n \t\tdecorate_get_color(use_color, DECORATION_NONE);\n \n+\tconst char *prefix = \" (\";\n+\tconst char *suffix = \")\";\n+\tconst char *separator = \", \";\n+\n \tdecoration = get_name_decoration(&commit->object);\n \tif (!decoration)\n \t\treturn;\n \n+\tif (opts) {\n+\t\tif (opts->prefix)\n+\t\t\tprefix = opts->prefix;\n+\t\tif (opts->suffix)\n+\t\t\tsuffix = opts->suffix;\n+\t\tif (opts->separator)\n+\t\t\tseparator = opts->separator;\n+\t}\n+\n \tcurrent_and_HEAD = current_pointed_by_HEAD(decoration);\n \twhile (decoration) {\n \t\t/*\n@@ -370,7 +381,7 @@ void show_decorations(struct rev_info *opt, struct commit *commit)\n \t}\n \tif (!opt->show_decorations)\n \t\treturn;\n-\tformat_decorations(&sb, commit, opt->diffopt.use_color);\n+\tformat_decorations(&sb, commit, opt->diffopt.use_color, NULL);\n \tfputs(sb.buf, opt->diffopt.file);\n \tstrbuf_release(&sb);\n }\ndiff --git a/log-tree.h b/log-tree.h\nindex bdb6432815..14898de8ac 100644\n--- a/log-tree.h\n+++ b/log-tree.h\n@@ -13,17 +13,18 @@ struct decoration_filter {\n \tstruct string_list *exclude_ref_config_pattern;\n };\n \n+struct decoration_options {\n+\tchar *prefix;\n+\tchar *suffix;\n+\tchar *separator;\n+};\n+\n int parse_decorate_color_config(const char *var, const char *slot_name, const char *value);\n int log_tree_diff_flush(struct rev_info *);\n int log_tree_commit(struct rev_info *, struct commit *);\n void show_log(struct rev_info *opt);\n-void format_decorations_extended(struct strbuf *sb, const struct commit *commit,\n-\t\t\t     int use_color,\n-\t\t\t     const char *prefix,\n-\t\t\t     const char *separator,\n-\t\t\t     const char *suffix);\n-#define format_decorations(strbuf, commit, color) \\\n-\t\t\t     format_decorations_extended((strbuf), (commit), (color), \" (\", \", \", \")\")\n+void format_decorations(struct strbuf *sb, const struct commit *commit,\n+\t\t\tint use_color, const struct decoration_options *opts);\n void show_decorations(struct rev_info *opt, struct commit *commit);\n void log_write_email_headers(struct rev_info *opt, struct commit *commit,\n \t\t\t     const char **extra_headers_p,\ndiff --git a/pretty.c b/pretty.c\nindex 718530bbab..24fb82a5a2 100644\n--- a/pretty.c\n+++ b/pretty.c\n@@ -1537,10 +1537,12 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */\n \t\tstrbuf_addstr(sb, get_revision_mark(NULL, commit));\n \t\treturn 1;\n \tcase 'd':\n-\t\tformat_decorations(sb, commit, c->auto_color);\n+\t\tformat_decorations(sb, commit, c->auto_color, NULL);\n \t\treturn 1;\n \tcase 'D':\n-\t\tformat_decorations_extended(sb, commit, c->auto_color, \"\", \", \", \"\");\n+\t\tformat_decorations(sb, commit, c->auto_color,\n+\t\t\t&(struct decoration_options){.prefix = \"\",\n+\t\t\t\t\t\t     .suffix = \"\"});\n \t\treturn 1;\n \tcase 'S':\t\t/* tag/branch like --source */\n \t\tif (!(c->pretty_ctx->rev && c->pretty_ctx->rev->sources))\n-- \n2.42.0-rc1\n\n"},{"id":"480486","messageId":"20230810211619.19055-6-andy.koppe@gmail.com","threadId":"59990","inReplyTo":"20230810211619.19055-1-andy.koppe@gmail.com","subject":"[PATCH v3 6/7] pretty: add %(decorate[:<options>]) format","fromName":"Andy Koppe","fromEmail":"andy.koppe@gmail.com","sentAt":"2023-08-10T21:16:18Z","receivedAt":"2023-08-10T21:17:14Z","isPatch":true,"sender":{"key":"andy.koppe@gmail.com","avatar":"https://avatars.githubusercontent.com/u/223411?v=4"},"body":"Add %(decorate[:<options>]) format that lists ref names similarly to the\n%d format, but which allows the otherwise fixed prefix, suffix and\nseparator strings to be customized. Omitted options default to the\nstrings used in %d.\n\nRename expand_separator() function used to expand %x literal formatting\ncodes to expand_string_arg(), as it is now used on strings other than\nseparators.\n\nExamples:\n- %(decorate) is equivalent to %d.\n- %(decorate:prefix=,suffix=) is equivalent to %D.\n- %(decorate:prefix=[,suffix=],separator=%x3B) produces a list enclosed\nin square brackets and separated by semicolons.\n\nTest the format in t4205-log-pretty-formats.sh and document it in\npretty-formats.txt.\n\nSigned-off-by: Andy Koppe <andy.koppe@gmail.com>\n---\n Documentation/pretty-formats.txt | 10 ++++++\n pretty.c                         | 59 +++++++++++++++++++++++++++++---\n t/t4205-log-pretty-formats.sh    | 27 +++++++++++++++\n 3 files changed, 92 insertions(+), 4 deletions(-)\n\ndiff --git a/Documentation/pretty-formats.txt b/Documentation/pretty-formats.txt\nindex 851a9878e6..709d85af21 100644\n--- a/Documentation/pretty-formats.txt\n+++ b/Documentation/pretty-formats.txt\n@@ -224,6 +224,16 @@ The placeholders are:\n \tlinkgit:git-rev-list[1])\n '%d':: ref names, like the --decorate option of linkgit:git-log[1]\n '%D':: ref names without the \" (\", \")\" wrapping.\n+'%(decorate[:<options>])'::\n+ref names with custom decorations. The `decorate` string may be followed by a\n+colon and zero or more comma-separated options. Option values may contain\n+literal formatting codes. These must be used for commas (`%x2C`) and closing\n+parentheses (`%x29`), due to their role in the option syntax.\n++\n+** 'prefix=<value>': Shown before the list of ref names.  Defaults to \"{nbsp}`(`\".\n+** 'suffix=<value>': Shown after the list of ref names.  Defaults to \"`)`\".\n+** 'separator=<value>': Shown between ref names.  Defaults to \"`,`{nbsp}\".\n+\n '%(describe[:<options>])'::\n human-readable name, like linkgit:git-describe[1]; empty string for\n undescribable commits.  The `describe` string may be followed by a colon and\ndiff --git a/pretty.c b/pretty.c\nindex 24fb82a5a2..d972051543 100644\n--- a/pretty.c\n+++ b/pretty.c\n@@ -1252,8 +1252,8 @@ static int format_trailer_match_cb(const struct strbuf *key, void *ud)\n \treturn 0;\n }\n \n-static struct strbuf *expand_separator(struct strbuf *sb,\n-\t\t\t\t       const char *argval, size_t arglen)\n+static struct strbuf *expand_string_arg(struct strbuf *sb,\n+\t\t\t\t\tconst char *argval, size_t arglen)\n {\n \tchar *fmt = xstrndup(argval, arglen);\n \tconst char *format = fmt;\n@@ -1301,9 +1301,9 @@ int format_set_trailers_options(struct process_trailer_options *opts,\n \t\t\topts->filter_data = filter_list;\n \t\t\topts->only_trailers = 1;\n \t\t} else if (match_placeholder_arg_value(*arg, \"separator\", arg, &argval, &arglen)) {\n-\t\t\topts->separator = expand_separator(sepbuf, argval, arglen);\n+\t\t\topts->separator = expand_string_arg(sepbuf, argval, arglen);\n \t\t} else if (match_placeholder_arg_value(*arg, \"key_value_separator\", arg, &argval, &arglen)) {\n-\t\t\topts->key_value_separator = expand_separator(kvsepbuf, argval, arglen);\n+\t\t\topts->key_value_separator = expand_string_arg(kvsepbuf, argval, arglen);\n \t\t} else if (!match_placeholder_bool_arg(*arg, \"only\", arg, &opts->only_trailers) &&\n \t\t\t   !match_placeholder_bool_arg(*arg, \"unfold\", arg, &opts->unfold) &&\n \t\t\t   !match_placeholder_bool_arg(*arg, \"keyonly\", arg, &opts->key_only) &&\n@@ -1384,6 +1384,40 @@ static size_t parse_describe_args(const char *start, struct strvec *args)\n \treturn arg - start;\n }\n \n+\n+static int parse_decoration_option(const char **arg,\n+\t\t\t\t   const char *name,\n+\t\t\t\t   char **opt)\n+{\n+\tconst char *argval;\n+\tsize_t arglen;\n+\n+\tif (match_placeholder_arg_value(*arg, name, arg, &argval, &arglen)) {\n+\t\tstruct strbuf sb = STRBUF_INIT;\n+\n+\t\texpand_string_arg(&sb, argval, arglen);\n+\t\t*opt = strbuf_detach(&sb, NULL);\n+\t\treturn 1;\n+\t}\n+\treturn 0;\n+}\n+\n+static void parse_decoration_options(const char **arg,\n+\t\t\t\t     struct decoration_options *opts)\n+{\n+\twhile (parse_decoration_option(arg, \"prefix\", &opts->prefix) ||\n+\t       parse_decoration_option(arg, \"suffix\", &opts->suffix) ||\n+\t       parse_decoration_option(arg, \"separator\", &opts->separator))\n+\t\t;\n+}\n+\n+static void free_decoration_options(const struct decoration_options *opts)\n+{\n+\tfree(opts->prefix);\n+\tfree(opts->suffix);\n+\tfree(opts->separator);\n+}\n+\n static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */\n \t\t\t\tconst char *placeholder,\n \t\t\t\tvoid *context)\n@@ -1640,6 +1674,23 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */\n \t\treturn 2;\n \t}\n \n+\tif (skip_prefix(placeholder, \"(decorate\", &arg)) {\n+\t\tstruct decoration_options opts = { NULL };\n+\t\tsize_t ret = 0;\n+\n+\t\tif (*arg == ':') {\n+\t\t\targ++;\n+\t\t\tparse_decoration_options(&arg, &opts);\n+\t\t}\n+\t\tif (*arg == ')') {\n+\t\t\tformat_decorations(sb, commit, c->auto_color, &opts);\n+\t\t\tret = arg - placeholder + 1;\n+\t\t}\n+\n+\t\tfree_decoration_options(&opts);\n+\t\treturn ret;\n+\t}\n+\n \t/* For the rest we have to parse the commit header. */\n \tif (!c->commit_header_parsed) {\n \t\tmsg = c->message =\ndiff --git a/t/t4205-log-pretty-formats.sh b/t/t4205-log-pretty-formats.sh\nindex dd9035aa38..6ba399c5be 100755\n--- a/t/t4205-log-pretty-formats.sh\n+++ b/t/t4205-log-pretty-formats.sh\n@@ -576,6 +576,33 @@ test_expect_success 'clean log decoration' '\n \ttest_cmp expected actual1\n '\n \n+test_expect_success 'pretty format %decorate' '\n+\tgit checkout -b foo &&\n+\tgit commit --allow-empty -m \"new commit\" &&\n+\tgit tag bar &&\n+\tgit branch qux &&\n+\n+\techo \" (HEAD -> foo, tag: bar, qux)\" >expect1 &&\n+\tgit log --format=\"%(decorate)\" -1 >actual1 &&\n+\ttest_cmp expect1 actual1 &&\n+\n+\techo \"HEAD -> foo, tag: bar, qux\" >expect2 &&\n+\tgit log --format=\"%(decorate:prefix=,suffix=)\" -1 >actual2 &&\n+\ttest_cmp expect2 actual2 &&\n+\n+\techo \"[ HEAD -> foo; tag: bar; qux ]\" >expect3 &&\n+\tgit log --format=\"%(decorate:prefix=[ ,suffix= ],separator=%x3B )\" \\\n+\t\t-1 >actual3 &&\n+\ttest_cmp expect3 actual3 &&\n+\n+\t# Try with a typo (in \"separator\"), in which case the placeholder should\n+\t# not be replaced.\n+\techo \"%(decorate:prefix=[ ,suffix= ],separater=; )\" >expect4 &&\n+\tgit log --format=\"%(decorate:prefix=[ ,suffix= ],separater=%x3B )\" \\\n+\t\t-1 >actual4 &&\n+\ttest_cmp expect4 actual4\n+'\n+\n cat >trailers <<EOF\n Signed-off-by: A U Thor <author@example.com>\n Acked-by: A U Thor <author@example.com>\n-- \n2.42.0-rc1\n\n"},{"id":"480487","messageId":"20230810211619.19055-5-andy.koppe@gmail.com","threadId":"59990","inReplyTo":"20230810211619.19055-1-andy.koppe@gmail.com","subject":"[PATCH v3 5/7] decorate: color each token separately","fromName":"Andy Koppe","fromEmail":"andy.koppe@gmail.com","sentAt":"2023-08-10T21:16:17Z","receivedAt":"2023-08-10T21:17:16Z","isPatch":true,"sender":{"key":"andy.koppe@gmail.com","avatar":"https://avatars.githubusercontent.com/u/223411?v=4"},"body":"Wrap \"tag:\" prefixes and the arrows in \"HEAD -> branch\" annotations in\ntheir own color sequences, because otherwise tag names or arrows can end\nup uncolored when %w width formatting breaks lines just before them.\n\nUse the commit color for arrows, for visual consistency with the '(',\n',' and ')' symbols used as prefix, separator and suffix, which are also\ncolored with the commit color.\n\nAmend test t4207-log-decoration-colors.sh accordingly.\n\nSigned-off-by: Andy Koppe <andy.koppe@gmail.com>\n---\n log-tree.c                       | 14 +++++++---\n t/t4207-log-decoration-colors.sh | 44 ++++++++++++++++++--------------\n 2 files changed, 36 insertions(+), 22 deletions(-)\n\ndiff --git a/log-tree.c b/log-tree.c\nindex 7c6d3f1ac3..3b62dca048 100644\n--- a/log-tree.c\n+++ b/log-tree.c\n@@ -342,26 +342,34 @@ void format_decorations(struct strbuf *sb,\n \t\t * appeared, skipping the entry for current.\n \t\t */\n \t\tif (decoration != current_and_HEAD) {\n+\t\t\tconst char *color =\n+\t\t\t\tdecorate_get_color(use_color, decoration->type);\n+\n \t\t\tif (*prefix) {\n \t\t\t\tstrbuf_addstr(sb, color_commit);\n \t\t\t\tstrbuf_addstr(sb, prefix);\n \t\t\t\tstrbuf_addstr(sb, color_reset);\n \t\t\t}\n \n-\t\t\tstrbuf_addstr(sb, decorate_get_color(use_color, decoration->type));\n-\t\t\tif (decoration->type == DECORATION_REF_TAG)\n+\t\t\tif (decoration->type == DECORATION_REF_TAG) {\n+\t\t\t\tstrbuf_addstr(sb, color);\n \t\t\t\tstrbuf_addstr(sb, \"tag: \");\n+\t\t\t\tstrbuf_addstr(sb, color_reset);\n+\t\t\t}\n \n+\t\t\tstrbuf_addstr(sb, color);\n \t\t\tshow_name(sb, decoration);\n+\t\t\tstrbuf_addstr(sb, color_reset);\n \n \t\t\tif (current_and_HEAD &&\n \t\t\t    decoration->type == DECORATION_REF_HEAD) {\n+\t\t\t\tstrbuf_addstr(sb, color_commit);\n \t\t\t\tstrbuf_addstr(sb, \" -> \");\n \t\t\t\tstrbuf_addstr(sb, color_reset);\n \t\t\t\tstrbuf_addstr(sb, decorate_get_color(use_color, current_and_HEAD->type));\n \t\t\t\tshow_name(sb, current_and_HEAD);\n+\t\t\t\tstrbuf_addstr(sb, color_reset);\n \t\t\t}\n-\t\t\tstrbuf_addstr(sb, color_reset);\n \n \t\t\tprefix = separator;\n \t\t}\ndiff --git a/t/t4207-log-decoration-colors.sh b/t/t4207-log-decoration-colors.sh\nindex ded33a82e2..21986a866d 100755\n--- a/t/t4207-log-decoration-colors.sh\n+++ b/t/t4207-log-decoration-colors.sh\n@@ -53,15 +53,17 @@ cmp_filtered_decorations () {\n # to this test since it does not contain any decoration, hence --first-parent\n test_expect_success 'commit decorations colored correctly' '\n \tcat >expect <<-EOF &&\n-\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_HEAD}HEAD -> \\\n-${c_reset}${c_branch}main${c_reset}${c_commit}, \\\n-${c_reset}${c_tag}tag: v1.0${c_reset}${c_commit}, \\\n-${c_reset}${c_tag}tag: B${c_reset}${c_commit})${c_reset} B\n-${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_tag}tag: A1${c_reset}${c_commit}, \\\n+\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_HEAD}HEAD${c_reset}\\\n+${c_commit} -> ${c_reset}${c_branch}main${c_reset}${c_commit}, \\\n+${c_reset}${c_tag}tag: ${c_reset}${c_tag}v1.0${c_reset}${c_commit}, \\\n+${c_reset}${c_tag}tag: ${c_reset}${c_tag}B${c_reset}${c_commit})${c_reset} B\n+${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\\\n+${c_tag}tag: ${c_reset}${c_tag}A1${c_reset}${c_commit}, \\\n ${c_reset}${c_remoteBranch}other/main${c_reset}${c_commit})${c_reset} A1\n-\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_stash}refs/stash${c_reset}${c_commit})${c_reset} \\\n-On main: Changes to A.t\n-\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_tag}tag: A${c_reset}${c_commit})${c_reset} A\n+\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\\\n+${c_stash}refs/stash${c_reset}${c_commit})${c_reset} On main: Changes to A.t\n+\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\\\n+${c_tag}tag: ${c_reset}${c_tag}A${c_reset}${c_commit})${c_reset} A\n \tEOF\n \n \tgit log --first-parent --no-abbrev --decorate --oneline --color=always --all >actual &&\n@@ -76,12 +78,14 @@ test_expect_success 'test coloring with replace-objects' '\n \tgit replace HEAD~1 HEAD~2 &&\n \n \tcat >expect <<-EOF &&\n-\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_HEAD}HEAD -> \\\n-${c_reset}${c_branch}main${c_reset}${c_commit}, \\\n-${c_reset}${c_tag}tag: D${c_reset}${c_commit})${c_reset} D\n-\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_tag}tag: C${c_reset}${c_commit}, \\\n+\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_HEAD}HEAD${c_reset}\\\n+${c_commit} -> ${c_reset}${c_branch}main${c_reset}${c_commit}, \\\n+${c_reset}${c_tag}tag: ${c_reset}${c_tag}D${c_reset}${c_commit})${c_reset} D\n+\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\\\n+${c_tag}tag: ${c_reset}${c_tag}C${c_reset}${c_commit}, \\\n ${c_reset}${c_grafted}replaced${c_reset}${c_commit})${c_reset} B\n-\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_tag}tag: A${c_reset}${c_commit})${c_reset} A\n+\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\\\n+${c_tag}tag: ${c_reset}${c_tag}A${c_reset}${c_commit})${c_reset} A\n EOF\n \n \tgit log --first-parent --no-abbrev --decorate --oneline --color=always HEAD >actual &&\n@@ -100,13 +104,15 @@ test_expect_success 'test coloring with grafted commit' '\n \tgit replace --graft HEAD HEAD~2 &&\n \n \tcat >expect <<-EOF &&\n-\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_HEAD}HEAD -> \\\n-${c_reset}${c_branch}main${c_reset}${c_commit}, \\\n-${c_reset}${c_tag}tag: D${c_reset}${c_commit}, \\\n+\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_HEAD}HEAD${c_reset}\\\n+${c_commit} -> ${c_reset}${c_branch}main${c_reset}${c_commit}, \\\n+${c_reset}${c_tag}tag: ${c_reset}${c_tag}D${c_reset}${c_commit}, \\\n ${c_reset}${c_grafted}replaced${c_reset}${c_commit})${c_reset} D\n-\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_tag}tag: v1.0${c_reset}${c_commit}, \\\n-${c_reset}${c_tag}tag: B${c_reset}${c_commit})${c_reset} B\n-\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_tag}tag: A${c_reset}${c_commit})${c_reset} A\n+\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\\\n+${c_tag}tag: ${c_reset}${c_tag}v1.0${c_reset}${c_commit}, \\\n+${c_reset}${c_tag}tag: ${c_reset}${c_tag}B${c_reset}${c_commit})${c_reset} B\n+\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\\\n+${c_tag}tag: ${c_reset}${c_tag}A${c_reset}${c_commit})${c_reset} A\n \tEOF\n \n \tgit log --first-parent --no-abbrev --decorate --oneline --color=always HEAD >actual &&\n-- \n2.42.0-rc1\n\n"},{"id":"480488","messageId":"20230810211619.19055-7-andy.koppe@gmail.com","threadId":"59990","inReplyTo":"20230810211619.19055-1-andy.koppe@gmail.com","subject":"[PATCH v3 7/7] pretty: add pointer and tag options to %(decorate)","fromName":"Andy Koppe","fromEmail":"andy.koppe@gmail.com","sentAt":"2023-08-10T21:16:19Z","receivedAt":"2023-08-10T21:17:17Z","isPatch":true,"sender":{"key":"andy.koppe@gmail.com","avatar":"https://avatars.githubusercontent.com/u/223411?v=4"},"body":"Add pointer and tag options to %(decorate) format, to allow to override\nthe \" -> \" string used to show where HEAD points and the \"tag: \" string\nused to mark tags.\n\nDocument in pretty-formats.txt and test in t4205-log-pretty-formats.sh.\n\nSigned-off-by: Andy Koppe <andy.koppe@gmail.com>\n---\n Documentation/pretty-formats.txt |  9 +++++++++\n log-tree.c                       | 12 +++++++++---\n log-tree.h                       |  2 ++\n pretty.c                         |  6 +++++-\n t/t4205-log-pretty-formats.sh    |  7 ++++++-\n 5 files changed, 31 insertions(+), 5 deletions(-)\n\ndiff --git a/Documentation/pretty-formats.txt b/Documentation/pretty-formats.txt\nindex 709d85af21..d38b4ab566 100644\n--- a/Documentation/pretty-formats.txt\n+++ b/Documentation/pretty-formats.txt\n@@ -233,6 +233,15 @@ parentheses (`%x29`), due to their role in the option syntax.\n ** 'prefix=<value>': Shown before the list of ref names.  Defaults to \"{nbsp}`(`\".\n ** 'suffix=<value>': Shown after the list of ref names.  Defaults to \"`)`\".\n ** 'separator=<value>': Shown between ref names.  Defaults to \"`,`{nbsp}\".\n+** 'pointer=<value>': Shown between HEAD and the branch it points to, if any.\n+\t\t      Defaults to \"{nbsp}`->`{nbsp}\".\n+** 'tag=<value>': Shown before tag names. Defaults to \"`tag:`{nbsp}\".\n+\n++\n+For example, to produce decorations with no wrapping\n+or tag annotations, and spaces as separators:\n++\n+`%(decorate:prefix=,suffix=,tag=,separator= )`\n \n '%(describe[:<options>])'::\n human-readable name, like linkgit:git-describe[1]; empty string for\ndiff --git a/log-tree.c b/log-tree.c\nindex 3b62dca048..504da6b519 100644\n--- a/log-tree.c\n+++ b/log-tree.c\n@@ -317,6 +317,8 @@ void format_decorations(struct strbuf *sb,\n \tconst char *prefix = \" (\";\n \tconst char *suffix = \")\";\n \tconst char *separator = \", \";\n+\tconst char *pointer = \" -> \";\n+\tconst char *tag = \"tag: \";\n \n \tdecoration = get_name_decoration(&commit->object);\n \tif (!decoration)\n@@ -329,6 +331,10 @@ void format_decorations(struct strbuf *sb,\n \t\t\tsuffix = opts->suffix;\n \t\tif (opts->separator)\n \t\t\tseparator = opts->separator;\n+\t\tif (opts->pointer)\n+\t\t\tpointer = opts->pointer;\n+\t\tif (opts->tag)\n+\t\t\ttag = opts->tag;\n \t}\n \n \tcolor_commit = diff_get_color(use_color, DIFF_COMMIT);\n@@ -351,9 +357,9 @@ void format_decorations(struct strbuf *sb,\n \t\t\t\tstrbuf_addstr(sb, color_reset);\n \t\t\t}\n \n-\t\t\tif (decoration->type == DECORATION_REF_TAG) {\n+\t\t\tif (*tag && decoration->type == DECORATION_REF_TAG) {\n \t\t\t\tstrbuf_addstr(sb, color);\n-\t\t\t\tstrbuf_addstr(sb, \"tag: \");\n+\t\t\t\tstrbuf_addstr(sb, tag);\n \t\t\t\tstrbuf_addstr(sb, color_reset);\n \t\t\t}\n \n@@ -364,7 +370,7 @@ void format_decorations(struct strbuf *sb,\n \t\t\tif (current_and_HEAD &&\n \t\t\t    decoration->type == DECORATION_REF_HEAD) {\n \t\t\t\tstrbuf_addstr(sb, color_commit);\n-\t\t\t\tstrbuf_addstr(sb, \" -> \");\n+\t\t\t\tstrbuf_addstr(sb, pointer);\n \t\t\t\tstrbuf_addstr(sb, color_reset);\n \t\t\t\tstrbuf_addstr(sb, decorate_get_color(use_color, current_and_HEAD->type));\n \t\t\t\tshow_name(sb, current_and_HEAD);\ndiff --git a/log-tree.h b/log-tree.h\nindex 14898de8ac..41c776fea5 100644\n--- a/log-tree.h\n+++ b/log-tree.h\n@@ -17,6 +17,8 @@ struct decoration_options {\n \tchar *prefix;\n \tchar *suffix;\n \tchar *separator;\n+\tchar *pointer;\n+\tchar *tag;\n };\n \n int parse_decorate_color_config(const char *var, const char *slot_name, const char *value);\ndiff --git a/pretty.c b/pretty.c\nindex d972051543..5effbe9ad9 100644\n--- a/pretty.c\n+++ b/pretty.c\n@@ -1407,7 +1407,9 @@ static void parse_decoration_options(const char **arg,\n {\n \twhile (parse_decoration_option(arg, \"prefix\", &opts->prefix) ||\n \t       parse_decoration_option(arg, \"suffix\", &opts->suffix) ||\n-\t       parse_decoration_option(arg, \"separator\", &opts->separator))\n+\t       parse_decoration_option(arg, \"separator\", &opts->separator) ||\n+\t       parse_decoration_option(arg, \"pointer\", &opts->pointer) ||\n+\t       parse_decoration_option(arg, \"tag\", &opts->tag))\n \t\t;\n }\n \n@@ -1416,6 +1418,8 @@ static void free_decoration_options(const struct decoration_options *opts)\n \tfree(opts->prefix);\n \tfree(opts->suffix);\n \tfree(opts->separator);\n+\tfree(opts->pointer);\n+\tfree(opts->tag);\n }\n \n static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */\ndiff --git a/t/t4205-log-pretty-formats.sh b/t/t4205-log-pretty-formats.sh\nindex 6ba399c5be..16626e4fe9 100755\n--- a/t/t4205-log-pretty-formats.sh\n+++ b/t/t4205-log-pretty-formats.sh\n@@ -600,7 +600,12 @@ test_expect_success 'pretty format %decorate' '\n \techo \"%(decorate:prefix=[ ,suffix= ],separater=; )\" >expect4 &&\n \tgit log --format=\"%(decorate:prefix=[ ,suffix= ],separater=%x3B )\" \\\n \t\t-1 >actual4 &&\n-\ttest_cmp expect4 actual4\n+\ttest_cmp expect4 actual4 &&\n+\n+\techo \"HEAD->foo bar qux\" >expect5 &&\n+\tgit log --format=\"%(decorate:prefix=,suffix=,separator= ,tag=,pointer=->)\" \\\n+\t\t-1 >actual5 &&\n+\ttest_cmp expect5 actual5\n '\n \n cat >trailers <<EOF\n-- \n2.42.0-rc1\n\n"},{"id":"480574","messageId":"CAHWeT-ZSirWAQcw8fDNp=Qxik5wg8PMPmBEj_n1Hg5q3ZbvVmw@mail.gmail.com","threadId":"59990","inReplyTo":"xmqq7cqydo5v.fsf@gitster.g","subject":"Re: [PATCH v2] pretty: add %(decorate[:<options>]) format","fromName":"Andy Koppe","fromEmail":"andy.koppe@gmail.com","sentAt":"2023-08-11T18:50:45Z","receivedAt":"2023-08-11T18:51:03Z","isPatch":true,"sender":{"key":"andy.koppe@gmail.com","avatar":"https://avatars.githubusercontent.com/u/223411?v=4"},"body":"Junio C Hamano <gitster@pobox.com> wrote:\n> Overall, the patch seems to be done very well when viewed as a\n> whole.  Thanks for working on it.\n>\n> It is just I cannot be as confident as I would like to be in my\n> review when the single patch does several different things at once.\n> If it were split in steps, each step focusing on doing a single\n> thing well and describing well what it does and why, reviewers can\n> be more confident that they did not miss something important in the\n> patch(es).\n\nThanks to Junio, Glen, Phil and the Review Club for the helpful\nreviews, especially the guidance on commit granularity.\n\nSorry for not getting back to this sooner, but the v3 patch series\naddressing the review comments is now here:\nhttps://lore.kernel.org/git/20230715160730.4046-1-andy.koppe@gmail.com/T/#m46ad3ebbe3163821f649f7122edcabd619fc5837\n\nKind regards,\nAndy\n"},{"id":"480575","messageId":"CAHWeT-ZA8f-TGRwDHixAvi5kddVBbuK8LpVGJ9cjYZMsMk5ODw@mail.gmail.com","threadId":"59990","inReplyTo":"kl6l351j22dr.fsf@chooglen-macbookpro.roam.corp.google.com","subject":"Re: [PATCH v2] pretty: add %(decorate[:<options>]) format","fromName":"Andy Koppe","fromEmail":"andy.koppe@gmail.com","sentAt":"2023-08-11T18:59:29Z","receivedAt":"2023-08-11T18:59:46Z","isPatch":true,"sender":{"key":"andy.koppe@gmail.com","avatar":"https://avatars.githubusercontent.com/u/223411?v=4"},"body":"On Wed, 19 Jul 2023 at 19:16, Glen Choo  wrote:\n\n> As a micro-nit: there's some useful context behind your chosen design in\n> [1]. It would have been useful to link to it in the `---` context, or\n> perhaps send this series as v3 and v4 to [1].\n>\n> [1] https://lore.kernel.org/git/20230712110732.8274-1-andy.koppe@gmail.com/\n\nPoint taken.\n\n> > +             strbuf_expand(&sb, val, strbuf_expand_literal_cb, NULL);\n>\n> strbuf_expand() got removed in 'master' recently, so this should be\n> rebased.\n\nDone. I think I had started off main, wrongly assuming that it's the\nsame as master.\n\nThanks again,\nAndy\n"},{"id":"480576","messageId":"CAHWeT-Zo3nTTGBfJPwNhg50KfLn-GAdpU8WZ96d7fT9_axAQXg@mail.gmail.com","threadId":"59990","inReplyTo":"783011d8-53ea-15cb-a9c7-6cb0c15bd5aa@gmail.com","subject":"Re: [PATCH v2] pretty: add %(decorate[:<options>]) format","fromName":"Andy Koppe","fromEmail":"andy.koppe@gmail.com","sentAt":"2023-08-11T19:04:51Z","receivedAt":"2023-08-11T19:05:08Z","isPatch":true,"sender":{"key":"andy.koppe@gmail.com","avatar":"https://avatars.githubusercontent.com/u/223411?v=4"},"body":"On Sun, 23 Jul 2023 at 17:26, Phillip Wood  wrote:\n>\n> On 19/07/2023 19:16, Glen Choo wrote:\n> >>      case 'D':\n> >> -            format_decorations_extended(sb, commit, c->auto_color, \"\", \", \", \"\");\n> >> +            format_decorations(sb, commit, c->auto_color,\n> >> +                               &(struct decoration_options){\"\", \"\"});\n> >\n> > I don't remember if C99 lets you name .prefix and .suffix here, but if\n> > so, it would be good to name them. Otherwise it's easy to get the order\n> > wrong, e.g. if someone reorders the fields in struct decoration_options.\n>\n> That's a good suggestion. I think this would be the first use of a\n> compound literal in the code base so it would be helpful to mention that\n> in the commit message.\n\nI've taken the suggestion, but then forgot to mention it in the commit\nmessage. Will do in the next round.\n\n> We've been depending on C99 for a while now so I'd support adding this\n> compound literal as a test balloon for compiler support. Ævar reported a\n> while back that they are supported by IBM xlc, Oracle SunCC and HP/UX's\n> aCC[1] and back then I looked at NonStop which seemed to offer support\n> with the right compiler flag.\n\nThere are a number of uses of designated initializers already, so\nhopefully compound literals aren't too much of an extra challenge.\n\nThanks,\nAndy\n"},{"id":"480583","messageId":"xmqqv8dlqpq7.fsf@gitster.g","threadId":"59990","inReplyTo":"CAHWeT-Zo3nTTGBfJPwNhg50KfLn-GAdpU8WZ96d7fT9_axAQXg@mail.gmail.com","subject":"Re: [PATCH v2] pretty: add %(decorate[:<options>]) format","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2023-08-11T20:38:24Z","receivedAt":"2023-08-11T20:38:33Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Andy Koppe <andy.koppe@gmail.com> writes:\n\n> There are a number of uses of designated initializers already, so\n> hopefully compound literals aren't too much of an extra challenge.\n\nI do not see how one leads to the other here.  I'd prefer not to see\nuse of a new construct we do not currently use mixed in a new code,\neven if it is mentioned in the proposed log message.\n\nIf we want to use compound literals in our codebase in the longer\nterm, we should first add a weatherballoon use to a very stable part\nof the codebase that rarely changes, in a single patch that is\ntrivial to revert when a platform that matters is found to have\nproblem with the language construct, just like what we did when we\nadopted the use of designated initializers.\n\nThanks.\n\n"},{"id":"480601","messageId":"61337dad-e1db-4f08-84f5-404bacc6c9bf@gmail.com","threadId":"59990","inReplyTo":"xmqqv8dlqpq7.fsf@gitster.g","subject":"Re: [PATCH v2] pretty: add %(decorate[:<options>]) format","fromName":"Andy Koppe","fromEmail":"andy.koppe@gmail.com","sentAt":"2023-08-11T22:06:57Z","receivedAt":"2023-08-11T22:07:20Z","isPatch":true,"sender":{"key":"andy.koppe@gmail.com","avatar":"https://avatars.githubusercontent.com/u/223411?v=4"},"body":"On 11/08/2023 21:38, Junio C Hamano wrote:\n > Andy Koppe <andy.koppe@gmail.com> writes:\n >\n >> There are a number of uses of designated initializers already, so\n >> hopefully compound literals aren't too much of an extra challenge.\n >\n > I do not see how one leads to the other here.  I'd prefer not to see\n > use of a new construct we do not currently use mixed in a new code,\n > even if it is mentioned in the proposed log message.\n\nOkay.\n\nWould this style be acceptable to fulfil Glen's request to name the\nfields?\n\n\tcase 'D':\n\t\t{\n\t\t\tconst struct decoration_options opts = {\n\t\t\t\t.prefix = \"\",\n\t\t\t\t.suffix = \"\"\n\t\t\t};\n\n\t\t\tformat_decorations(sb, commit, c->auto_color, &opts);\n\t\t}\n\t\treturn 1;\n\nAndy\n"},{"id":"480621","messageId":"xmqqcyztnjpz.fsf@gitster.g","threadId":"59990","inReplyTo":"61337dad-e1db-4f08-84f5-404bacc6c9bf@gmail.com","subject":"Re: [PATCH v2] pretty: add %(decorate[:<options>]) format","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2023-08-12T01:16:24Z","receivedAt":"2023-08-12T01:16:28Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Andy Koppe <andy.koppe@gmail.com> writes:\n\n> On 11/08/2023 21:38, Junio C Hamano wrote:\n>> Andy Koppe <andy.koppe@gmail.com> writes:\n>>\n>>> There are a number of uses of designated initializers already, so\n>>> hopefully compound literals aren't too much of an extra challenge.\n>>\n>> I do not see how one leads to the other here.  I'd prefer not to see\n>> use of a new construct we do not currently use mixed in a new code,\n>> even if it is mentioned in the proposed log message.\n>\n> Okay.\n>\n> Would this style be acceptable to fulfil Glen's request to name the\n> fields?\n>\n> \tcase 'D':\n> \t\t{\n> \t\t\tconst struct decoration_options opts = {\n> \t\t\t\t.prefix = \"\",\n> \t\t\t\t.suffix = \"\"\n> \t\t\t};\n>\n> \t\t\tformat_decorations(sb, commit, c->auto_color, &opts);\n> \t\t}\n> \t\treturn 1;\n>\n> Andy\n\nSounds good to me.\n"},{"id":"480680","messageId":"xmqq7cpwjhr1.fsf@gitster.g","threadId":"59990","inReplyTo":"CAHWeT-ZA8f-TGRwDHixAvi5kddVBbuK8LpVGJ9cjYZMsMk5ODw@mail.gmail.com","subject":"Re: [PATCH v2] pretty: add %(decorate[:<options>]) format","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2023-08-15T18:13:54Z","receivedAt":"2023-08-15T18:14:48Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Andy Koppe <andy.koppe@gmail.com> writes:\n\n> Done. I think I had started off main, wrongly assuming that it's the\n> same as master.\n\nIf there is 'main' that is different from 'master', that sounds like\na problem to me.  This project predates the newer convention that\nallows the primary branch to be named 'main', but many new folks of\ncourse expect to see 'main', so while my primary working areas all\ncall the primary branch 'master', it is pushed out to both names.\n\nOr at least I thought I arranged that to happen.\n\nThanks.\n\n\n"},{"id":"480681","messageId":"006574bd-4d53-495e-8cfe-677ede521908@gmail.com","threadId":"59990","inReplyTo":"xmqq7cpwjhr1.fsf@gitster.g","subject":"Re: [PATCH v2] pretty: add %(decorate[:<options>]) format","fromName":"Andy Koppe","fromEmail":"andy.koppe@gmail.com","sentAt":"2023-08-15T18:28:48Z","receivedAt":"2023-08-15T18:29:56Z","isPatch":true,"sender":{"key":"andy.koppe@gmail.com","avatar":"https://avatars.githubusercontent.com/u/223411?v=4"},"body":"On 15/08/2023 19:13, Junio C Hamano wrote:\n> If there is 'main' that is different from 'master', that sounds like\n> a problem to me.  This project predates the newer convention that\n> allows the primary branch to be named 'main', but many new folks of\n> course expect to see 'main', so while my primary working areas all\n> call the primary branch 'master', it is pushed out to both names.\n> \n> Or at least I thought I arranged that to happen.\n\nSee [1], where main currently is at v2.41.0.\n\nRegards,\nAndy\n\n[1] https://github.com/git/git/tree/main\n"},{"id":"480682","messageId":"xmqq350kjfk8.fsf@gitster.g","threadId":"59990","inReplyTo":"006574bd-4d53-495e-8cfe-677ede521908@gmail.com","subject":"Re: [PATCH v2] pretty: add %(decorate[:<options>]) format","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2023-08-15T19:01:11Z","receivedAt":"2023-08-15T19:03:19Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Andy Koppe <andy.koppe@gmail.com> writes:\n\n> On 15/08/2023 19:13, Junio C Hamano wrote:\n>> If there is 'main' that is different from 'master', that sounds like\n>> a problem to me.  This project predates the newer convention that\n>> allows the primary branch to be named 'main', but many new folks of\n>> course expect to see 'main', so while my primary working areas all\n>> call the primary branch 'master', it is pushed out to both names.\n>> Or at least I thought I arranged that to happen.\n>\n> See [1], where main currently is at v2.41.0.\n>\n> Regards,\n> Andy\n>\n> [1] https://github.com/git/git/tree/main\n\nAh, that one.  The CI job is unfortunately attached to that tree and\nupdating 'master' and 'main' with the same commit at the same time\nwastes CI cycles, so I had to tentatively stop updating it.\n\nIt used to be that 'main' was set to lag behind 'master' by 24 hours\nor so to prevent the problem---CI notices that the commit updated\n'main' has been already dealt with 24 hours ago at 'master' and\nrefrains from wasting time on it.  But resurrecting it would still\nmake folks confused about how 'main' is different from 'master'.\nPerhaps it is a good time to remove stale 'main' and keep only\n'master' there?\n\n\n\n"},{"id":"480683","messageId":"ce5d20db-2220-45dd-8c39-2a52e0f9f816@gmail.com","threadId":"59990","inReplyTo":"xmqq350kjfk8.fsf@gitster.g","subject":"main != master at github.com/git/git","fromName":"Andy Koppe","fromEmail":"andy.koppe@gmail.com","sentAt":"2023-08-15T19:29:43Z","receivedAt":"2023-08-15T19:30:47Z","isPatch":false,"sender":{"key":"andy.koppe@gmail.com","avatar":"https://avatars.githubusercontent.com/u/223411?v=4"},"body":"On 15/08/2023 20:01, Junio C Hamano wrote:\n>> See [1], where main currently is at v2.41.0.\n>>\n>> [1] https://github.com/git/git/tree/main\n> \n> Ah, that one.  The CI job is unfortunately attached to that tree and\n> updating 'master' and 'main' with the same commit at the same time\n> wastes CI cycles, so I had to tentatively stop updating it.\n> \n> It used to be that 'main' was set to lag behind 'master' by 24 hours\n> or so to prevent the problem---CI notices that the commit updated\n> 'main' has been already dealt with 24 hours ago at 'master' and\n> refrains from wasting time on it.  But resurrecting it would still\n> make folks confused about how 'main' is different from 'master'.\n> Perhaps it is a good time to remove stale 'main' and keep only\n> 'master' there?\n\nAn alternative might be to exclude one of the branches in the workflow \nfile, as per [1].\n\nAndy\n\n[1] \nhttps://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#example-excluding-branches\n"},{"id":"480689","messageId":"ZNv5PQlkn6tbUcH7@nand.local","threadId":"59990","inReplyTo":"ce5d20db-2220-45dd-8c39-2a52e0f9f816@gmail.com","subject":"Re: main != master at github.com/git/git","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2023-08-15T22:16:29Z","receivedAt":"2023-08-15T22:17:14Z","isPatch":false,"sender":{"key":"me@ttaylorr.com","avatar":"https://avatars.githubusercontent.com/u/301000140?v=4"},"body":"On Tue, Aug 15, 2023 at 08:29:43PM +0100, Andy Koppe wrote:\n> On 15/08/2023 20:01, Junio C Hamano wrote:\n> > > See [1], where main currently is at v2.41.0.\n> > >\n> > > [1] https://github.com/git/git/tree/main\n> >\n> > Ah, that one.  The CI job is unfortunately attached to that tree and\n> > updating 'master' and 'main' with the same commit at the same time\n> > wastes CI cycles, so I had to tentatively stop updating it.\n> >\n> > It used to be that 'main' was set to lag behind 'master' by 24 hours\n> > or so to prevent the problem---CI notices that the commit updated\n> > 'main' has been already dealt with 24 hours ago at 'master' and\n> > refrains from wasting time on it.  But resurrecting it would still\n> > make folks confused about how 'main' is different from 'master'.\n> > Perhaps it is a good time to remove stale 'main' and keep only\n> > 'master' there?\n>\n> An alternative might be to exclude one of the branches in the workflow file,\n> as per [1].\n\nI think that this should be relatively straightforward to do, and would\nbe preferable to dropping 'main'.\n\nHere's an (untested) patch that should do the trick:\n\n--- >8 ---\ndiff --git a/.github/workflows/check-whitespace.yml b/.github/workflows/check-whitespace.yml\nindex a58e2dc8ad..764f46b21f 100644\n--- a/.github/workflows/check-whitespace.yml\n+++ b/.github/workflows/check-whitespace.yml\n@@ -8,6 +8,8 @@ name: check-whitespace\n on:\n   pull_request:\n     types: [opened, synchronize]\n+    branches_ignore:\n+      - main\n\n # Avoid unnecessary builds. Unlike the main CI jobs, these are not\n # ci-configurable (but could be).\ndiff --git a/.github/workflows/l10n.yml b/.github/workflows/l10n.yml\nindex 6c3849658a..f6767a73d2 100644\n--- a/.github/workflows/l10n.yml\n+++ b/.github/workflows/l10n.yml\n@@ -1,6 +1,12 @@\n name: git-l10n\n\n-on: [push, pull_request_target]\n+on:\n+  push:\n+    branches_ignore:\n+      - main\n+  pull_request_target:\n+    branches_ignore:\n+      - main\n\n # Avoid unnecessary builds. Unlike the main CI jobs, these are not\n # ci-configurable (but could be).\ndiff --git a/.github/workflows/main.yml b/.github/workflows/main.yml\nindex 079645b776..eaaf6a9151 100644\n--- a/.github/workflows/main.yml\n+++ b/.github/workflows/main.yml\n@@ -1,6 +1,12 @@\n name: CI\n\n-on: [push, pull_request]\n+on:\n+  push:\n+    branches-ignore:\n+      - main\n+  pull_request:\n+    branches-ignore:\n+      - main\n\n env:\n   DEVELOPER: 1\n--- 8< ---\n\n> [1] https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#example-excluding-branches\n\nThanks,\nTaylor\n"},{"id":"480702","messageId":"20230816022420.GA2248431@coredump.intra.peff.net","threadId":"59990","inReplyTo":"ZNv5PQlkn6tbUcH7@nand.local","subject":"Re: main != master at github.com/git/git","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2023-08-16T02:24:20Z","receivedAt":"2023-08-16T02:24:56Z","isPatch":false,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Tue, Aug 15, 2023 at 06:16:29PM -0400, Taylor Blau wrote:\n\n> > An alternative might be to exclude one of the branches in the workflow file,\n> > as per [1].\n> \n> I think that this should be relatively straightforward to do, and would\n> be preferable to dropping 'main'.\n\nThat was my inclination, too, though I wonder if that might cause\nhassles for Git for Windows:\n\n  $ git ls-remote --symref https://github.com/git-for-windows/git HEAD\n  ref: refs/heads/main\tHEAD\n  a67b85bf88ddbccae96714edb64d741ddfc3a1c9\tHEAD\n\nI'm not sure how big a deal it would be in practice. Obviously they\ncarry patches that are not in upstream git and could adjust the file\nthemselves that way. But it might introduce extra friction, and in my\nexperience changes to \"meta\" files like this can be a hassle, because\nyou often want them independently on every branch (though in theory this\none only matters for the \"main\" branch itself).\n\nSo I won't say it's obviously a bad idea, but it might bear some\nthinking on what the ramifications would be for downstream.\n\n-Peff\n"},{"id":"480705","messageId":"xmqq8rabhaxs.fsf@gitster.g","threadId":"59990","inReplyTo":"20230810211619.19055-1-andy.koppe@gmail.com","subject":"Re: [PATCH v3 1/7] pretty-formats: define \"literal formatting code\"","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2023-08-16T04:23:59Z","receivedAt":"2023-08-16T04:28:28Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"The v3 iteration of the series that begins at\n\n  https://lore.kernel.org/git/20230810211619.19055-1-andy.koppe@gmail.com/\n\n08-10 ` [PATCH v3 1/7] pretty-formats: define \"literal formatting code\"\n08-10   ` [PATCH v3 2/7] pretty-formats: enclose options in angle brackets\n08-10   ` [PATCH v3 3/7] decorate: refactor format_decorations()\n08-10   ` [PATCH v3 4/7] decorate: avoid some unnecessary color overhead\n08-10   ` [PATCH v3 5/7] decorate: color each token separately\n08-10   ` [PATCH v3 6/7] pretty: add %(decorate[:<options>]) format\n08-10   ` [PATCH v3 7/7] pretty: add pointer and tag options to %(decorate)\n\nunfortunately did not see any discussion.  Any comments?\n"},{"id":"480709","messageId":"000901d9d045$e780f790$b682e6b0$@nexbridge.com","threadId":"59990","inReplyTo":"20230816022420.GA2248431@coredump.intra.peff.net","subject":"RE: main != master at github.com/git/git","fromName":"","fromEmail":"rsbecker@nexbridge.com","sentAt":"2023-08-16T13:30:57Z","receivedAt":"2023-08-16T13:31:55Z","isPatch":false,"sender":{"key":"randall.becker@nexbridge.ca","avatar":"https://avatars.githubusercontent.com/u/28956764?v=4"},"body":"On Tuesday, August 15, 2023 10:24 PM, Jeff King wrote:\n>On Tue, Aug 15, 2023 at 06:16:29PM -0400, Taylor Blau wrote:\n>\n>> > An alternative might be to exclude one of the branches in the\n>> > workflow file, as per [1].\n>>\n>> I think that this should be relatively straightforward to do, and\n>> would be preferable to dropping 'main'.\n>\n>That was my inclination, too, though I wonder if that might cause hassles for Git for\n>Windows:\n>\n>  $ git ls-remote --symref https://github.com/git-for-windows/git HEAD\n>  ref: refs/heads/main\tHEAD\n>  a67b85bf88ddbccae96714edb64d741ddfc3a1c9\tHEAD\n>\n>I'm not sure how big a deal it would be in practice. Obviously they carry patches that\n>are not in upstream git and could adjust the file themselves that way. But it might\n>introduce extra friction, and in my experience changes to \"meta\" files like this can be\n>a hassle, because you often want them independently on every branch (though in\n>theory this one only matters for the \"main\" branch itself).\n>\n>So I won't say it's obviously a bad idea, but it might bear some thinking on what the\n>ramifications would be for downstream.\n\nWould it not be more convenient just to add a GitHub action that set main = master for each push?\n\n"},{"id":"480779","messageId":"xmqqo7j5uqza.fsf@gitster.g","threadId":"59990","inReplyTo":"000901d9d045$e780f790$b682e6b0$@nexbridge.com","subject":"Re: main != master at github.com/git/git","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2023-08-18T00:35:53Z","receivedAt":"2023-08-18T00:36:53Z","isPatch":false,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"<rsbecker@nexbridge.com> writes:\n\n> Would it not be more convenient just to add a GitHub action that\n> set main = master for each push?\n\nIf \"my private working area calls the primary integration branch\n'master', but for publishing repositories, I have to push it twice,\nonce to 'master' and then to 'main'\" were the problem, the solution\nI would rather want to see implemented is to an ability for the\nrepository owners to set a symref that makes 'main' refer to\n'master', so that I do not have to worry about the aliasing.  But it\nis not a problem (the push refspec can be set up to send the same\ncommit to two different branches just fine).\n\nIn any case, I am not sure if it would solve the problem being\ndiscussed: when CI runner sees branches updated to commit that\nhasn't been worked on, a new job is created to work on that commit,\nand updating two branches with the same commit at the same time\nunfortunately means two independent CI jobs work on the same commit\nin parallel.  The 'lagging behind by 24 hours' hack I mentioned\nearlier was one way to work it around, but it would confuse folks.\n\nI'd really prefer not to special case 'main' (or 'master' for that\nmatter), primarily because some downstreams rely more heavily on\n'main' as Peff pointed out, but also because the problem is not\n'master' vs 'main'.  If 'next' happens to become empty soon after a\nnew cycle starts and points at the same commit as 'master', we will\nsee the same waste of cycles between 'master' and 'next'.\n\nThanks.\n"},{"id":"480813","messageId":"20230820085336.8615-1-andy.koppe@gmail.com","threadId":"59990","inReplyTo":"20230810211619.19055-1-andy.koppe@gmail.com","subject":"[PATCH v4 0/8] pretty: add %(decorate[:<options>]) format","fromName":"Andy Koppe","fromEmail":"andy.koppe@gmail.com","sentAt":"2023-08-20T08:53:28Z","receivedAt":"2023-08-20T08:57:04Z","isPatch":true,"sender":{"key":"andy.koppe@gmail.com","avatar":"https://avatars.githubusercontent.com/u/223411?v=4"},"body":"Compared to v3, this avoids introducing a compound literal, and splits\npart of patch 5 into an additional patch 8.\n\nAndy Koppe (7):\n  pretty-formats: enclose options in angle brackets\n  decorate: refactor format_decorations()\n  decorate: avoid some unnecessary color overhead\n  decorate: color each token separately\n  pretty: add %(decorate[:<options>]) format\n  pretty: add pointer and tag options to %(decorate)\n  decorate: use commit color for HEAD arrow\n\nJunio C Hamano (1):\n  pretty-formats: define \"literal formatting code\"\n\n Documentation/pretty-formats.txt | 47 +++++++++++++-------\n log-tree.c                       | 72 +++++++++++++++++++++---------\n log-tree.h                       | 17 ++++---\n pretty.c                         | 76 +++++++++++++++++++++++++++++---\n t/t4205-log-pretty-formats.sh    | 32 ++++++++++++++\n t/t4207-log-decoration-colors.sh | 44 ++++++++++--------\n 6 files changed, 219 insertions(+), 69 deletions(-)\n\n-- \n2.42.0-rc2\n\n"},{"id":"480814","messageId":"20230820085336.8615-2-andy.koppe@gmail.com","threadId":"59990","inReplyTo":"20230820085336.8615-1-andy.koppe@gmail.com","subject":"[PATCH v4 1/8] pretty-formats: define \"literal formatting code\"","fromName":"Andy Koppe","fromEmail":"andy.koppe@gmail.com","sentAt":"2023-08-20T08:53:29Z","receivedAt":"2023-08-20T08:59:57Z","isPatch":true,"sender":{"key":"andy.koppe@gmail.com","avatar":"https://avatars.githubusercontent.com/u/223411?v=4"},"body":"From: Junio C Hamano <gitster@pobox.com>\n\nThe description for a %(trailer) option already uses this term without\nhaving a definition anywhere in the document, and we are about to add\nanother one in %(decorate) that uses it.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\nSigned-off-by: Andy Koppe <andy.koppe@gmail.com>\n---\n Documentation/pretty-formats.txt | 4 +++-\n 1 file changed, 3 insertions(+), 1 deletion(-)\n\ndiff --git a/Documentation/pretty-formats.txt b/Documentation/pretty-formats.txt\nindex 3b71334459..5e1432951b 100644\n--- a/Documentation/pretty-formats.txt\n+++ b/Documentation/pretty-formats.txt\n@@ -122,7 +122,9 @@ The placeholders are:\n - Placeholders that expand to a single literal character:\n '%n':: newline\n '%%':: a raw '%'\n-'%x00':: print a byte from a hex code\n+'%x00':: '%x' followed by two hexadecimal digits is replaced with a\n+\t byte with the hexadecimal digits' value (we will call this\n+\t \"literal formatting code\" in the rest of this document).\n \n - Placeholders that affect formatting of later placeholders:\n '%Cred':: switch color to red\n-- \n2.42.0-rc2\n\n"},{"id":"480815","messageId":"20230820085336.8615-3-andy.koppe@gmail.com","threadId":"59990","inReplyTo":"20230820085336.8615-1-andy.koppe@gmail.com","subject":"[PATCH v4 2/8] pretty-formats: enclose options in angle brackets","fromName":"Andy Koppe","fromEmail":"andy.koppe@gmail.com","sentAt":"2023-08-20T08:53:30Z","receivedAt":"2023-08-20T09:00:02Z","isPatch":true,"sender":{"key":"andy.koppe@gmail.com","avatar":"https://avatars.githubusercontent.com/u/223411?v=4"},"body":"Enclose the 'options' placeholders in the %(describe) and %(trailers)\nformat specifiers in angle brackets to clarify that they are\nplaceholders rather than keywords.\n\nAlso remove the indentation from their descriptions, instead of\nincreasing it to account for the extra two angle bracket in the\nheadings. The indentation isn't required by asciidoc, it doesn't reflect\nhow the output text is formatted, and it's inconsistent with the\nfollowing bullet points that are at the same level in the output.\n\nSigned-off-by: Andy Koppe <andy.koppe@gmail.com>\n---\n Documentation/pretty-formats.txt | 24 ++++++++++--------------\n 1 file changed, 10 insertions(+), 14 deletions(-)\n\ndiff --git a/Documentation/pretty-formats.txt b/Documentation/pretty-formats.txt\nindex 5e1432951b..851a9878e6 100644\n--- a/Documentation/pretty-formats.txt\n+++ b/Documentation/pretty-formats.txt\n@@ -224,13 +224,11 @@ The placeholders are:\n \tlinkgit:git-rev-list[1])\n '%d':: ref names, like the --decorate option of linkgit:git-log[1]\n '%D':: ref names without the \" (\", \")\" wrapping.\n-'%(describe[:options])':: human-readable name, like\n-\t\t\t  linkgit:git-describe[1]; empty string for\n-\t\t\t  undescribable commits.  The `describe` string\n-\t\t\t  may be followed by a colon and zero or more\n-\t\t\t  comma-separated options.  Descriptions can be\n-\t\t\t  inconsistent when tags are added or removed at\n-\t\t\t  the same time.\n+'%(describe[:<options>])'::\n+human-readable name, like linkgit:git-describe[1]; empty string for\n+undescribable commits.  The `describe` string may be followed by a colon and\n+zero or more comma-separated options.  Descriptions can be inconsistent when\n+tags are added or removed at the same time.\n +\n ** 'tags[=<bool-value>]': Instead of only considering annotated tags,\n    consider lightweight tags as well.\n@@ -283,13 +281,11 @@ endif::git-rev-list[]\n '%gE':: reflog identity email (respecting .mailmap, see\n \tlinkgit:git-shortlog[1] or linkgit:git-blame[1])\n '%gs':: reflog subject\n-'%(trailers[:options])':: display the trailers of the body as\n-\t\t\t  interpreted by\n-\t\t\t  linkgit:git-interpret-trailers[1]. The\n-\t\t\t  `trailers` string may be followed by a colon\n-\t\t\t  and zero or more comma-separated options.\n-\t\t\t  If any option is provided multiple times the\n-\t\t\t  last occurrence wins.\n+'%(trailers[:<options>])'::\n+display the trailers of the body as interpreted by\n+linkgit:git-interpret-trailers[1]. The `trailers` string may be followed by\n+a colon and zero or more comma-separated options. If any option is provided\n+multiple times, the last occurrence wins.\n +\n ** 'key=<key>': only show trailers with specified <key>. Matching is done\n    case-insensitively and trailing colon is optional. If option is\n-- \n2.42.0-rc2\n\n"},{"id":"480816","messageId":"20230820085336.8615-6-andy.koppe@gmail.com","threadId":"59990","inReplyTo":"20230820085336.8615-1-andy.koppe@gmail.com","subject":"[PATCH v4 5/8] decorate: color each token separately","fromName":"Andy Koppe","fromEmail":"andy.koppe@gmail.com","sentAt":"2023-08-20T08:53:33Z","receivedAt":"2023-08-20T09:00:02Z","isPatch":true,"sender":{"key":"andy.koppe@gmail.com","avatar":"https://avatars.githubusercontent.com/u/223411?v=4"},"body":"Wrap \"tag:\" prefixes and the arrows in \"HEAD -> branch\" decorations in\ntheir own color sequences. Otherwise, if --graph is used, tag names or\narrows can end up uncolored when %w width formatting breaks a line just\nbefore them. This is because --graph resets the color after doing its\ndrawing at the start of a line.\n\nAmend test t4207-log-decoration-colors.sh accordingly.\n\nSigned-off-by: Andy Koppe <andy.koppe@gmail.com>\n---\n log-tree.c                       | 14 +++++++---\n t/t4207-log-decoration-colors.sh | 44 ++++++++++++++++++--------------\n 2 files changed, 36 insertions(+), 22 deletions(-)\n\ndiff --git a/log-tree.c b/log-tree.c\nindex 7c6d3f1ac3..44f4693567 100644\n--- a/log-tree.c\n+++ b/log-tree.c\n@@ -342,26 +342,34 @@ void format_decorations(struct strbuf *sb,\n \t\t * appeared, skipping the entry for current.\n \t\t */\n \t\tif (decoration != current_and_HEAD) {\n+\t\t\tconst char *color =\n+\t\t\t\tdecorate_get_color(use_color, decoration->type);\n+\n \t\t\tif (*prefix) {\n \t\t\t\tstrbuf_addstr(sb, color_commit);\n \t\t\t\tstrbuf_addstr(sb, prefix);\n \t\t\t\tstrbuf_addstr(sb, color_reset);\n \t\t\t}\n \n-\t\t\tstrbuf_addstr(sb, decorate_get_color(use_color, decoration->type));\n-\t\t\tif (decoration->type == DECORATION_REF_TAG)\n+\t\t\tif (decoration->type == DECORATION_REF_TAG) {\n+\t\t\t\tstrbuf_addstr(sb, color);\n \t\t\t\tstrbuf_addstr(sb, \"tag: \");\n+\t\t\t\tstrbuf_addstr(sb, color_reset);\n+\t\t\t}\n \n+\t\t\tstrbuf_addstr(sb, color);\n \t\t\tshow_name(sb, decoration);\n+\t\t\tstrbuf_addstr(sb, color_reset);\n \n \t\t\tif (current_and_HEAD &&\n \t\t\t    decoration->type == DECORATION_REF_HEAD) {\n+\t\t\t\tstrbuf_addstr(sb, color);\n \t\t\t\tstrbuf_addstr(sb, \" -> \");\n \t\t\t\tstrbuf_addstr(sb, color_reset);\n \t\t\t\tstrbuf_addstr(sb, decorate_get_color(use_color, current_and_HEAD->type));\n \t\t\t\tshow_name(sb, current_and_HEAD);\n+\t\t\t\tstrbuf_addstr(sb, color_reset);\n \t\t\t}\n-\t\t\tstrbuf_addstr(sb, color_reset);\n \n \t\t\tprefix = separator;\n \t\t}\ndiff --git a/t/t4207-log-decoration-colors.sh b/t/t4207-log-decoration-colors.sh\nindex ded33a82e2..df804f38e2 100755\n--- a/t/t4207-log-decoration-colors.sh\n+++ b/t/t4207-log-decoration-colors.sh\n@@ -53,15 +53,17 @@ cmp_filtered_decorations () {\n # to this test since it does not contain any decoration, hence --first-parent\n test_expect_success 'commit decorations colored correctly' '\n \tcat >expect <<-EOF &&\n-\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_HEAD}HEAD -> \\\n-${c_reset}${c_branch}main${c_reset}${c_commit}, \\\n-${c_reset}${c_tag}tag: v1.0${c_reset}${c_commit}, \\\n-${c_reset}${c_tag}tag: B${c_reset}${c_commit})${c_reset} B\n-${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_tag}tag: A1${c_reset}${c_commit}, \\\n+\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_HEAD}HEAD${c_reset}\\\n+${c_HEAD} -> ${c_reset}${c_branch}main${c_reset}${c_commit}, \\\n+${c_reset}${c_tag}tag: ${c_reset}${c_tag}v1.0${c_reset}${c_commit}, \\\n+${c_reset}${c_tag}tag: ${c_reset}${c_tag}B${c_reset}${c_commit})${c_reset} B\n+${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\\\n+${c_tag}tag: ${c_reset}${c_tag}A1${c_reset}${c_commit}, \\\n ${c_reset}${c_remoteBranch}other/main${c_reset}${c_commit})${c_reset} A1\n-\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_stash}refs/stash${c_reset}${c_commit})${c_reset} \\\n-On main: Changes to A.t\n-\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_tag}tag: A${c_reset}${c_commit})${c_reset} A\n+\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\\\n+${c_stash}refs/stash${c_reset}${c_commit})${c_reset} On main: Changes to A.t\n+\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\\\n+${c_tag}tag: ${c_reset}${c_tag}A${c_reset}${c_commit})${c_reset} A\n \tEOF\n \n \tgit log --first-parent --no-abbrev --decorate --oneline --color=always --all >actual &&\n@@ -76,12 +78,14 @@ test_expect_success 'test coloring with replace-objects' '\n \tgit replace HEAD~1 HEAD~2 &&\n \n \tcat >expect <<-EOF &&\n-\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_HEAD}HEAD -> \\\n-${c_reset}${c_branch}main${c_reset}${c_commit}, \\\n-${c_reset}${c_tag}tag: D${c_reset}${c_commit})${c_reset} D\n-\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_tag}tag: C${c_reset}${c_commit}, \\\n+\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_HEAD}HEAD${c_reset}\\\n+${c_HEAD} -> ${c_reset}${c_branch}main${c_reset}${c_commit}, \\\n+${c_reset}${c_tag}tag: ${c_reset}${c_tag}D${c_reset}${c_commit})${c_reset} D\n+\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\\\n+${c_tag}tag: ${c_reset}${c_tag}C${c_reset}${c_commit}, \\\n ${c_reset}${c_grafted}replaced${c_reset}${c_commit})${c_reset} B\n-\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_tag}tag: A${c_reset}${c_commit})${c_reset} A\n+\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\\\n+${c_tag}tag: ${c_reset}${c_tag}A${c_reset}${c_commit})${c_reset} A\n EOF\n \n \tgit log --first-parent --no-abbrev --decorate --oneline --color=always HEAD >actual &&\n@@ -100,13 +104,15 @@ test_expect_success 'test coloring with grafted commit' '\n \tgit replace --graft HEAD HEAD~2 &&\n \n \tcat >expect <<-EOF &&\n-\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_HEAD}HEAD -> \\\n-${c_reset}${c_branch}main${c_reset}${c_commit}, \\\n-${c_reset}${c_tag}tag: D${c_reset}${c_commit}, \\\n+\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_HEAD}HEAD${c_reset}\\\n+${c_HEAD} -> ${c_reset}${c_branch}main${c_reset}${c_commit}, \\\n+${c_reset}${c_tag}tag: ${c_reset}${c_tag}D${c_reset}${c_commit}, \\\n ${c_reset}${c_grafted}replaced${c_reset}${c_commit})${c_reset} D\n-\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_tag}tag: v1.0${c_reset}${c_commit}, \\\n-${c_reset}${c_tag}tag: B${c_reset}${c_commit})${c_reset} B\n-\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_tag}tag: A${c_reset}${c_commit})${c_reset} A\n+\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\\\n+${c_tag}tag: ${c_reset}${c_tag}v1.0${c_reset}${c_commit}, \\\n+${c_reset}${c_tag}tag: ${c_reset}${c_tag}B${c_reset}${c_commit})${c_reset} B\n+\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\\\n+${c_tag}tag: ${c_reset}${c_tag}A${c_reset}${c_commit})${c_reset} A\n \tEOF\n \n \tgit log --first-parent --no-abbrev --decorate --oneline --color=always HEAD >actual &&\n-- \n2.42.0-rc2\n\n"},{"id":"480817","messageId":"20230820085336.8615-5-andy.koppe@gmail.com","threadId":"59990","inReplyTo":"20230820085336.8615-1-andy.koppe@gmail.com","subject":"[PATCH v4 4/8] decorate: avoid some unnecessary color overhead","fromName":"Andy Koppe","fromEmail":"andy.koppe@gmail.com","sentAt":"2023-08-20T08:53:32Z","receivedAt":"2023-08-20T09:00:02Z","isPatch":true,"sender":{"key":"andy.koppe@gmail.com","avatar":"https://avatars.githubusercontent.com/u/223411?v=4"},"body":"In format_decorations(), don't obtain color sequences if there are no\ndecorations, and don't emit color sequences around empty strings.\n\nSigned-off-by: Andy Koppe <andy.koppe@gmail.com>\n---\n log-tree.c | 25 +++++++++++++++----------\n 1 file changed, 15 insertions(+), 10 deletions(-)\n\ndiff --git a/log-tree.c b/log-tree.c\nindex cd12c26c29..7c6d3f1ac3 100644\n--- a/log-tree.c\n+++ b/log-tree.c\n@@ -312,10 +312,7 @@ void format_decorations(struct strbuf *sb,\n {\n \tconst struct name_decoration *decoration;\n \tconst struct name_decoration *current_and_HEAD;\n-\tconst char *color_commit =\n-\t\tdiff_get_color(use_color, DIFF_COMMIT);\n-\tconst char *color_reset =\n-\t\tdecorate_get_color(use_color, DECORATION_NONE);\n+\tconst char *color_commit, *color_reset;\n \n \tconst char *prefix = \" (\";\n \tconst char *suffix = \")\";\n@@ -334,6 +331,9 @@ void format_decorations(struct strbuf *sb,\n \t\t\tseparator = opts->separator;\n \t}\n \n+\tcolor_commit = diff_get_color(use_color, DIFF_COMMIT);\n+\tcolor_reset = decorate_get_color(use_color, DECORATION_NONE);\n+\n \tcurrent_and_HEAD = current_pointed_by_HEAD(decoration);\n \twhile (decoration) {\n \t\t/*\n@@ -342,9 +342,12 @@ void format_decorations(struct strbuf *sb,\n \t\t * appeared, skipping the entry for current.\n \t\t */\n \t\tif (decoration != current_and_HEAD) {\n-\t\t\tstrbuf_addstr(sb, color_commit);\n-\t\t\tstrbuf_addstr(sb, prefix);\n-\t\t\tstrbuf_addstr(sb, color_reset);\n+\t\t\tif (*prefix) {\n+\t\t\t\tstrbuf_addstr(sb, color_commit);\n+\t\t\t\tstrbuf_addstr(sb, prefix);\n+\t\t\t\tstrbuf_addstr(sb, color_reset);\n+\t\t\t}\n+\n \t\t\tstrbuf_addstr(sb, decorate_get_color(use_color, decoration->type));\n \t\t\tif (decoration->type == DECORATION_REF_TAG)\n \t\t\t\tstrbuf_addstr(sb, \"tag: \");\n@@ -364,9 +367,11 @@ void format_decorations(struct strbuf *sb,\n \t\t}\n \t\tdecoration = decoration->next;\n \t}\n-\tstrbuf_addstr(sb, color_commit);\n-\tstrbuf_addstr(sb, suffix);\n-\tstrbuf_addstr(sb, color_reset);\n+\tif (*suffix) {\n+\t\tstrbuf_addstr(sb, color_commit);\n+\t\tstrbuf_addstr(sb, suffix);\n+\t\tstrbuf_addstr(sb, color_reset);\n+\t}\n }\n \n void show_decorations(struct rev_info *opt, struct commit *commit)\n-- \n2.42.0-rc2\n\n"},{"id":"480818","messageId":"20230820085336.8615-4-andy.koppe@gmail.com","threadId":"59990","inReplyTo":"20230820085336.8615-1-andy.koppe@gmail.com","subject":"[PATCH v4 3/8] decorate: refactor format_decorations()","fromName":"Andy Koppe","fromEmail":"andy.koppe@gmail.com","sentAt":"2023-08-20T08:53:31Z","receivedAt":"2023-08-20T09:00:06Z","isPatch":true,"sender":{"key":"andy.koppe@gmail.com","avatar":"https://avatars.githubusercontent.com/u/223411?v=4"},"body":"Rename the format_decorations_extended function to format_decorations\nand drop the format_decorations wrapper macro. Pass the prefix, suffix\nand separator strings as a single 'struct format_decorations' pointer\nargument instead of separate arguments. Use default values defined in\nthe function when either the struct pointer or any of the struct fields\nare NULL. This is to ease extension with additional options.\n\nSigned-off-by: Andy Koppe <andy.koppe@gmail.com>\n---\n log-tree.c | 23 +++++++++++++++++------\n log-tree.h | 15 ++++++++-------\n pretty.c   |  6 ++++--\n 3 files changed, 29 insertions(+), 15 deletions(-)\n\ndiff --git a/log-tree.c b/log-tree.c\nindex 208c69cbb7..cd12c26c29 100644\n--- a/log-tree.c\n+++ b/log-tree.c\n@@ -303,14 +303,12 @@ static void show_name(struct strbuf *sb, const struct name_decoration *decoratio\n \n /*\n  * The caller makes sure there is no funny color before calling.\n- * format_decorations_extended makes sure the same after return.\n+ * format_decorations ensures the same after return.\n  */\n-void format_decorations_extended(struct strbuf *sb,\n+void format_decorations(struct strbuf *sb,\n \t\t\tconst struct commit *commit,\n \t\t\tint use_color,\n-\t\t\tconst char *prefix,\n-\t\t\tconst char *separator,\n-\t\t\tconst char *suffix)\n+\t\t\tconst struct decoration_options *opts)\n {\n \tconst struct name_decoration *decoration;\n \tconst struct name_decoration *current_and_HEAD;\n@@ -319,10 +317,23 @@ void format_decorations_extended(struct strbuf *sb,\n \tconst char *color_reset =\n \t\tdecorate_get_color(use_color, DECORATION_NONE);\n \n+\tconst char *prefix = \" (\";\n+\tconst char *suffix = \")\";\n+\tconst char *separator = \", \";\n+\n \tdecoration = get_name_decoration(&commit->object);\n \tif (!decoration)\n \t\treturn;\n \n+\tif (opts) {\n+\t\tif (opts->prefix)\n+\t\t\tprefix = opts->prefix;\n+\t\tif (opts->suffix)\n+\t\t\tsuffix = opts->suffix;\n+\t\tif (opts->separator)\n+\t\t\tseparator = opts->separator;\n+\t}\n+\n \tcurrent_and_HEAD = current_pointed_by_HEAD(decoration);\n \twhile (decoration) {\n \t\t/*\n@@ -370,7 +381,7 @@ void show_decorations(struct rev_info *opt, struct commit *commit)\n \t}\n \tif (!opt->show_decorations)\n \t\treturn;\n-\tformat_decorations(&sb, commit, opt->diffopt.use_color);\n+\tformat_decorations(&sb, commit, opt->diffopt.use_color, NULL);\n \tfputs(sb.buf, opt->diffopt.file);\n \tstrbuf_release(&sb);\n }\ndiff --git a/log-tree.h b/log-tree.h\nindex bdb6432815..14898de8ac 100644\n--- a/log-tree.h\n+++ b/log-tree.h\n@@ -13,17 +13,18 @@ struct decoration_filter {\n \tstruct string_list *exclude_ref_config_pattern;\n };\n \n+struct decoration_options {\n+\tchar *prefix;\n+\tchar *suffix;\n+\tchar *separator;\n+};\n+\n int parse_decorate_color_config(const char *var, const char *slot_name, const char *value);\n int log_tree_diff_flush(struct rev_info *);\n int log_tree_commit(struct rev_info *, struct commit *);\n void show_log(struct rev_info *opt);\n-void format_decorations_extended(struct strbuf *sb, const struct commit *commit,\n-\t\t\t     int use_color,\n-\t\t\t     const char *prefix,\n-\t\t\t     const char *separator,\n-\t\t\t     const char *suffix);\n-#define format_decorations(strbuf, commit, color) \\\n-\t\t\t     format_decorations_extended((strbuf), (commit), (color), \" (\", \", \", \")\")\n+void format_decorations(struct strbuf *sb, const struct commit *commit,\n+\t\t\tint use_color, const struct decoration_options *opts);\n void show_decorations(struct rev_info *opt, struct commit *commit);\n void log_write_email_headers(struct rev_info *opt, struct commit *commit,\n \t\t\t     const char **extra_headers_p,\ndiff --git a/pretty.c b/pretty.c\nindex 718530bbab..24fb82a5a2 100644\n--- a/pretty.c\n+++ b/pretty.c\n@@ -1537,10 +1537,12 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */\n \t\tstrbuf_addstr(sb, get_revision_mark(NULL, commit));\n \t\treturn 1;\n \tcase 'd':\n-\t\tformat_decorations(sb, commit, c->auto_color);\n+\t\tformat_decorations(sb, commit, c->auto_color, NULL);\n \t\treturn 1;\n \tcase 'D':\n-\t\tformat_decorations_extended(sb, commit, c->auto_color, \"\", \", \", \"\");\n+\t\tformat_decorations(sb, commit, c->auto_color,\n+\t\t\t&(struct decoration_options){.prefix = \"\",\n+\t\t\t\t\t\t     .suffix = \"\"});\n \t\treturn 1;\n \tcase 'S':\t\t/* tag/branch like --source */\n \t\tif (!(c->pretty_ctx->rev && c->pretty_ctx->rev->sources))\n-- \n2.42.0-rc2\n\n"},{"id":"480819","messageId":"20230820085336.8615-7-andy.koppe@gmail.com","threadId":"59990","inReplyTo":"20230820085336.8615-1-andy.koppe@gmail.com","subject":"[PATCH v4 6/8] pretty: add %(decorate[:<options>]) format","fromName":"Andy Koppe","fromEmail":"andy.koppe@gmail.com","sentAt":"2023-08-20T08:53:34Z","receivedAt":"2023-08-20T09:00:06Z","isPatch":true,"sender":{"key":"andy.koppe@gmail.com","avatar":"https://avatars.githubusercontent.com/u/223411?v=4"},"body":"Add %(decorate[:<options>]) format that lists ref names similarly to the\n%d format, but which allows the otherwise fixed prefix, suffix and\nseparator strings to be customized. Omitted options default to the\nstrings used in %d.\n\nRename expand_separator() function used to expand %x literal formatting\ncodes to expand_string_arg(), as it is now used on strings other than\nseparators.\n\nExamples:\n- %(decorate) is equivalent to %d.\n- %(decorate:prefix=,suffix=) is equivalent to %D.\n- %(decorate:prefix=[,suffix=],separator=%x3B) produces a list enclosed\nin square brackets and separated by semicolons.\n\nTest the format in t4205-log-pretty-formats.sh and document it in\npretty-formats.txt.\n\nSigned-off-by: Andy Koppe <andy.koppe@gmail.com>\n---\n Documentation/pretty-formats.txt | 10 +++++\n pretty.c                         | 72 ++++++++++++++++++++++++++++----\n t/t4205-log-pretty-formats.sh    | 27 ++++++++++++\n 3 files changed, 101 insertions(+), 8 deletions(-)\n\ndiff --git a/Documentation/pretty-formats.txt b/Documentation/pretty-formats.txt\nindex 851a9878e6..709d85af21 100644\n--- a/Documentation/pretty-formats.txt\n+++ b/Documentation/pretty-formats.txt\n@@ -224,6 +224,16 @@ The placeholders are:\n \tlinkgit:git-rev-list[1])\n '%d':: ref names, like the --decorate option of linkgit:git-log[1]\n '%D':: ref names without the \" (\", \")\" wrapping.\n+'%(decorate[:<options>])'::\n+ref names with custom decorations. The `decorate` string may be followed by a\n+colon and zero or more comma-separated options. Option values may contain\n+literal formatting codes. These must be used for commas (`%x2C`) and closing\n+parentheses (`%x29`), due to their role in the option syntax.\n++\n+** 'prefix=<value>': Shown before the list of ref names.  Defaults to \"{nbsp}`(`\".\n+** 'suffix=<value>': Shown after the list of ref names.  Defaults to \"`)`\".\n+** 'separator=<value>': Shown between ref names.  Defaults to \"`,`{nbsp}\".\n+\n '%(describe[:<options>])'::\n human-readable name, like linkgit:git-describe[1]; empty string for\n undescribable commits.  The `describe` string may be followed by a colon and\ndiff --git a/pretty.c b/pretty.c\nindex 24fb82a5a2..1639efe2f8 100644\n--- a/pretty.c\n+++ b/pretty.c\n@@ -1252,8 +1252,8 @@ static int format_trailer_match_cb(const struct strbuf *key, void *ud)\n \treturn 0;\n }\n \n-static struct strbuf *expand_separator(struct strbuf *sb,\n-\t\t\t\t       const char *argval, size_t arglen)\n+static struct strbuf *expand_string_arg(struct strbuf *sb,\n+\t\t\t\t\tconst char *argval, size_t arglen)\n {\n \tchar *fmt = xstrndup(argval, arglen);\n \tconst char *format = fmt;\n@@ -1301,9 +1301,9 @@ int format_set_trailers_options(struct process_trailer_options *opts,\n \t\t\topts->filter_data = filter_list;\n \t\t\topts->only_trailers = 1;\n \t\t} else if (match_placeholder_arg_value(*arg, \"separator\", arg, &argval, &arglen)) {\n-\t\t\topts->separator = expand_separator(sepbuf, argval, arglen);\n+\t\t\topts->separator = expand_string_arg(sepbuf, argval, arglen);\n \t\t} else if (match_placeholder_arg_value(*arg, \"key_value_separator\", arg, &argval, &arglen)) {\n-\t\t\topts->key_value_separator = expand_separator(kvsepbuf, argval, arglen);\n+\t\t\topts->key_value_separator = expand_string_arg(kvsepbuf, argval, arglen);\n \t\t} else if (!match_placeholder_bool_arg(*arg, \"only\", arg, &opts->only_trailers) &&\n \t\t\t   !match_placeholder_bool_arg(*arg, \"unfold\", arg, &opts->unfold) &&\n \t\t\t   !match_placeholder_bool_arg(*arg, \"keyonly\", arg, &opts->key_only) &&\n@@ -1384,6 +1384,40 @@ static size_t parse_describe_args(const char *start, struct strvec *args)\n \treturn arg - start;\n }\n \n+\n+static int parse_decoration_option(const char **arg,\n+\t\t\t\t   const char *name,\n+\t\t\t\t   char **opt)\n+{\n+\tconst char *argval;\n+\tsize_t arglen;\n+\n+\tif (match_placeholder_arg_value(*arg, name, arg, &argval, &arglen)) {\n+\t\tstruct strbuf sb = STRBUF_INIT;\n+\n+\t\texpand_string_arg(&sb, argval, arglen);\n+\t\t*opt = strbuf_detach(&sb, NULL);\n+\t\treturn 1;\n+\t}\n+\treturn 0;\n+}\n+\n+static void parse_decoration_options(const char **arg,\n+\t\t\t\t     struct decoration_options *opts)\n+{\n+\twhile (parse_decoration_option(arg, \"prefix\", &opts->prefix) ||\n+\t       parse_decoration_option(arg, \"suffix\", &opts->suffix) ||\n+\t       parse_decoration_option(arg, \"separator\", &opts->separator))\n+\t\t;\n+}\n+\n+static void free_decoration_options(const struct decoration_options *opts)\n+{\n+\tfree(opts->prefix);\n+\tfree(opts->suffix);\n+\tfree(opts->separator);\n+}\n+\n static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */\n \t\t\t\tconst char *placeholder,\n \t\t\t\tvoid *context)\n@@ -1540,10 +1574,15 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */\n \t\tformat_decorations(sb, commit, c->auto_color, NULL);\n \t\treturn 1;\n \tcase 'D':\n-\t\tformat_decorations(sb, commit, c->auto_color,\n-\t\t\t&(struct decoration_options){.prefix = \"\",\n-\t\t\t\t\t\t     .suffix = \"\"});\n-\t\treturn 1;\n+\t\t{\n+\t\t\tconst struct decoration_options opts = {\n+\t\t\t\t.prefix = \"\",\n+\t\t\t\t.suffix = \"\"\n+\t\t\t};\n+\n+\t\t\tformat_decorations(sb, commit, c->auto_color, &opts);\n+\t\t\treturn 1;\n+\t\t}\n \tcase 'S':\t\t/* tag/branch like --source */\n \t\tif (!(c->pretty_ctx->rev && c->pretty_ctx->rev->sources))\n \t\t\treturn 0;\n@@ -1640,6 +1679,23 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */\n \t\treturn 2;\n \t}\n \n+\tif (skip_prefix(placeholder, \"(decorate\", &arg)) {\n+\t\tstruct decoration_options opts = { NULL };\n+\t\tsize_t ret = 0;\n+\n+\t\tif (*arg == ':') {\n+\t\t\targ++;\n+\t\t\tparse_decoration_options(&arg, &opts);\n+\t\t}\n+\t\tif (*arg == ')') {\n+\t\t\tformat_decorations(sb, commit, c->auto_color, &opts);\n+\t\t\tret = arg - placeholder + 1;\n+\t\t}\n+\n+\t\tfree_decoration_options(&opts);\n+\t\treturn ret;\n+\t}\n+\n \t/* For the rest we have to parse the commit header. */\n \tif (!c->commit_header_parsed) {\n \t\tmsg = c->message =\ndiff --git a/t/t4205-log-pretty-formats.sh b/t/t4205-log-pretty-formats.sh\nindex dd9035aa38..6ba399c5be 100755\n--- a/t/t4205-log-pretty-formats.sh\n+++ b/t/t4205-log-pretty-formats.sh\n@@ -576,6 +576,33 @@ test_expect_success 'clean log decoration' '\n \ttest_cmp expected actual1\n '\n \n+test_expect_success 'pretty format %decorate' '\n+\tgit checkout -b foo &&\n+\tgit commit --allow-empty -m \"new commit\" &&\n+\tgit tag bar &&\n+\tgit branch qux &&\n+\n+\techo \" (HEAD -> foo, tag: bar, qux)\" >expect1 &&\n+\tgit log --format=\"%(decorate)\" -1 >actual1 &&\n+\ttest_cmp expect1 actual1 &&\n+\n+\techo \"HEAD -> foo, tag: bar, qux\" >expect2 &&\n+\tgit log --format=\"%(decorate:prefix=,suffix=)\" -1 >actual2 &&\n+\ttest_cmp expect2 actual2 &&\n+\n+\techo \"[ HEAD -> foo; tag: bar; qux ]\" >expect3 &&\n+\tgit log --format=\"%(decorate:prefix=[ ,suffix= ],separator=%x3B )\" \\\n+\t\t-1 >actual3 &&\n+\ttest_cmp expect3 actual3 &&\n+\n+\t# Try with a typo (in \"separator\"), in which case the placeholder should\n+\t# not be replaced.\n+\techo \"%(decorate:prefix=[ ,suffix= ],separater=; )\" >expect4 &&\n+\tgit log --format=\"%(decorate:prefix=[ ,suffix= ],separater=%x3B )\" \\\n+\t\t-1 >actual4 &&\n+\ttest_cmp expect4 actual4\n+'\n+\n cat >trailers <<EOF\n Signed-off-by: A U Thor <author@example.com>\n Acked-by: A U Thor <author@example.com>\n-- \n2.42.0-rc2\n\n"},{"id":"480820","messageId":"20230820085336.8615-8-andy.koppe@gmail.com","threadId":"59990","inReplyTo":"20230820085336.8615-1-andy.koppe@gmail.com","subject":"[PATCH v4 7/8] pretty: add pointer and tag options to %(decorate)","fromName":"Andy Koppe","fromEmail":"andy.koppe@gmail.com","sentAt":"2023-08-20T08:53:35Z","receivedAt":"2023-08-20T09:00:11Z","isPatch":true,"sender":{"key":"andy.koppe@gmail.com","avatar":"https://avatars.githubusercontent.com/u/223411?v=4"},"body":"Add pointer and tag options to %(decorate) format, to allow to override\nthe \" -> \" string used to show where HEAD points and the \"tag: \" string\nused to mark tags.\n\nDocument in pretty-formats.txt and test in t4205-log-pretty-formats.sh.\n\nSigned-off-by: Andy Koppe <andy.koppe@gmail.com>\n---\n Documentation/pretty-formats.txt |  9 +++++++++\n log-tree.c                       | 12 +++++++++---\n log-tree.h                       |  2 ++\n pretty.c                         |  6 +++++-\n t/t4205-log-pretty-formats.sh    |  7 ++++++-\n 5 files changed, 31 insertions(+), 5 deletions(-)\n\ndiff --git a/Documentation/pretty-formats.txt b/Documentation/pretty-formats.txt\nindex 709d85af21..d38b4ab566 100644\n--- a/Documentation/pretty-formats.txt\n+++ b/Documentation/pretty-formats.txt\n@@ -233,6 +233,15 @@ parentheses (`%x29`), due to their role in the option syntax.\n ** 'prefix=<value>': Shown before the list of ref names.  Defaults to \"{nbsp}`(`\".\n ** 'suffix=<value>': Shown after the list of ref names.  Defaults to \"`)`\".\n ** 'separator=<value>': Shown between ref names.  Defaults to \"`,`{nbsp}\".\n+** 'pointer=<value>': Shown between HEAD and the branch it points to, if any.\n+\t\t      Defaults to \"{nbsp}`->`{nbsp}\".\n+** 'tag=<value>': Shown before tag names. Defaults to \"`tag:`{nbsp}\".\n+\n++\n+For example, to produce decorations with no wrapping\n+or tag annotations, and spaces as separators:\n++\n+`%(decorate:prefix=,suffix=,tag=,separator= )`\n \n '%(describe[:<options>])'::\n human-readable name, like linkgit:git-describe[1]; empty string for\ndiff --git a/log-tree.c b/log-tree.c\nindex 44f4693567..50b4850eda 100644\n--- a/log-tree.c\n+++ b/log-tree.c\n@@ -317,6 +317,8 @@ void format_decorations(struct strbuf *sb,\n \tconst char *prefix = \" (\";\n \tconst char *suffix = \")\";\n \tconst char *separator = \", \";\n+\tconst char *pointer = \" -> \";\n+\tconst char *tag = \"tag: \";\n \n \tdecoration = get_name_decoration(&commit->object);\n \tif (!decoration)\n@@ -329,6 +331,10 @@ void format_decorations(struct strbuf *sb,\n \t\t\tsuffix = opts->suffix;\n \t\tif (opts->separator)\n \t\t\tseparator = opts->separator;\n+\t\tif (opts->pointer)\n+\t\t\tpointer = opts->pointer;\n+\t\tif (opts->tag)\n+\t\t\ttag = opts->tag;\n \t}\n \n \tcolor_commit = diff_get_color(use_color, DIFF_COMMIT);\n@@ -351,9 +357,9 @@ void format_decorations(struct strbuf *sb,\n \t\t\t\tstrbuf_addstr(sb, color_reset);\n \t\t\t}\n \n-\t\t\tif (decoration->type == DECORATION_REF_TAG) {\n+\t\t\tif (*tag && decoration->type == DECORATION_REF_TAG) {\n \t\t\t\tstrbuf_addstr(sb, color);\n-\t\t\t\tstrbuf_addstr(sb, \"tag: \");\n+\t\t\t\tstrbuf_addstr(sb, tag);\n \t\t\t\tstrbuf_addstr(sb, color_reset);\n \t\t\t}\n \n@@ -364,7 +370,7 @@ void format_decorations(struct strbuf *sb,\n \t\t\tif (current_and_HEAD &&\n \t\t\t    decoration->type == DECORATION_REF_HEAD) {\n \t\t\t\tstrbuf_addstr(sb, color);\n-\t\t\t\tstrbuf_addstr(sb, \" -> \");\n+\t\t\t\tstrbuf_addstr(sb, pointer);\n \t\t\t\tstrbuf_addstr(sb, color_reset);\n \t\t\t\tstrbuf_addstr(sb, decorate_get_color(use_color, current_and_HEAD->type));\n \t\t\t\tshow_name(sb, current_and_HEAD);\ndiff --git a/log-tree.h b/log-tree.h\nindex 14898de8ac..41c776fea5 100644\n--- a/log-tree.h\n+++ b/log-tree.h\n@@ -17,6 +17,8 @@ struct decoration_options {\n \tchar *prefix;\n \tchar *suffix;\n \tchar *separator;\n+\tchar *pointer;\n+\tchar *tag;\n };\n \n int parse_decorate_color_config(const char *var, const char *slot_name, const char *value);\ndiff --git a/pretty.c b/pretty.c\nindex 1639efe2f8..7f3abb676c 100644\n--- a/pretty.c\n+++ b/pretty.c\n@@ -1407,7 +1407,9 @@ static void parse_decoration_options(const char **arg,\n {\n \twhile (parse_decoration_option(arg, \"prefix\", &opts->prefix) ||\n \t       parse_decoration_option(arg, \"suffix\", &opts->suffix) ||\n-\t       parse_decoration_option(arg, \"separator\", &opts->separator))\n+\t       parse_decoration_option(arg, \"separator\", &opts->separator) ||\n+\t       parse_decoration_option(arg, \"pointer\", &opts->pointer) ||\n+\t       parse_decoration_option(arg, \"tag\", &opts->tag))\n \t\t;\n }\n \n@@ -1416,6 +1418,8 @@ static void free_decoration_options(const struct decoration_options *opts)\n \tfree(opts->prefix);\n \tfree(opts->suffix);\n \tfree(opts->separator);\n+\tfree(opts->pointer);\n+\tfree(opts->tag);\n }\n \n static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */\ndiff --git a/t/t4205-log-pretty-formats.sh b/t/t4205-log-pretty-formats.sh\nindex 6ba399c5be..16626e4fe9 100755\n--- a/t/t4205-log-pretty-formats.sh\n+++ b/t/t4205-log-pretty-formats.sh\n@@ -600,7 +600,12 @@ test_expect_success 'pretty format %decorate' '\n \techo \"%(decorate:prefix=[ ,suffix= ],separater=; )\" >expect4 &&\n \tgit log --format=\"%(decorate:prefix=[ ,suffix= ],separater=%x3B )\" \\\n \t\t-1 >actual4 &&\n-\ttest_cmp expect4 actual4\n+\ttest_cmp expect4 actual4 &&\n+\n+\techo \"HEAD->foo bar qux\" >expect5 &&\n+\tgit log --format=\"%(decorate:prefix=,suffix=,separator= ,tag=,pointer=->)\" \\\n+\t\t-1 >actual5 &&\n+\ttest_cmp expect5 actual5\n '\n \n cat >trailers <<EOF\n-- \n2.42.0-rc2\n\n"},{"id":"480821","messageId":"20230820085336.8615-9-andy.koppe@gmail.com","threadId":"59990","inReplyTo":"20230820085336.8615-1-andy.koppe@gmail.com","subject":"[PATCH v4 8/8] decorate: use commit color for HEAD arrow","fromName":"Andy Koppe","fromEmail":"andy.koppe@gmail.com","sentAt":"2023-08-20T08:53:36Z","receivedAt":"2023-08-20T09:00:11Z","isPatch":true,"sender":{"key":"andy.koppe@gmail.com","avatar":"https://avatars.githubusercontent.com/u/223411?v=4"},"body":"Use the commit color instead of the HEAD color for the arrow or custom\nsymbol in \"HEAD -> branch\" decorations, for visual consistency with the\nprefix, separator and suffix symbols, which are also colored with the\ncommit color.\n\nThis change was triggered by the possibility that one could choose to\nuse the same symbol for the pointer and the separator options in\n%(decorate), in which case they ought to be the same color.\n\nA related precedent is 'ls -l', where the arrow for symlinks gets the\ndefault color rather than that of the symlink name.\n\nAmend test t4207-log-decoration-colors.sh accordingly.\n\nSigned-off-by: Andy Koppe <andy.koppe@gmail.com>\n---\n log-tree.c                       | 2 +-\n t/t4207-log-decoration-colors.sh | 6 +++---\n 2 files changed, 4 insertions(+), 4 deletions(-)\n\ndiff --git a/log-tree.c b/log-tree.c\nindex 50b4850eda..504da6b519 100644\n--- a/log-tree.c\n+++ b/log-tree.c\n@@ -369,7 +369,7 @@ void format_decorations(struct strbuf *sb,\n \n \t\t\tif (current_and_HEAD &&\n \t\t\t    decoration->type == DECORATION_REF_HEAD) {\n-\t\t\t\tstrbuf_addstr(sb, color);\n+\t\t\t\tstrbuf_addstr(sb, color_commit);\n \t\t\t\tstrbuf_addstr(sb, pointer);\n \t\t\t\tstrbuf_addstr(sb, color_reset);\n \t\t\t\tstrbuf_addstr(sb, decorate_get_color(use_color, current_and_HEAD->type));\ndiff --git a/t/t4207-log-decoration-colors.sh b/t/t4207-log-decoration-colors.sh\nindex df804f38e2..21986a866d 100755\n--- a/t/t4207-log-decoration-colors.sh\n+++ b/t/t4207-log-decoration-colors.sh\n@@ -54,7 +54,7 @@ cmp_filtered_decorations () {\n test_expect_success 'commit decorations colored correctly' '\n \tcat >expect <<-EOF &&\n \t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_HEAD}HEAD${c_reset}\\\n-${c_HEAD} -> ${c_reset}${c_branch}main${c_reset}${c_commit}, \\\n+${c_commit} -> ${c_reset}${c_branch}main${c_reset}${c_commit}, \\\n ${c_reset}${c_tag}tag: ${c_reset}${c_tag}v1.0${c_reset}${c_commit}, \\\n ${c_reset}${c_tag}tag: ${c_reset}${c_tag}B${c_reset}${c_commit})${c_reset} B\n ${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\\\n@@ -79,7 +79,7 @@ test_expect_success 'test coloring with replace-objects' '\n \n \tcat >expect <<-EOF &&\n \t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_HEAD}HEAD${c_reset}\\\n-${c_HEAD} -> ${c_reset}${c_branch}main${c_reset}${c_commit}, \\\n+${c_commit} -> ${c_reset}${c_branch}main${c_reset}${c_commit}, \\\n ${c_reset}${c_tag}tag: ${c_reset}${c_tag}D${c_reset}${c_commit})${c_reset} D\n \t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\\\n ${c_tag}tag: ${c_reset}${c_tag}C${c_reset}${c_commit}, \\\n@@ -105,7 +105,7 @@ test_expect_success 'test coloring with grafted commit' '\n \n \tcat >expect <<-EOF &&\n \t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_HEAD}HEAD${c_reset}\\\n-${c_HEAD} -> ${c_reset}${c_branch}main${c_reset}${c_commit}, \\\n+${c_commit} -> ${c_reset}${c_branch}main${c_reset}${c_commit}, \\\n ${c_reset}${c_tag}tag: ${c_reset}${c_tag}D${c_reset}${c_commit}, \\\n ${c_reset}${c_grafted}replaced${c_reset}${c_commit})${c_reset} D\n \t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\\\n-- \n2.42.0-rc2\n\n"},{"id":"480823","messageId":"20230820185009.20095-1-andy.koppe@gmail.com","threadId":"59990","inReplyTo":"20230820085336.8615-1-andy.koppe@gmail.com","subject":"[PATCH v5 0/8] pretty: add %(decorate[:<options>]) format","fromName":"Andy Koppe","fromEmail":"andy.koppe@gmail.com","sentAt":"2023-08-20T18:50:01Z","receivedAt":"2023-08-20T18:55:58Z","isPatch":true,"sender":{"key":"andy.koppe@gmail.com","avatar":"https://avatars.githubusercontent.com/u/223411?v=4"},"body":"Apologies for sending another version so soon, but I realized that I\nhadn't removed the use of a compound literal from the first commit where\nI had added it, so it still appeared in the patches. The overall diff\nfor v5 is the same as for v4.\n\nAndy Koppe (7):\n  pretty-formats: enclose options in angle brackets\n  decorate: refactor format_decorations()\n  decorate: avoid some unnecessary color overhead\n  decorate: color each token separately\n  pretty: add %(decorate[:<options>]) format\n  pretty: add pointer and tag options to %(decorate)\n  decorate: use commit color for HEAD arrow\n\nJunio C Hamano (1):\n  pretty-formats: define \"literal formatting code\"\n\n Documentation/pretty-formats.txt | 47 +++++++++++++-------\n log-tree.c                       | 72 +++++++++++++++++++++---------\n log-tree.h                       | 17 ++++---\n pretty.c                         | 76 +++++++++++++++++++++++++++++---\n t/t4205-log-pretty-formats.sh    | 32 ++++++++++++++\n t/t4207-log-decoration-colors.sh | 44 ++++++++++--------\n 6 files changed, 219 insertions(+), 69 deletions(-)\n\n-- \n2.42.0-rc2\n\n"},{"id":"480824","messageId":"20230820185009.20095-2-andy.koppe@gmail.com","threadId":"59990","inReplyTo":"20230820185009.20095-1-andy.koppe@gmail.com","subject":"[PATCH v5 1/8] pretty-formats: define \"literal formatting code\"","fromName":"Andy Koppe","fromEmail":"andy.koppe@gmail.com","sentAt":"2023-08-20T18:50:02Z","receivedAt":"2023-08-20T18:55:58Z","isPatch":true,"sender":{"key":"andy.koppe@gmail.com","avatar":"https://avatars.githubusercontent.com/u/223411?v=4"},"body":"From: Junio C Hamano <gitster@pobox.com>\n\nThe description for a %(trailer) option already uses this term without\nhaving a definition anywhere in the document, and we are about to add\nanother one in %(decorate) that uses it.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\nSigned-off-by: Andy Koppe <andy.koppe@gmail.com>\n---\n Documentation/pretty-formats.txt | 4 +++-\n 1 file changed, 3 insertions(+), 1 deletion(-)\n\ndiff --git a/Documentation/pretty-formats.txt b/Documentation/pretty-formats.txt\nindex 3b71334459..5e1432951b 100644\n--- a/Documentation/pretty-formats.txt\n+++ b/Documentation/pretty-formats.txt\n@@ -122,7 +122,9 @@ The placeholders are:\n - Placeholders that expand to a single literal character:\n '%n':: newline\n '%%':: a raw '%'\n-'%x00':: print a byte from a hex code\n+'%x00':: '%x' followed by two hexadecimal digits is replaced with a\n+\t byte with the hexadecimal digits' value (we will call this\n+\t \"literal formatting code\" in the rest of this document).\n \n - Placeholders that affect formatting of later placeholders:\n '%Cred':: switch color to red\n-- \n2.42.0-rc2\n\n"},{"id":"480825","messageId":"20230820185009.20095-3-andy.koppe@gmail.com","threadId":"59990","inReplyTo":"20230820185009.20095-1-andy.koppe@gmail.com","subject":"[PATCH v5 2/8] pretty-formats: enclose options in angle brackets","fromName":"Andy Koppe","fromEmail":"andy.koppe@gmail.com","sentAt":"2023-08-20T18:50:03Z","receivedAt":"2023-08-20T18:55:58Z","isPatch":true,"sender":{"key":"andy.koppe@gmail.com","avatar":"https://avatars.githubusercontent.com/u/223411?v=4"},"body":"Enclose the 'options' placeholders in the documentation of the\n%(describe) and %(trailers) format specifiers in angle brackets to\nclarify that they are placeholders rather than keywords.\n\nAlso remove the indentation from their descriptions, instead of\nincreasing it to account for the extra two angle brackets in the\nheadings. The indentation isn't required by asciidoc, it doesn't reflect\nhow the output text is formatted, and it's inconsistent with the\nfollowing bullet points that are at the same level in the output.\n\nSigned-off-by: Andy Koppe <andy.koppe@gmail.com>\n---\n Documentation/pretty-formats.txt | 24 ++++++++++--------------\n 1 file changed, 10 insertions(+), 14 deletions(-)\n\ndiff --git a/Documentation/pretty-formats.txt b/Documentation/pretty-formats.txt\nindex 5e1432951b..851a9878e6 100644\n--- a/Documentation/pretty-formats.txt\n+++ b/Documentation/pretty-formats.txt\n@@ -224,13 +224,11 @@ The placeholders are:\n \tlinkgit:git-rev-list[1])\n '%d':: ref names, like the --decorate option of linkgit:git-log[1]\n '%D':: ref names without the \" (\", \")\" wrapping.\n-'%(describe[:options])':: human-readable name, like\n-\t\t\t  linkgit:git-describe[1]; empty string for\n-\t\t\t  undescribable commits.  The `describe` string\n-\t\t\t  may be followed by a colon and zero or more\n-\t\t\t  comma-separated options.  Descriptions can be\n-\t\t\t  inconsistent when tags are added or removed at\n-\t\t\t  the same time.\n+'%(describe[:<options>])'::\n+human-readable name, like linkgit:git-describe[1]; empty string for\n+undescribable commits.  The `describe` string may be followed by a colon and\n+zero or more comma-separated options.  Descriptions can be inconsistent when\n+tags are added or removed at the same time.\n +\n ** 'tags[=<bool-value>]': Instead of only considering annotated tags,\n    consider lightweight tags as well.\n@@ -283,13 +281,11 @@ endif::git-rev-list[]\n '%gE':: reflog identity email (respecting .mailmap, see\n \tlinkgit:git-shortlog[1] or linkgit:git-blame[1])\n '%gs':: reflog subject\n-'%(trailers[:options])':: display the trailers of the body as\n-\t\t\t  interpreted by\n-\t\t\t  linkgit:git-interpret-trailers[1]. The\n-\t\t\t  `trailers` string may be followed by a colon\n-\t\t\t  and zero or more comma-separated options.\n-\t\t\t  If any option is provided multiple times the\n-\t\t\t  last occurrence wins.\n+'%(trailers[:<options>])'::\n+display the trailers of the body as interpreted by\n+linkgit:git-interpret-trailers[1]. The `trailers` string may be followed by\n+a colon and zero or more comma-separated options. If any option is provided\n+multiple times, the last occurrence wins.\n +\n ** 'key=<key>': only show trailers with specified <key>. Matching is done\n    case-insensitively and trailing colon is optional. If option is\n-- \n2.42.0-rc2\n\n"},{"id":"480826","messageId":"20230820185009.20095-5-andy.koppe@gmail.com","threadId":"59990","inReplyTo":"20230820185009.20095-1-andy.koppe@gmail.com","subject":"[PATCH v5 4/8] decorate: avoid some unnecessary color overhead","fromName":"Andy Koppe","fromEmail":"andy.koppe@gmail.com","sentAt":"2023-08-20T18:50:05Z","receivedAt":"2023-08-20T18:56:06Z","isPatch":true,"sender":{"key":"andy.koppe@gmail.com","avatar":"https://avatars.githubusercontent.com/u/223411?v=4"},"body":"In format_decorations(), don't obtain color sequences if there are no\ndecorations, and don't emit color sequences around empty strings.\n\nSigned-off-by: Andy Koppe <andy.koppe@gmail.com>\n---\n log-tree.c | 25 +++++++++++++++----------\n 1 file changed, 15 insertions(+), 10 deletions(-)\n\ndiff --git a/log-tree.c b/log-tree.c\nindex cd12c26c29..7c6d3f1ac3 100644\n--- a/log-tree.c\n+++ b/log-tree.c\n@@ -312,10 +312,7 @@ void format_decorations(struct strbuf *sb,\n {\n \tconst struct name_decoration *decoration;\n \tconst struct name_decoration *current_and_HEAD;\n-\tconst char *color_commit =\n-\t\tdiff_get_color(use_color, DIFF_COMMIT);\n-\tconst char *color_reset =\n-\t\tdecorate_get_color(use_color, DECORATION_NONE);\n+\tconst char *color_commit, *color_reset;\n \n \tconst char *prefix = \" (\";\n \tconst char *suffix = \")\";\n@@ -334,6 +331,9 @@ void format_decorations(struct strbuf *sb,\n \t\t\tseparator = opts->separator;\n \t}\n \n+\tcolor_commit = diff_get_color(use_color, DIFF_COMMIT);\n+\tcolor_reset = decorate_get_color(use_color, DECORATION_NONE);\n+\n \tcurrent_and_HEAD = current_pointed_by_HEAD(decoration);\n \twhile (decoration) {\n \t\t/*\n@@ -342,9 +342,12 @@ void format_decorations(struct strbuf *sb,\n \t\t * appeared, skipping the entry for current.\n \t\t */\n \t\tif (decoration != current_and_HEAD) {\n-\t\t\tstrbuf_addstr(sb, color_commit);\n-\t\t\tstrbuf_addstr(sb, prefix);\n-\t\t\tstrbuf_addstr(sb, color_reset);\n+\t\t\tif (*prefix) {\n+\t\t\t\tstrbuf_addstr(sb, color_commit);\n+\t\t\t\tstrbuf_addstr(sb, prefix);\n+\t\t\t\tstrbuf_addstr(sb, color_reset);\n+\t\t\t}\n+\n \t\t\tstrbuf_addstr(sb, decorate_get_color(use_color, decoration->type));\n \t\t\tif (decoration->type == DECORATION_REF_TAG)\n \t\t\t\tstrbuf_addstr(sb, \"tag: \");\n@@ -364,9 +367,11 @@ void format_decorations(struct strbuf *sb,\n \t\t}\n \t\tdecoration = decoration->next;\n \t}\n-\tstrbuf_addstr(sb, color_commit);\n-\tstrbuf_addstr(sb, suffix);\n-\tstrbuf_addstr(sb, color_reset);\n+\tif (*suffix) {\n+\t\tstrbuf_addstr(sb, color_commit);\n+\t\tstrbuf_addstr(sb, suffix);\n+\t\tstrbuf_addstr(sb, color_reset);\n+\t}\n }\n \n void show_decorations(struct rev_info *opt, struct commit *commit)\n-- \n2.42.0-rc2\n\n"},{"id":"480827","messageId":"20230820185009.20095-6-andy.koppe@gmail.com","threadId":"59990","inReplyTo":"20230820185009.20095-1-andy.koppe@gmail.com","subject":"[PATCH v5 5/8] decorate: color each token separately","fromName":"Andy Koppe","fromEmail":"andy.koppe@gmail.com","sentAt":"2023-08-20T18:50:06Z","receivedAt":"2023-08-20T18:56:06Z","isPatch":true,"sender":{"key":"andy.koppe@gmail.com","avatar":"https://avatars.githubusercontent.com/u/223411?v=4"},"body":"Wrap \"tag:\" prefixes and the arrows in \"HEAD -> branch\" decorations in\ntheir own color sequences. Otherwise, if --graph is used, tag names or\narrows can end up uncolored when %w width formatting breaks a line just\nbefore them. This is because --graph resets the color after doing its\ndrawing at the start of a line.\n\nAmend test t4207-log-decoration-colors.sh accordingly.\n\nSigned-off-by: Andy Koppe <andy.koppe@gmail.com>\n---\n log-tree.c                       | 14 +++++++---\n t/t4207-log-decoration-colors.sh | 44 ++++++++++++++++++--------------\n 2 files changed, 36 insertions(+), 22 deletions(-)\n\ndiff --git a/log-tree.c b/log-tree.c\nindex 7c6d3f1ac3..44f4693567 100644\n--- a/log-tree.c\n+++ b/log-tree.c\n@@ -342,26 +342,34 @@ void format_decorations(struct strbuf *sb,\n \t\t * appeared, skipping the entry for current.\n \t\t */\n \t\tif (decoration != current_and_HEAD) {\n+\t\t\tconst char *color =\n+\t\t\t\tdecorate_get_color(use_color, decoration->type);\n+\n \t\t\tif (*prefix) {\n \t\t\t\tstrbuf_addstr(sb, color_commit);\n \t\t\t\tstrbuf_addstr(sb, prefix);\n \t\t\t\tstrbuf_addstr(sb, color_reset);\n \t\t\t}\n \n-\t\t\tstrbuf_addstr(sb, decorate_get_color(use_color, decoration->type));\n-\t\t\tif (decoration->type == DECORATION_REF_TAG)\n+\t\t\tif (decoration->type == DECORATION_REF_TAG) {\n+\t\t\t\tstrbuf_addstr(sb, color);\n \t\t\t\tstrbuf_addstr(sb, \"tag: \");\n+\t\t\t\tstrbuf_addstr(sb, color_reset);\n+\t\t\t}\n \n+\t\t\tstrbuf_addstr(sb, color);\n \t\t\tshow_name(sb, decoration);\n+\t\t\tstrbuf_addstr(sb, color_reset);\n \n \t\t\tif (current_and_HEAD &&\n \t\t\t    decoration->type == DECORATION_REF_HEAD) {\n+\t\t\t\tstrbuf_addstr(sb, color);\n \t\t\t\tstrbuf_addstr(sb, \" -> \");\n \t\t\t\tstrbuf_addstr(sb, color_reset);\n \t\t\t\tstrbuf_addstr(sb, decorate_get_color(use_color, current_and_HEAD->type));\n \t\t\t\tshow_name(sb, current_and_HEAD);\n+\t\t\t\tstrbuf_addstr(sb, color_reset);\n \t\t\t}\n-\t\t\tstrbuf_addstr(sb, color_reset);\n \n \t\t\tprefix = separator;\n \t\t}\ndiff --git a/t/t4207-log-decoration-colors.sh b/t/t4207-log-decoration-colors.sh\nindex ded33a82e2..df804f38e2 100755\n--- a/t/t4207-log-decoration-colors.sh\n+++ b/t/t4207-log-decoration-colors.sh\n@@ -53,15 +53,17 @@ cmp_filtered_decorations () {\n # to this test since it does not contain any decoration, hence --first-parent\n test_expect_success 'commit decorations colored correctly' '\n \tcat >expect <<-EOF &&\n-\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_HEAD}HEAD -> \\\n-${c_reset}${c_branch}main${c_reset}${c_commit}, \\\n-${c_reset}${c_tag}tag: v1.0${c_reset}${c_commit}, \\\n-${c_reset}${c_tag}tag: B${c_reset}${c_commit})${c_reset} B\n-${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_tag}tag: A1${c_reset}${c_commit}, \\\n+\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_HEAD}HEAD${c_reset}\\\n+${c_HEAD} -> ${c_reset}${c_branch}main${c_reset}${c_commit}, \\\n+${c_reset}${c_tag}tag: ${c_reset}${c_tag}v1.0${c_reset}${c_commit}, \\\n+${c_reset}${c_tag}tag: ${c_reset}${c_tag}B${c_reset}${c_commit})${c_reset} B\n+${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\\\n+${c_tag}tag: ${c_reset}${c_tag}A1${c_reset}${c_commit}, \\\n ${c_reset}${c_remoteBranch}other/main${c_reset}${c_commit})${c_reset} A1\n-\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_stash}refs/stash${c_reset}${c_commit})${c_reset} \\\n-On main: Changes to A.t\n-\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_tag}tag: A${c_reset}${c_commit})${c_reset} A\n+\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\\\n+${c_stash}refs/stash${c_reset}${c_commit})${c_reset} On main: Changes to A.t\n+\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\\\n+${c_tag}tag: ${c_reset}${c_tag}A${c_reset}${c_commit})${c_reset} A\n \tEOF\n \n \tgit log --first-parent --no-abbrev --decorate --oneline --color=always --all >actual &&\n@@ -76,12 +78,14 @@ test_expect_success 'test coloring with replace-objects' '\n \tgit replace HEAD~1 HEAD~2 &&\n \n \tcat >expect <<-EOF &&\n-\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_HEAD}HEAD -> \\\n-${c_reset}${c_branch}main${c_reset}${c_commit}, \\\n-${c_reset}${c_tag}tag: D${c_reset}${c_commit})${c_reset} D\n-\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_tag}tag: C${c_reset}${c_commit}, \\\n+\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_HEAD}HEAD${c_reset}\\\n+${c_HEAD} -> ${c_reset}${c_branch}main${c_reset}${c_commit}, \\\n+${c_reset}${c_tag}tag: ${c_reset}${c_tag}D${c_reset}${c_commit})${c_reset} D\n+\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\\\n+${c_tag}tag: ${c_reset}${c_tag}C${c_reset}${c_commit}, \\\n ${c_reset}${c_grafted}replaced${c_reset}${c_commit})${c_reset} B\n-\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_tag}tag: A${c_reset}${c_commit})${c_reset} A\n+\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\\\n+${c_tag}tag: ${c_reset}${c_tag}A${c_reset}${c_commit})${c_reset} A\n EOF\n \n \tgit log --first-parent --no-abbrev --decorate --oneline --color=always HEAD >actual &&\n@@ -100,13 +104,15 @@ test_expect_success 'test coloring with grafted commit' '\n \tgit replace --graft HEAD HEAD~2 &&\n \n \tcat >expect <<-EOF &&\n-\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_HEAD}HEAD -> \\\n-${c_reset}${c_branch}main${c_reset}${c_commit}, \\\n-${c_reset}${c_tag}tag: D${c_reset}${c_commit}, \\\n+\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_HEAD}HEAD${c_reset}\\\n+${c_HEAD} -> ${c_reset}${c_branch}main${c_reset}${c_commit}, \\\n+${c_reset}${c_tag}tag: ${c_reset}${c_tag}D${c_reset}${c_commit}, \\\n ${c_reset}${c_grafted}replaced${c_reset}${c_commit})${c_reset} D\n-\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_tag}tag: v1.0${c_reset}${c_commit}, \\\n-${c_reset}${c_tag}tag: B${c_reset}${c_commit})${c_reset} B\n-\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_tag}tag: A${c_reset}${c_commit})${c_reset} A\n+\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\\\n+${c_tag}tag: ${c_reset}${c_tag}v1.0${c_reset}${c_commit}, \\\n+${c_reset}${c_tag}tag: ${c_reset}${c_tag}B${c_reset}${c_commit})${c_reset} B\n+\t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\\\n+${c_tag}tag: ${c_reset}${c_tag}A${c_reset}${c_commit})${c_reset} A\n \tEOF\n \n \tgit log --first-parent --no-abbrev --decorate --oneline --color=always HEAD >actual &&\n-- \n2.42.0-rc2\n\n"},{"id":"480828","messageId":"20230820185009.20095-4-andy.koppe@gmail.com","threadId":"59990","inReplyTo":"20230820185009.20095-1-andy.koppe@gmail.com","subject":"[PATCH v5 3/8] decorate: refactor format_decorations()","fromName":"Andy Koppe","fromEmail":"andy.koppe@gmail.com","sentAt":"2023-08-20T18:50:04Z","receivedAt":"2023-08-20T18:56:06Z","isPatch":true,"sender":{"key":"andy.koppe@gmail.com","avatar":"https://avatars.githubusercontent.com/u/223411?v=4"},"body":"Rename the format_decorations_extended function to format_decorations\nand drop the format_decorations wrapper macro. Pass the prefix, suffix\nand separator strings as a single 'struct format_decorations' pointer\nargument instead of separate arguments. Use default values defined in\nthe function when either the struct pointer or any of the struct fields\nare NULL. This is to ease extension with additional options.\n\nSigned-off-by: Andy Koppe <andy.koppe@gmail.com>\n---\n log-tree.c | 23 +++++++++++++++++------\n log-tree.h | 15 ++++++++-------\n pretty.c   | 13 ++++++++++---\n 3 files changed, 35 insertions(+), 16 deletions(-)\n\ndiff --git a/log-tree.c b/log-tree.c\nindex 208c69cbb7..cd12c26c29 100644\n--- a/log-tree.c\n+++ b/log-tree.c\n@@ -303,14 +303,12 @@ static void show_name(struct strbuf *sb, const struct name_decoration *decoratio\n \n /*\n  * The caller makes sure there is no funny color before calling.\n- * format_decorations_extended makes sure the same after return.\n+ * format_decorations ensures the same after return.\n  */\n-void format_decorations_extended(struct strbuf *sb,\n+void format_decorations(struct strbuf *sb,\n \t\t\tconst struct commit *commit,\n \t\t\tint use_color,\n-\t\t\tconst char *prefix,\n-\t\t\tconst char *separator,\n-\t\t\tconst char *suffix)\n+\t\t\tconst struct decoration_options *opts)\n {\n \tconst struct name_decoration *decoration;\n \tconst struct name_decoration *current_and_HEAD;\n@@ -319,10 +317,23 @@ void format_decorations_extended(struct strbuf *sb,\n \tconst char *color_reset =\n \t\tdecorate_get_color(use_color, DECORATION_NONE);\n \n+\tconst char *prefix = \" (\";\n+\tconst char *suffix = \")\";\n+\tconst char *separator = \", \";\n+\n \tdecoration = get_name_decoration(&commit->object);\n \tif (!decoration)\n \t\treturn;\n \n+\tif (opts) {\n+\t\tif (opts->prefix)\n+\t\t\tprefix = opts->prefix;\n+\t\tif (opts->suffix)\n+\t\t\tsuffix = opts->suffix;\n+\t\tif (opts->separator)\n+\t\t\tseparator = opts->separator;\n+\t}\n+\n \tcurrent_and_HEAD = current_pointed_by_HEAD(decoration);\n \twhile (decoration) {\n \t\t/*\n@@ -370,7 +381,7 @@ void show_decorations(struct rev_info *opt, struct commit *commit)\n \t}\n \tif (!opt->show_decorations)\n \t\treturn;\n-\tformat_decorations(&sb, commit, opt->diffopt.use_color);\n+\tformat_decorations(&sb, commit, opt->diffopt.use_color, NULL);\n \tfputs(sb.buf, opt->diffopt.file);\n \tstrbuf_release(&sb);\n }\ndiff --git a/log-tree.h b/log-tree.h\nindex bdb6432815..14898de8ac 100644\n--- a/log-tree.h\n+++ b/log-tree.h\n@@ -13,17 +13,18 @@ struct decoration_filter {\n \tstruct string_list *exclude_ref_config_pattern;\n };\n \n+struct decoration_options {\n+\tchar *prefix;\n+\tchar *suffix;\n+\tchar *separator;\n+};\n+\n int parse_decorate_color_config(const char *var, const char *slot_name, const char *value);\n int log_tree_diff_flush(struct rev_info *);\n int log_tree_commit(struct rev_info *, struct commit *);\n void show_log(struct rev_info *opt);\n-void format_decorations_extended(struct strbuf *sb, const struct commit *commit,\n-\t\t\t     int use_color,\n-\t\t\t     const char *prefix,\n-\t\t\t     const char *separator,\n-\t\t\t     const char *suffix);\n-#define format_decorations(strbuf, commit, color) \\\n-\t\t\t     format_decorations_extended((strbuf), (commit), (color), \" (\", \", \", \")\")\n+void format_decorations(struct strbuf *sb, const struct commit *commit,\n+\t\t\tint use_color, const struct decoration_options *opts);\n void show_decorations(struct rev_info *opt, struct commit *commit);\n void log_write_email_headers(struct rev_info *opt, struct commit *commit,\n \t\t\t     const char **extra_headers_p,\ndiff --git a/pretty.c b/pretty.c\nindex 718530bbab..69b6db3340 100644\n--- a/pretty.c\n+++ b/pretty.c\n@@ -1537,11 +1537,18 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */\n \t\tstrbuf_addstr(sb, get_revision_mark(NULL, commit));\n \t\treturn 1;\n \tcase 'd':\n-\t\tformat_decorations(sb, commit, c->auto_color);\n+\t\tformat_decorations(sb, commit, c->auto_color, NULL);\n \t\treturn 1;\n \tcase 'D':\n-\t\tformat_decorations_extended(sb, commit, c->auto_color, \"\", \", \", \"\");\n-\t\treturn 1;\n+\t\t{\n+\t\t\tconst struct decoration_options opts = {\n+\t\t\t\t.prefix = \"\",\n+\t\t\t\t.suffix = \"\"\n+\t\t\t};\n+\n+\t\t\tformat_decorations(sb, commit, c->auto_color, &opts);\n+\t\t\treturn 1;\n+\t\t}\n \tcase 'S':\t\t/* tag/branch like --source */\n \t\tif (!(c->pretty_ctx->rev && c->pretty_ctx->rev->sources))\n \t\t\treturn 0;\n-- \n2.42.0-rc2\n\n"},{"id":"480830","messageId":"20230820185009.20095-7-andy.koppe@gmail.com","threadId":"59990","inReplyTo":"20230820185009.20095-1-andy.koppe@gmail.com","subject":"[PATCH v5 6/8] pretty: add %(decorate[:<options>]) format","fromName":"Andy Koppe","fromEmail":"andy.koppe@gmail.com","sentAt":"2023-08-20T18:50:07Z","receivedAt":"2023-08-20T18:56:06Z","isPatch":true,"sender":{"key":"andy.koppe@gmail.com","avatar":"https://avatars.githubusercontent.com/u/223411?v=4"},"body":"Add %(decorate[:<options>]) format that lists ref names similarly to the\n%d format, but which allows the otherwise fixed prefix, suffix and\nseparator strings to be customized. Omitted options default to the\nstrings used in %d.\n\nRename expand_separator() function used to expand %x literal formatting\ncodes to expand_string_arg(), as it is now used on strings other than\nseparators.\n\nExamples:\n- %(decorate) is equivalent to %d.\n- %(decorate:prefix=,suffix=) is equivalent to %D.\n- %(decorate:prefix=[,suffix=],separator=%x3B) produces a list enclosed\nin square brackets and separated by semicolons.\n\nTest the format in t4205-log-pretty-formats.sh and document it in\npretty-formats.txt.\n\nSigned-off-by: Andy Koppe <andy.koppe@gmail.com>\n---\n Documentation/pretty-formats.txt | 10 ++++++\n pretty.c                         | 59 +++++++++++++++++++++++++++++---\n t/t4205-log-pretty-formats.sh    | 27 +++++++++++++++\n 3 files changed, 92 insertions(+), 4 deletions(-)\n\ndiff --git a/Documentation/pretty-formats.txt b/Documentation/pretty-formats.txt\nindex 851a9878e6..709d85af21 100644\n--- a/Documentation/pretty-formats.txt\n+++ b/Documentation/pretty-formats.txt\n@@ -224,6 +224,16 @@ The placeholders are:\n \tlinkgit:git-rev-list[1])\n '%d':: ref names, like the --decorate option of linkgit:git-log[1]\n '%D':: ref names without the \" (\", \")\" wrapping.\n+'%(decorate[:<options>])'::\n+ref names with custom decorations. The `decorate` string may be followed by a\n+colon and zero or more comma-separated options. Option values may contain\n+literal formatting codes. These must be used for commas (`%x2C`) and closing\n+parentheses (`%x29`), due to their role in the option syntax.\n++\n+** 'prefix=<value>': Shown before the list of ref names.  Defaults to \"{nbsp}`(`\".\n+** 'suffix=<value>': Shown after the list of ref names.  Defaults to \"`)`\".\n+** 'separator=<value>': Shown between ref names.  Defaults to \"`,`{nbsp}\".\n+\n '%(describe[:<options>])'::\n human-readable name, like linkgit:git-describe[1]; empty string for\n undescribable commits.  The `describe` string may be followed by a colon and\ndiff --git a/pretty.c b/pretty.c\nindex 69b6db3340..1639efe2f8 100644\n--- a/pretty.c\n+++ b/pretty.c\n@@ -1252,8 +1252,8 @@ static int format_trailer_match_cb(const struct strbuf *key, void *ud)\n \treturn 0;\n }\n \n-static struct strbuf *expand_separator(struct strbuf *sb,\n-\t\t\t\t       const char *argval, size_t arglen)\n+static struct strbuf *expand_string_arg(struct strbuf *sb,\n+\t\t\t\t\tconst char *argval, size_t arglen)\n {\n \tchar *fmt = xstrndup(argval, arglen);\n \tconst char *format = fmt;\n@@ -1301,9 +1301,9 @@ int format_set_trailers_options(struct process_trailer_options *opts,\n \t\t\topts->filter_data = filter_list;\n \t\t\topts->only_trailers = 1;\n \t\t} else if (match_placeholder_arg_value(*arg, \"separator\", arg, &argval, &arglen)) {\n-\t\t\topts->separator = expand_separator(sepbuf, argval, arglen);\n+\t\t\topts->separator = expand_string_arg(sepbuf, argval, arglen);\n \t\t} else if (match_placeholder_arg_value(*arg, \"key_value_separator\", arg, &argval, &arglen)) {\n-\t\t\topts->key_value_separator = expand_separator(kvsepbuf, argval, arglen);\n+\t\t\topts->key_value_separator = expand_string_arg(kvsepbuf, argval, arglen);\n \t\t} else if (!match_placeholder_bool_arg(*arg, \"only\", arg, &opts->only_trailers) &&\n \t\t\t   !match_placeholder_bool_arg(*arg, \"unfold\", arg, &opts->unfold) &&\n \t\t\t   !match_placeholder_bool_arg(*arg, \"keyonly\", arg, &opts->key_only) &&\n@@ -1384,6 +1384,40 @@ static size_t parse_describe_args(const char *start, struct strvec *args)\n \treturn arg - start;\n }\n \n+\n+static int parse_decoration_option(const char **arg,\n+\t\t\t\t   const char *name,\n+\t\t\t\t   char **opt)\n+{\n+\tconst char *argval;\n+\tsize_t arglen;\n+\n+\tif (match_placeholder_arg_value(*arg, name, arg, &argval, &arglen)) {\n+\t\tstruct strbuf sb = STRBUF_INIT;\n+\n+\t\texpand_string_arg(&sb, argval, arglen);\n+\t\t*opt = strbuf_detach(&sb, NULL);\n+\t\treturn 1;\n+\t}\n+\treturn 0;\n+}\n+\n+static void parse_decoration_options(const char **arg,\n+\t\t\t\t     struct decoration_options *opts)\n+{\n+\twhile (parse_decoration_option(arg, \"prefix\", &opts->prefix) ||\n+\t       parse_decoration_option(arg, \"suffix\", &opts->suffix) ||\n+\t       parse_decoration_option(arg, \"separator\", &opts->separator))\n+\t\t;\n+}\n+\n+static void free_decoration_options(const struct decoration_options *opts)\n+{\n+\tfree(opts->prefix);\n+\tfree(opts->suffix);\n+\tfree(opts->separator);\n+}\n+\n static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */\n \t\t\t\tconst char *placeholder,\n \t\t\t\tvoid *context)\n@@ -1645,6 +1679,23 @@ static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */\n \t\treturn 2;\n \t}\n \n+\tif (skip_prefix(placeholder, \"(decorate\", &arg)) {\n+\t\tstruct decoration_options opts = { NULL };\n+\t\tsize_t ret = 0;\n+\n+\t\tif (*arg == ':') {\n+\t\t\targ++;\n+\t\t\tparse_decoration_options(&arg, &opts);\n+\t\t}\n+\t\tif (*arg == ')') {\n+\t\t\tformat_decorations(sb, commit, c->auto_color, &opts);\n+\t\t\tret = arg - placeholder + 1;\n+\t\t}\n+\n+\t\tfree_decoration_options(&opts);\n+\t\treturn ret;\n+\t}\n+\n \t/* For the rest we have to parse the commit header. */\n \tif (!c->commit_header_parsed) {\n \t\tmsg = c->message =\ndiff --git a/t/t4205-log-pretty-formats.sh b/t/t4205-log-pretty-formats.sh\nindex dd9035aa38..6ba399c5be 100755\n--- a/t/t4205-log-pretty-formats.sh\n+++ b/t/t4205-log-pretty-formats.sh\n@@ -576,6 +576,33 @@ test_expect_success 'clean log decoration' '\n \ttest_cmp expected actual1\n '\n \n+test_expect_success 'pretty format %decorate' '\n+\tgit checkout -b foo &&\n+\tgit commit --allow-empty -m \"new commit\" &&\n+\tgit tag bar &&\n+\tgit branch qux &&\n+\n+\techo \" (HEAD -> foo, tag: bar, qux)\" >expect1 &&\n+\tgit log --format=\"%(decorate)\" -1 >actual1 &&\n+\ttest_cmp expect1 actual1 &&\n+\n+\techo \"HEAD -> foo, tag: bar, qux\" >expect2 &&\n+\tgit log --format=\"%(decorate:prefix=,suffix=)\" -1 >actual2 &&\n+\ttest_cmp expect2 actual2 &&\n+\n+\techo \"[ HEAD -> foo; tag: bar; qux ]\" >expect3 &&\n+\tgit log --format=\"%(decorate:prefix=[ ,suffix= ],separator=%x3B )\" \\\n+\t\t-1 >actual3 &&\n+\ttest_cmp expect3 actual3 &&\n+\n+\t# Try with a typo (in \"separator\"), in which case the placeholder should\n+\t# not be replaced.\n+\techo \"%(decorate:prefix=[ ,suffix= ],separater=; )\" >expect4 &&\n+\tgit log --format=\"%(decorate:prefix=[ ,suffix= ],separater=%x3B )\" \\\n+\t\t-1 >actual4 &&\n+\ttest_cmp expect4 actual4\n+'\n+\n cat >trailers <<EOF\n Signed-off-by: A U Thor <author@example.com>\n Acked-by: A U Thor <author@example.com>\n-- \n2.42.0-rc2\n\n"},{"id":"480829","messageId":"20230820185009.20095-9-andy.koppe@gmail.com","threadId":"59990","inReplyTo":"20230820185009.20095-1-andy.koppe@gmail.com","subject":"[PATCH v5 8/8] decorate: use commit color for HEAD arrow","fromName":"Andy Koppe","fromEmail":"andy.koppe@gmail.com","sentAt":"2023-08-20T18:50:09Z","receivedAt":"2023-08-20T18:56:08Z","isPatch":true,"sender":{"key":"andy.koppe@gmail.com","avatar":"https://avatars.githubusercontent.com/u/223411?v=4"},"body":"Use the commit color instead of the HEAD color for the arrow or custom\nsymbol in \"HEAD -> branch\" decorations, for visual consistency with the\nprefix, separator and suffix symbols, which are also colored with the\ncommit color.\n\nThis change was triggered by the possibility that one could choose to\nuse the same symbol for the pointer and the separator options in\n%(decorate), in which case they ought to be the same color.\n\nA related precedent is 'ls -l', where the arrow for symlinks gets the\ndefault color rather than that of the symlink name.\n\nAmend test t4207-log-decoration-colors.sh accordingly.\n\nSigned-off-by: Andy Koppe <andy.koppe@gmail.com>\n---\n log-tree.c                       | 2 +-\n t/t4207-log-decoration-colors.sh | 6 +++---\n 2 files changed, 4 insertions(+), 4 deletions(-)\n\ndiff --git a/log-tree.c b/log-tree.c\nindex 50b4850eda..504da6b519 100644\n--- a/log-tree.c\n+++ b/log-tree.c\n@@ -369,7 +369,7 @@ void format_decorations(struct strbuf *sb,\n \n \t\t\tif (current_and_HEAD &&\n \t\t\t    decoration->type == DECORATION_REF_HEAD) {\n-\t\t\t\tstrbuf_addstr(sb, color);\n+\t\t\t\tstrbuf_addstr(sb, color_commit);\n \t\t\t\tstrbuf_addstr(sb, pointer);\n \t\t\t\tstrbuf_addstr(sb, color_reset);\n \t\t\t\tstrbuf_addstr(sb, decorate_get_color(use_color, current_and_HEAD->type));\ndiff --git a/t/t4207-log-decoration-colors.sh b/t/t4207-log-decoration-colors.sh\nindex df804f38e2..21986a866d 100755\n--- a/t/t4207-log-decoration-colors.sh\n+++ b/t/t4207-log-decoration-colors.sh\n@@ -54,7 +54,7 @@ cmp_filtered_decorations () {\n test_expect_success 'commit decorations colored correctly' '\n \tcat >expect <<-EOF &&\n \t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_HEAD}HEAD${c_reset}\\\n-${c_HEAD} -> ${c_reset}${c_branch}main${c_reset}${c_commit}, \\\n+${c_commit} -> ${c_reset}${c_branch}main${c_reset}${c_commit}, \\\n ${c_reset}${c_tag}tag: ${c_reset}${c_tag}v1.0${c_reset}${c_commit}, \\\n ${c_reset}${c_tag}tag: ${c_reset}${c_tag}B${c_reset}${c_commit})${c_reset} B\n ${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\\\n@@ -79,7 +79,7 @@ test_expect_success 'test coloring with replace-objects' '\n \n \tcat >expect <<-EOF &&\n \t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_HEAD}HEAD${c_reset}\\\n-${c_HEAD} -> ${c_reset}${c_branch}main${c_reset}${c_commit}, \\\n+${c_commit} -> ${c_reset}${c_branch}main${c_reset}${c_commit}, \\\n ${c_reset}${c_tag}tag: ${c_reset}${c_tag}D${c_reset}${c_commit})${c_reset} D\n \t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\\\n ${c_tag}tag: ${c_reset}${c_tag}C${c_reset}${c_commit}, \\\n@@ -105,7 +105,7 @@ test_expect_success 'test coloring with grafted commit' '\n \n \tcat >expect <<-EOF &&\n \t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}${c_HEAD}HEAD${c_reset}\\\n-${c_HEAD} -> ${c_reset}${c_branch}main${c_reset}${c_commit}, \\\n+${c_commit} -> ${c_reset}${c_branch}main${c_reset}${c_commit}, \\\n ${c_reset}${c_tag}tag: ${c_reset}${c_tag}D${c_reset}${c_commit}, \\\n ${c_reset}${c_grafted}replaced${c_reset}${c_commit})${c_reset} D\n \t${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\\\n-- \n2.42.0-rc2\n\n"},{"id":"480831","messageId":"20230820185009.20095-8-andy.koppe@gmail.com","threadId":"59990","inReplyTo":"20230820185009.20095-1-andy.koppe@gmail.com","subject":"[PATCH v5 7/8] pretty: add pointer and tag options to %(decorate)","fromName":"Andy Koppe","fromEmail":"andy.koppe@gmail.com","sentAt":"2023-08-20T18:50:08Z","receivedAt":"2023-08-20T18:56:10Z","isPatch":true,"sender":{"key":"andy.koppe@gmail.com","avatar":"https://avatars.githubusercontent.com/u/223411?v=4"},"body":"Add pointer and tag options to %(decorate) format, to allow to override\nthe \" -> \" string used to show where HEAD points and the \"tag: \" string\nused to mark tags.\n\nDocument in pretty-formats.txt and test in t4205-log-pretty-formats.sh.\n\nSigned-off-by: Andy Koppe <andy.koppe@gmail.com>\n---\n Documentation/pretty-formats.txt |  9 +++++++++\n log-tree.c                       | 12 +++++++++---\n log-tree.h                       |  2 ++\n pretty.c                         |  6 +++++-\n t/t4205-log-pretty-formats.sh    |  7 ++++++-\n 5 files changed, 31 insertions(+), 5 deletions(-)\n\ndiff --git a/Documentation/pretty-formats.txt b/Documentation/pretty-formats.txt\nindex 709d85af21..d38b4ab566 100644\n--- a/Documentation/pretty-formats.txt\n+++ b/Documentation/pretty-formats.txt\n@@ -233,6 +233,15 @@ parentheses (`%x29`), due to their role in the option syntax.\n ** 'prefix=<value>': Shown before the list of ref names.  Defaults to \"{nbsp}`(`\".\n ** 'suffix=<value>': Shown after the list of ref names.  Defaults to \"`)`\".\n ** 'separator=<value>': Shown between ref names.  Defaults to \"`,`{nbsp}\".\n+** 'pointer=<value>': Shown between HEAD and the branch it points to, if any.\n+\t\t      Defaults to \"{nbsp}`->`{nbsp}\".\n+** 'tag=<value>': Shown before tag names. Defaults to \"`tag:`{nbsp}\".\n+\n++\n+For example, to produce decorations with no wrapping\n+or tag annotations, and spaces as separators:\n++\n+`%(decorate:prefix=,suffix=,tag=,separator= )`\n \n '%(describe[:<options>])'::\n human-readable name, like linkgit:git-describe[1]; empty string for\ndiff --git a/log-tree.c b/log-tree.c\nindex 44f4693567..50b4850eda 100644\n--- a/log-tree.c\n+++ b/log-tree.c\n@@ -317,6 +317,8 @@ void format_decorations(struct strbuf *sb,\n \tconst char *prefix = \" (\";\n \tconst char *suffix = \")\";\n \tconst char *separator = \", \";\n+\tconst char *pointer = \" -> \";\n+\tconst char *tag = \"tag: \";\n \n \tdecoration = get_name_decoration(&commit->object);\n \tif (!decoration)\n@@ -329,6 +331,10 @@ void format_decorations(struct strbuf *sb,\n \t\t\tsuffix = opts->suffix;\n \t\tif (opts->separator)\n \t\t\tseparator = opts->separator;\n+\t\tif (opts->pointer)\n+\t\t\tpointer = opts->pointer;\n+\t\tif (opts->tag)\n+\t\t\ttag = opts->tag;\n \t}\n \n \tcolor_commit = diff_get_color(use_color, DIFF_COMMIT);\n@@ -351,9 +357,9 @@ void format_decorations(struct strbuf *sb,\n \t\t\t\tstrbuf_addstr(sb, color_reset);\n \t\t\t}\n \n-\t\t\tif (decoration->type == DECORATION_REF_TAG) {\n+\t\t\tif (*tag && decoration->type == DECORATION_REF_TAG) {\n \t\t\t\tstrbuf_addstr(sb, color);\n-\t\t\t\tstrbuf_addstr(sb, \"tag: \");\n+\t\t\t\tstrbuf_addstr(sb, tag);\n \t\t\t\tstrbuf_addstr(sb, color_reset);\n \t\t\t}\n \n@@ -364,7 +370,7 @@ void format_decorations(struct strbuf *sb,\n \t\t\tif (current_and_HEAD &&\n \t\t\t    decoration->type == DECORATION_REF_HEAD) {\n \t\t\t\tstrbuf_addstr(sb, color);\n-\t\t\t\tstrbuf_addstr(sb, \" -> \");\n+\t\t\t\tstrbuf_addstr(sb, pointer);\n \t\t\t\tstrbuf_addstr(sb, color_reset);\n \t\t\t\tstrbuf_addstr(sb, decorate_get_color(use_color, current_and_HEAD->type));\n \t\t\t\tshow_name(sb, current_and_HEAD);\ndiff --git a/log-tree.h b/log-tree.h\nindex 14898de8ac..41c776fea5 100644\n--- a/log-tree.h\n+++ b/log-tree.h\n@@ -17,6 +17,8 @@ struct decoration_options {\n \tchar *prefix;\n \tchar *suffix;\n \tchar *separator;\n+\tchar *pointer;\n+\tchar *tag;\n };\n \n int parse_decorate_color_config(const char *var, const char *slot_name, const char *value);\ndiff --git a/pretty.c b/pretty.c\nindex 1639efe2f8..7f3abb676c 100644\n--- a/pretty.c\n+++ b/pretty.c\n@@ -1407,7 +1407,9 @@ static void parse_decoration_options(const char **arg,\n {\n \twhile (parse_decoration_option(arg, \"prefix\", &opts->prefix) ||\n \t       parse_decoration_option(arg, \"suffix\", &opts->suffix) ||\n-\t       parse_decoration_option(arg, \"separator\", &opts->separator))\n+\t       parse_decoration_option(arg, \"separator\", &opts->separator) ||\n+\t       parse_decoration_option(arg, \"pointer\", &opts->pointer) ||\n+\t       parse_decoration_option(arg, \"tag\", &opts->tag))\n \t\t;\n }\n \n@@ -1416,6 +1418,8 @@ static void free_decoration_options(const struct decoration_options *opts)\n \tfree(opts->prefix);\n \tfree(opts->suffix);\n \tfree(opts->separator);\n+\tfree(opts->pointer);\n+\tfree(opts->tag);\n }\n \n static size_t format_commit_one(struct strbuf *sb, /* in UTF-8 */\ndiff --git a/t/t4205-log-pretty-formats.sh b/t/t4205-log-pretty-formats.sh\nindex 6ba399c5be..16626e4fe9 100755\n--- a/t/t4205-log-pretty-formats.sh\n+++ b/t/t4205-log-pretty-formats.sh\n@@ -600,7 +600,12 @@ test_expect_success 'pretty format %decorate' '\n \techo \"%(decorate:prefix=[ ,suffix= ],separater=; )\" >expect4 &&\n \tgit log --format=\"%(decorate:prefix=[ ,suffix= ],separater=%x3B )\" \\\n \t\t-1 >actual4 &&\n-\ttest_cmp expect4 actual4\n+\ttest_cmp expect4 actual4 &&\n+\n+\techo \"HEAD->foo bar qux\" >expect5 &&\n+\tgit log --format=\"%(decorate:prefix=,suffix=,separator= ,tag=,pointer=->)\" \\\n+\t\t-1 >actual5 &&\n+\ttest_cmp expect5 actual5\n '\n \n cat >trailers <<EOF\n-- \n2.42.0-rc2\n\n"},{"id":"480839","messageId":"15b89f2e-adb8-ea2b-fd74-2cbe95e20501@gmx.de","threadId":"59990","inReplyTo":"xmqqo7j5uqza.fsf@gitster.g","subject":"Re: main != master at github.com/git/git","fromName":"Johannes Schindelin","fromEmail":"johannes.schindelin@gmx.de","sentAt":"2023-08-21T14:56:28Z","receivedAt":"2023-08-21T14:56:51Z","isPatch":false,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"Hi Junio,\n\nOn Thu, 17 Aug 2023, Junio C Hamano wrote:\n\n> <rsbecker@nexbridge.com> writes:\n>\n> [...] when CI runner sees branches updated to commit that hasn't been\n> worked on, a new job is created to work on that commit, and updating two\n> branches with the same commit at the same time unfortunately means two\n> independent CI jobs work on the same commit in parallel.\n\nMy understanding is that the recommended way to handle this via the\n`concurrency` key [*1*]. That is, if we changed\n\n    concurrency:\n      group: windows-build-${{ github.ref }}\n      cancel-in-progress: ${{ needs.ci-config.outputs.skip_concurrent == 'yes' }}\n\nto\n\n    concurrency:\n      group: windows-build-${{ github.sha }}\n\nthen pushing both `master` and `next` pointing at the same commit would\nstart only one of the workflow runs immediately, keeping the second one\npending until the first run is done. If the first run succeeds, the second\nrun will pick up that status and avoid running everything all over again,\nvia `skip-if-redundant`.\n\nCiao,\nJohannes\n\nFootnote *1*:\nhttps://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#concurrency\n"},{"id":"480842","messageId":"xmqqttssqsj5.fsf@gitster.g","threadId":"59990","inReplyTo":"15b89f2e-adb8-ea2b-fd74-2cbe95e20501@gmx.de","subject":"Re: main != master at github.com/git/git","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2023-08-21T16:17:18Z","receivedAt":"2023-08-21T16:18:15Z","isPatch":false,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:\n\n> My understanding is that the recommended way to handle this via the\n> `concurrency` key [*1*]. That is, if we changed\n>\n>     concurrency:\n>       group: windows-build-${{ github.ref }}\n>       cancel-in-progress: ${{ needs.ci-config.outputs.skip_concurrent == 'yes' }}\n>\n> to\n>\n>     concurrency:\n>       group: windows-build-${{ github.sha }}\n>\n> then pushing both `master` and `next` pointing at the same commit would\n> start only one of the workflow runs immediately, keeping the second one\n> pending until the first run is done.\n\nPerfect.  It is much better than pushing 'master@{24.hours.ago} to\n'main' which was what I used to do avoid the problem between these\ntwo, which I stopped doing because it did not work well.\n\n> If the first run succeeds, the second\n> run will pick up that status and avoid running everything all over again,\n> via `skip-if-redundant`.\n\nNice.  I understand that this would kick in regardless, but the\nright use of the concurrency key would make it far more effective.\n\nVery nice.\n"},{"id":"480857","messageId":"xmqqbkf0p6d9.fsf@gitster.g","threadId":"59990","inReplyTo":"20230820085336.8615-1-andy.koppe@gmail.com","subject":"Re: [PATCH v4 0/8] pretty: add %(decorate[:<options>]) format","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2023-08-21T19:01:22Z","receivedAt":"2023-08-21T19:02:23Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Andy Koppe <andy.koppe@gmail.com> writes:\n\n> Compared to v3, this avoids introducing a compound literal, and splits\n> part of patch 5 into an additional patch 8.\n\nThanks, will requeue.\n"},{"id":"480910","messageId":"xmqq1qfvor35.fsf_-_@gitster.g","threadId":"59990","inReplyTo":"xmqqttssqsj5.fsf@gitster.g","subject":"[PATCH] ci: avoid building from the same commit in parallel","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2023-08-22T00:31:26Z","receivedAt":"2023-08-22T00:33:20Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"At times, we may need to push the same commit to multiple branches\nin the same push.  Rewinding 'next' to rebuild on top of 'master'\nsoon after a release is such an occasion.  Making sure 'main' stays\nin sync with 'master' to help those who expect that primary branch\nof the project is named either of these is another.\n\nWe used to use the branch name as the \"concurrency group\" key, but\nby switching to use the commit object name would make sure the\nbuilds for the same commit would happen serially, and by the time\nthe second job becomes ready to run, the first job's outcome would\nbe available and mking it unnecessary to run the second job.\n\nHelped-by: Johannes Schindelin <johannes.schindelin@gmx.de>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n\n * There are tons of concurrency groups defined, but as a trial\n   change, here is to cover the \"regular\" matrix that consumes the\n   most resources (linux-asan-ubsan is the worst culprit, it seems).\n\n .github/workflows/main.yml | 3 +--\n 1 file changed, 1 insertion(+), 2 deletions(-)\n\ndiff --git a/.github/workflows/main.yml b/.github/workflows/main.yml\nindex 30492eacdd..27b151aadf 100644\n--- a/.github/workflows/main.yml\n+++ b/.github/workflows/main.yml\n@@ -240,8 +240,7 @@ jobs:\n     needs: ci-config\n     if: needs.ci-config.outputs.enabled == 'yes'\n     concurrency:\n-      group: ${{ matrix.vector.jobname }}-${{ matrix.vector.pool }}-${{ github.ref }}\n-      cancel-in-progress: ${{ needs.ci-config.outputs.skip_concurrent == 'yes' }}\n+      group: ${{ matrix.vector.jobname }}-${{ matrix.vector.pool }}-${{ github.sha }}\n     strategy:\n       fail-fast: false\n       matrix:\n-- \n2.42.0\n\n"},{"id":"480916","messageId":"xmqqpm3fn16f.fsf@gitster.g","threadId":"59990","inReplyTo":"xmqq1qfvor35.fsf_-_@gitster.g","subject":"Re: [PATCH] ci: avoid building from the same commit in parallel","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2023-08-22T04:36:24Z","receivedAt":"2023-08-22T04:36:35Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n>  * There are tons of concurrency groups defined, but as a trial\n>    change, here is to cover the \"regular\" matrix that consumes the\n>    most resources (linux-asan-ubsan is the worst culprit, it seems).\n\nUnfortunately, this did not work.\n\nhttps://github.com/git/git/actions/runs/5933451874\nhttps://github.com/git/git/actions/runs/5933451805\n\nare trial runs that had the same commit with this patch pushed to\n'seen' and 'pu'.  While one of them started the \"regular\" matrix,\nthe other one indeed went into paused state and waited.  But that is\nway too late.  What happened was that their \"config\" (which everything\nelse depends on) started in parallel before the serialization at the\n\"regular\" matrix kicked in.\n\nSo, one did wait before doing the \"regular\" matrix, until the other\none finished everything, and then kept going and did its own\n\"regular\" matrix for the same commit.  It is because the avoidance\nof \"redundant build\" was done at the \"config\" phase, which both of\nthem had already done X-<.\n\nIf we wanted to do this, I suspect that we need to serialize the\nentire thing, not at the individual level where we currently define\nthe \"concurrency\" thing.\n"},{"id":"480917","messageId":"99e19de6-c17d-e85f-dc58-1019aed1e2b1@gmx.de","threadId":"59990","inReplyTo":"xmqqpm3fn16f.fsf@gitster.g","subject":"Re: [PATCH] ci: avoid building from the same commit in parallel","fromName":"Johannes Schindelin","fromEmail":"johannes.schindelin@gmx.de","sentAt":"2023-08-22T04:48:33Z","receivedAt":"2023-08-22T04:48:59Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"Hi Junio,\n\nOn Mon, 21 Aug 2023, Junio C Hamano wrote:\n\n> If we wanted to do this, I suspect that we need to serialize the\n> entire thing, not at the individual level where we currently define\n> the \"concurrency\" thing.\n\nRight, we'd need that `concurrency: ${{ github.sha }}` attribute on the\n`config` job.\n\nBTW there is another caveat. According to the documentation, if a job is\nqueued while another job is already queued, that other job is canceled in\nfavor of the latest one.\n\nI have not verified this by testing it out, but have no reason to doubt\nit.\n\nIf that's the case, pushing, say, `master`, `main` and `next` to the same\nSHA will see one of them canceled. Which might be okay, if the cancelation\nmessage on the canceled one is indicative enough.\n\nThis all depends on reducing the number of flakes (in particular the p4\ntests in the ASAN job), but that's a story for another day.\n\nCiao,\nJohannes\n"},{"id":"480925","messageId":"xmqqjztnm6v0.fsf@gitster.g","threadId":"59990","inReplyTo":"99e19de6-c17d-e85f-dc58-1019aed1e2b1@gmx.de","subject":"Re: [PATCH] ci: avoid building from the same commit in parallel","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2023-08-22T15:31:15Z","receivedAt":"2023-08-22T15:31:23Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:\n\n> Right, we'd need that `concurrency: ${{ github.sha }}` attribute on the\n> `config` job.\n\nThat was my first thought, but I am not sure how it would work.\n\nDoesn't skip-if-redundant grab the workflow runs that have succeeded\nand then see if one for the same commit already exists?  If you used\nconcurrency on the 'config', what gets serialized between two jobs\nfor the same commit is only the 'config' phase, so 'master' may wait\nstarting (because 'config' is what everybody else 'needs' it) while\n'config' phase of 'main' runs, and then when it gets to the turn of\n'config' phase of 'master', it would not find the run for the same\ncommit being done for 'main' completed yet, would it?\n\n> BTW there is another caveat. According to the documentation, if a job is\n> queued while another job is already queued, that other job is canceled in\n> favor of the latest one.\n\nYes, that was the impression I got; your second one will wait (so\nyou need a working skip-if-redundant to turn it into noop), but the\nthird and subsequent ones are discarded without starting, which\nunfortunately is what we may want to see happen.\n\nHmph, from that point of view, would the best and simplest we can do\nbe to use the commit object name as the concurrency key for the\n'config' phase, and use something similar to cancel-in-progress\n(which kills the other one when the new one starts, but what we want\nis what stops the new one to start when it notices there is already\none running)?\n"},{"id":"480946","messageId":"1da763f3-60bf-a572-2c71-336b1fa5553d@gmx.de","threadId":"59990","inReplyTo":"xmqqjztnm6v0.fsf@gitster.g","subject":"Re: [PATCH] ci: avoid building from the same commit in parallel","fromName":"Johannes Schindelin","fromEmail":"johannes.schindelin@gmx.de","sentAt":"2023-08-23T08:42:45Z","receivedAt":"2023-08-23T08:49:09Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"Hi Junio,\n\nOn Tue, 22 Aug 2023, Junio C Hamano wrote:\n\n> Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:\n>\n> > Right, we'd need that `concurrency: ${{ github.sha }}` attribute on\n> > the `config` job.\n>\n> That was my first thought, but I am not sure how it would work.\n>\n> Doesn't skip-if-redundant grab the workflow runs that have succeeded\n> and then see if one for the same commit already exists?  If you used\n> concurrency on the 'config', what gets serialized between two jobs\n> for the same commit is only the 'config' phase, so 'master' may wait\n> starting (because 'config' is what everybody else 'needs' it) while\n> 'config' phase of 'main' runs, and then when it gets to the turn of\n> 'config' phase of 'master', it would not find the run for the same\n> commit being done for 'main' completed yet, would it?\n\nYes, that's true.\n\nBut there is a silver lining: the `concurrency` can not only be specified\non the job level, but also on the workflow run level.\n\nI tested this, and present the corresponding patch at the end of this\nmail.\n\n> > BTW there is another caveat. According to the documentation, if a job\n> > is queued while another job is already queued, that other job is\n> > canceled in favor of the latest one.\n>\n> Yes, that was the impression I got; your second one will wait (so\n> you need a working skip-if-redundant to turn it into noop), but the\n> third and subsequent ones are discarded without starting, which\n> unfortunately is what we may want to see happen.\n\nIt's actually the last one that is still pending, while the intermediate\nones will be canceled before they are started (see also the attached\nscreenshot). The message that is shown in the web UI reads like this:\n\n 𐤈 CI: .github#L1\n   Canceling since a higher priority waiting request for '<SHA>' exists\n\nSee https://github.com/dscho/git/actions/runs/5948890677 for an example.\n\nHere is the patch:\n\n-- snipsnap --\nFrom: Junio C Hamano <gitster@pobox.com>\nDate: Mon, 21 Aug 2023 17:31:26 -0700\nSubject: [PATCH] ci: avoid building from the same commit in parallel\n\nAt times, we may need to push the same commit to multiple branches\nin the same push.  Rewinding 'next' to rebuild on top of 'master'\nsoon after a release is such an occasion.  Making sure 'main' stays\nin sync with 'master' to help those who expect that primary branch\nof the project is named either of these is another.\n\nWe already use the branch name as a \"concurrency group\" key, but\nthat does not address the situation illustrated above.\n\nLet's introduce another `concurrency` attribute, using the commit\nhash as the concurrency group key, on the workflow run level, to\naddress this. This will hold any workflow run in the queued state\nwhen there is already a workflow run targeting the same commit,\nuntil that latter run completed. The `skip-if-redundant` check of\nthe second run will then have a chance to see whether the first\nrun succeeded.\n\nThe only caveat with this strategy is that only one workflow run\nwill be kept in the queued state by the `concurrency` feature: if\nanother run targeting the same commit is triggered, the\npreviously-queued run will be canceled. Considering the benefit,\nthis seems the smaller price to pay than to overload Git's build\nagent pool with undesired workflow runs.\n\nHelped-by: Johannes Schindelin <johannes.schindelin@gmx.de>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n .github/workflows/main.yml | 13 +++++++++++++\n 1 file changed, 13 insertions(+)\n\ndiff --git a/.github/workflows/main.yml b/.github/workflows/main.yml\nindex 30492eacddf..1739a0278dc 100644\n--- a/.github/workflows/main.yml\n+++ b/.github/workflows/main.yml\n@@ -5,6 +5,19 @@ on: [push, pull_request]\n env:\n   DEVELOPER: 1\n\n+# If more than one workflow run is triggered for the very same commit hash\n+# (which happens when multiple branches pointing to the same commit), only\n+# the first one is allowed to run, the second will be kept in the \"queued\"\n+# state. This allows a successful completion of the first run to be reused\n+# in the second run via the `skip-if-redundant` logic in the `config` job.\n+#\n+# The only caveat is that if a workflow run is triggered for the same commit\n+# hash that another run is already being held, that latter run will be\n+# canceled. For more details about the `concurrency` attribute, see:\n+# https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#concurrency\n+concurrency:\n+  group: ${{ github.sha }}\n+\n jobs:\n   ci-config:\n     name: config\n--\n2.42.0.rc2.windows.1\n\n"},{"id":"480956","messageId":"xmqq4jkphhco.fsf@gitster.g","threadId":"59990","inReplyTo":"1da763f3-60bf-a572-2c71-336b1fa5553d@gmx.de","subject":"Re: [PATCH] ci: avoid building from the same commit in parallel","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2023-08-23T16:08:07Z","receivedAt":"2023-08-23T16:08:13Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:\n\n> Hi Junio,\n>\n> On Tue, 22 Aug 2023, Junio C Hamano wrote:\n>\n>> Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:\n>>\n>> > Right, we'd need that `concurrency: ${{ github.sha }}` attribute on\n>> > the `config` job.\n>>\n>> That was my first thought, but I am not sure how it would work.\n>>\n>> Doesn't skip-if-redundant grab the workflow runs that have succeeded\n>> and then see if one for the same commit already exists?  If you used\n>> concurrency on the 'config', what gets serialized between two jobs\n>> for the same commit is only the 'config' phase, so 'master' may wait\n>> starting (because 'config' is what everybody else 'needs' it) while\n>> 'config' phase of 'main' runs, and then when it gets to the turn of\n>> 'config' phase of 'master', it would not find the run for the same\n>> commit being done for 'main' completed yet, would it?\n>\n> Yes, that's true.\n>\n> But there is a silver lining: the `concurrency` can not only be specified\n> on the job level, but also on the workflow run level.\n> I tested this, and present the corresponding patch at the end of this\n> mail.\n\nYeah, serializing the whole thing was the only way I thought that\nwould work, and I am glad you already tested that it works.\n\nThanks.\n"},{"id":"480957","messageId":"xmqqzg2hg2nv.fsf@gitster.g","threadId":"59990","inReplyTo":"1da763f3-60bf-a572-2c71-336b1fa5553d@gmx.de","subject":"Re: [PATCH] ci: avoid building from the same commit in parallel","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2023-08-23T16:10:44Z","receivedAt":"2023-08-23T16:10:56Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:\n\n> Here is the patch:\n>\n> -- snipsnap --\n> From: Junio C Hamano <gitster@pobox.com>\n> Date: Mon, 21 Aug 2023 17:31:26 -0700\n> Subject: [PATCH] ci: avoid building from the same commit in parallel\n\nI forgot to say that I do not think I deserve the credit in the end\nresult, as you've done all the hard part.  \n\nMind taking the authorship, while demoting me to \"Helped-by\" status?\n\nThanks.\n"},{"id":"481028","messageId":"312f2c2c-1411-53c8-dd5f-24ab9097571a@gmx.de","threadId":"59990","inReplyTo":"xmqqzg2hg2nv.fsf@gitster.g","subject":"Re: [PATCH] ci: avoid building from the same commit in parallel","fromName":"Johannes Schindelin","fromEmail":"johannes.schindelin@gmx.de","sentAt":"2023-08-25T12:56:55Z","receivedAt":"2023-08-25T12:58:48Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"Hi Junio,\n\nOn Wed, 23 Aug 2023, Junio C Hamano wrote:\n\n> Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:\n>\n> > Here is the patch:\n> >\n> > -- snipsnap --\n> > From: Junio C Hamano <gitster@pobox.com>\n> > Date: Mon, 21 Aug 2023 17:31:26 -0700\n> > Subject: [PATCH] ci: avoid building from the same commit in parallel\n>\n> I forgot to say that I do not think I deserve the credit in the end\n> result, as you've done all the hard part.\n>\n> Mind taking the authorship, while demoting me to \"Helped-by\" status?\n\nSure. But I disagree that I did most of the work. You tried all the\navenues that I would have tried, saving me tons of time by spending yours.\n\nThank you,\nJohannes\n"},{"id":"481136","messageId":"xmqqcyz5cxy3.fsf@gitster.g","threadId":"59990","inReplyTo":"20230820185009.20095-1-andy.koppe@gmail.com","subject":"Re: [PATCH v5 0/8] pretty: add %(decorate[:<options>]) format","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2023-08-29T21:59:00Z","receivedAt":"2023-08-29T22:00:08Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Andy Koppe <andy.koppe@gmail.com> writes:\n\n> Apologies for sending another version so soon, but I realized that I\n> hadn't removed the use of a compound literal from the first commit where\n> I had added it, so it still appeared in the patches. The overall diff\n> for v5 is the same as for v4.\n\nSorry, but I lost track.  How does this latest round of the topic\nlook to folks?  Ready to go?\n\nThanks.\n"},{"id":"481316","messageId":"e8ff187f-9566-4761-b240-6bdc06c003d8@gmail.com","threadId":"59990","inReplyTo":"xmqqcyz5cxy3.fsf@gitster.g","subject":"Re: [PATCH v5 0/8] pretty: add %(decorate[:<options>]) format","fromName":"Andy Koppe","fromEmail":"andy.koppe@gmail.com","sentAt":"2023-09-01T21:33:03Z","receivedAt":"2023-09-01T21:50:07Z","isPatch":true,"sender":{"key":"andy.koppe@gmail.com","avatar":"https://avatars.githubusercontent.com/u/223411?v=4"},"body":"On 29/08/2023 22:59, Junio C Hamano wrote:\n> Andy Koppe <andy.koppe@gmail.com> writes:\n> \n>> Apologies for sending another version so soon, but I realized that I\n>> hadn't removed the use of a compound literal from the first commit where\n>> I had added it, so it still appeared in the patches. The overall diff\n>> for v5 is the same as for v4.\n> \n> Sorry, but I lost track.  How does this latest round of the topic\n> look to folks?  Ready to go?\n\nUnfortunately there haven't been any comments beyond v2, apart from your \nrequest not to introduce compound literals, which I've addressed.\n\nI haven't got any changes to this pending.\n\nRegards,\nAndy\n"}]}