{"thread":{"id":"56448","subject":"[PATCH v3] pretty: colorize pattern matches in commit messages","startedAt":"2021-09-06T00:45:17Z","lastAt":"2021-09-06T00:45:17Z","messageCount":1,"participants":["Hamza Mahfooz"],"isPatch":true,"patchVersion":3,"patchTotal":null},"messages":[{"id":"434746","messageId":"20210906002842.10995-1-someguy@effective-light.com","threadId":"56448","inReplyTo":null,"subject":"[PATCH v3] pretty: colorize pattern matches in commit messages","fromName":"Hamza Mahfooz","fromEmail":"someguy@effective-light.com","sentAt":"2021-09-06T00:28:42Z","receivedAt":"2021-09-06T00:45:17Z","isPatch":true,"sender":{"key":"someguy@effective-light.com","avatar":"https://avatars.githubusercontent.com/u/3885502?v=4"},"body":"The \"git log\" command limits its output to the commits that contain strings\nmatched by a pattern when the \"--grep=<pattern>\" option is used, but unlike\noutput from \"git grep -e <pattern>\", the matches are not highlighted,\nmaking them harder to spot.\n\nTeach the pretty-printer code to highlight matches from the\n\"--grep=<pattern>\", \"--author=<pattern>\" and \"--committer=<pattern>\"\noptions (to view the last one, you may have to ask for --pretty=fuller).\n\nSigned-off-by: Hamza Mahfooz <someguy@effective-light.com>\n---\nv2: make the commit message whole (add the missing ingredients), rename\n    append_matched_line() to append_line_with_color(), use\n    colors[GREP_COLOR_MATCH_SELECTED] instead of\n    colors[GREP_COLOR_MATCH_CONTEXT], allow the background color to be\n    customized, don't copy strings to a buffer when not coloring in\n    append_line_with_color(), rename next_match() to grep_next_match(),\n    repurpose grep_next_match()/match_one_pattern() for use in\n    append_line_with_color() (allowing us to remove duplicated matching\n    code in append_line_with_color()), document how to customize the\n    feature and modify some of the tests to fit the feature better.\n\nv3: fix a formatting issue with the added documentation.\n---\n Documentation/git-log.txt |   8 +++\n grep.c                    |  41 +++++++++------\n grep.h                    |   3 ++\n pretty.c                  | 107 +++++++++++++++++++++++++++++++++-----\n t/t4202-log.sh            |  42 +++++++++++++++\n 5 files changed, 174 insertions(+), 27 deletions(-)\n\ndiff --git a/Documentation/git-log.txt b/Documentation/git-log.txt\nindex 0498e7bacb..c689f7d235 100644\n--- a/Documentation/git-log.txt\n+++ b/Documentation/git-log.txt\n@@ -241,6 +241,14 @@ This setting can be disabled by the `--no-notes` option,\n overridden by the `GIT_NOTES_DISPLAY_REF` environment variable,\n and overridden by the `--notes=<ref>` option.\n \n+color.grep.selected::\n+\tDetermines the non matching text (background) color of selected lines,\n+\twhen `--grep`, `--author` or `--committer` are used.\n+\n+color.grep.matchSelected::\n+\tDetermines the matching text (foreground) color of selected lines, when\n+\t`--grep`, `--author` or `--committer` are used.\n+\n GIT\n ---\n Part of the linkgit:git[1] suite\ndiff --git a/grep.c b/grep.c\nindex 424a39591b..6b036ee18a 100644\n--- a/grep.c\n+++ b/grep.c\n@@ -956,18 +956,23 @@ static int match_one_pattern(struct grep_pat *p, char *bol, char *eol,\n \tconst char *start = bol;\n \n \tif ((p->token != GREP_PATTERN) &&\n-\t    ((p->token == GREP_PATTERN_HEAD) != (ctx == GREP_CONTEXT_HEAD)))\n+\t    ((p->token == GREP_PATTERN_HEAD) != (ctx == GREP_CONTEXT_HEAD)) &&\n+\t    ((p->token == GREP_PATTERN_BODY) != (ctx == GREP_CONTEXT_BODY)))\n \t\treturn 0;\n \n \tif (p->token == GREP_PATTERN_HEAD) {\n-\t\tconst char *field;\n-\t\tsize_t len;\n-\t\tassert(p->field < ARRAY_SIZE(header_field));\n-\t\tfield = header_field[p->field].field;\n-\t\tlen = header_field[p->field].len;\n-\t\tif (strncmp(bol, field, len))\n-\t\t\treturn 0;\n-\t\tbol += len;\n+\t\tif (!(eflags & REG_NOTBOL)) {\n+\t\t\tconst char *field;\n+\t\t\tsize_t len;\n+\n+\t\t\tassert(p->field < ARRAY_SIZE(header_field));\n+\t\t\tfield = header_field[p->field].field;\n+\t\t\tlen = header_field[p->field].len;\n+\t\t\tif (strncmp(bol, field, len))\n+\t\t\t\treturn 0;\n+\t\t\tbol += len;\n+\t\t}\n+\n \t\tswitch (p->field) {\n \t\tcase GREP_HEADER_AUTHOR:\n \t\tcase GREP_HEADER_COMMITTER:\n@@ -1159,21 +1164,26 @@ static int match_next_pattern(struct grep_pat *p, char *bol, char *eol,\n \treturn 1;\n }\n \n-static int next_match(struct grep_opt *opt, char *bol, char *eol,\n-\t\t      enum grep_context ctx, regmatch_t *pmatch, int eflags)\n+int grep_next_match(struct grep_opt *opt, char *bol, char *eol,\n+\t\t    enum grep_context ctx, regmatch_t *pmatch,\n+\t\t    enum grep_header_field field, int eflags)\n {\n \tstruct grep_pat *p;\n \tint hit = 0;\n \n \tpmatch->rm_so = pmatch->rm_eo = -1;\n \tif (bol < eol) {\n-\t\tfor (p = opt->pattern_list; p; p = p->next) {\n+\t\tfor (p = ((ctx == GREP_CONTEXT_HEAD)\n+\t\t\t   ? opt->header_list : opt->pattern_list);\n+\t\t\t  p; p = p->next) {\n \t\t\tswitch (p->token) {\n \t\t\tcase GREP_PATTERN: /* atom */\n \t\t\tcase GREP_PATTERN_HEAD:\n \t\t\tcase GREP_PATTERN_BODY:\n-\t\t\t\thit |= match_next_pattern(p, bol, eol, ctx,\n-\t\t\t\t\t\t\t  pmatch, eflags);\n+\t\t\t\tif ((field == GREP_HEADER_FIELD_MAX) || (p->field == field))\n+\t\t\t\t\thit |= match_next_pattern(p, bol, eol,\n+\t\t\t\t\t\t\t\t  ctx, pmatch,\n+\t\t\t\t\t\t\t\t  eflags);\n \t\t\t\tbreak;\n \t\t\tdefault:\n \t\t\t\tbreak;\n@@ -1262,7 +1272,8 @@ static void show_line(struct grep_opt *opt, char *bol, char *eol,\n \t\t\t\tline_color = opt->colors[GREP_COLOR_FUNCTION];\n \t\t}\n \t\t*eol = '\\0';\n-\t\twhile (next_match(opt, bol, eol, ctx, &match, eflags)) {\n+\t\twhile (grep_next_match(opt, bol, eol, ctx, &match,\n+\t\t\t\t       GREP_HEADER_FIELD_MAX, eflags)) {\n \t\t\tif (match.rm_so == match.rm_eo)\n \t\t\t\tbreak;\n \ndiff --git a/grep.h b/grep.h\nindex 72f82b1e30..d2943e29ea 100644\n--- a/grep.h\n+++ b/grep.h\n@@ -177,6 +177,9 @@ void append_header_grep_pattern(struct grep_opt *, enum grep_header_field, const\n void compile_grep_patterns(struct grep_opt *opt);\n void free_grep_patterns(struct grep_opt *opt);\n int grep_buffer(struct grep_opt *opt, char *buf, unsigned long size);\n+int grep_next_match(struct grep_opt *opt, char *bol, char *eol,\n+\t\t    enum grep_context ctx, regmatch_t *pmatch,\n+\t\t    enum grep_header_field field, int eflags);\n \n struct grep_source {\n \tchar *name;\ndiff --git a/pretty.c b/pretty.c\nindex 9631529c10..65cb63e725 100644\n--- a/pretty.c\n+++ b/pretty.c\n@@ -431,15 +431,71 @@ const char *show_ident_date(const struct ident_split *ident,\n \treturn show_date(date, tz, mode);\n }\n \n+static void append_line_with_color(struct strbuf *sb, struct grep_opt *opt,\n+\t\t\t\t   const char *line, size_t linelen,\n+\t\t\t\t   int color, enum grep_context ctx,\n+\t\t\t\t   enum grep_header_field field)\n+{\n+\tchar *buf, *eol;\n+\tconst char *line_color, *match_color;\n+\tregmatch_t match;\n+\tstruct strbuf tmp_sb;\n+\tint eflags = 0;\n+\n+\tif (!opt || !want_color(color) || opt->invert) {\n+\t\tstrbuf_add(sb, line, linelen);\n+\t\treturn;\n+\t}\n+\n+\tif (ctx == GREP_CONTEXT_HEAD)\n+\t\teflags = REG_NOTBOL;\n+\n+\tstrbuf_init(&tmp_sb, linelen + 1);\n+\tstrbuf_add(&tmp_sb, line, linelen);\n+\n+\tbuf = tmp_sb.buf;\n+\teol = buf + linelen;\n+\tline_color = opt->colors[GREP_COLOR_SELECTED];\n+\tmatch_color = opt->colors[GREP_COLOR_MATCH_SELECTED];\n+\n+\twhile (grep_next_match(opt, buf, eol, ctx, &match, field, eflags)) {\n+\t\tif (match.rm_so == match.rm_eo)\n+\t\t\tbreak;\n+\n+\t\tstrbuf_grow(sb, strlen(line_color) + strlen(match_color) +\n+\t\t\t    strlen(GIT_COLOR_RESET));\n+\t\tstrbuf_add(sb, line_color, strlen(line_color));\n+\t\tstrbuf_add(sb, buf, match.rm_so);\n+\t\tstrbuf_add(sb, match_color, strlen(match_color));\n+\t\tstrbuf_add(sb, buf + match.rm_so,\n+\t\t\t   match.rm_eo - match.rm_so);\n+\t\tstrbuf_add(sb, GIT_COLOR_RESET,\n+\t\t\t   strlen(GIT_COLOR_RESET));\n+\t\tbuf += match.rm_eo;\n+\t\teflags = REG_NOTBOL;\n+\t}\n+\n+\tif (buf != line) {\n+\t\tstrbuf_grow(sb, strlen(line_color));\n+\t\tstrbuf_add(sb, line_color, strlen(line_color));\n+\t}\n+\n+\tstrbuf_add(sb, buf, eol - buf);\n+\tstrbuf_release(&tmp_sb);\n+}\n+\n void pp_user_info(struct pretty_print_context *pp,\n \t\t  const char *what, struct strbuf *sb,\n \t\t  const char *line, const char *encoding)\n {\n+\tstruct strbuf id;\n \tstruct ident_split ident;\n \tchar *line_end;\n \tconst char *mailbuf, *namebuf;\n \tsize_t namelen, maillen;\n \tint max_length = 78; /* per rfc2822 */\n+\tenum grep_header_field field = GREP_HEADER_FIELD_MAX;\n+\tstruct grep_opt *opt = pp->rev ? &pp->rev->grep_filter : NULL;\n \n \tif (pp->fmt == CMIT_FMT_ONELINE)\n \t\treturn;\n@@ -496,9 +552,22 @@ void pp_user_info(struct pretty_print_context *pp,\n \t\t\tstrbuf_addch(sb, '\\n');\n \t\tstrbuf_addf(sb, \" <%.*s>\\n\", (int)maillen, mailbuf);\n \t} else {\n-\t\tstrbuf_addf(sb, \"%s: %.*s%.*s <%.*s>\\n\", what,\n-\t\t\t    (pp->fmt == CMIT_FMT_FULLER) ? 4 : 0, \"    \",\n-\t\t\t    (int)namelen, namebuf, (int)maillen, mailbuf);\n+\t\tstrbuf_init(&id, namelen + maillen + 4);\n+\n+\t\tif (!strcmp(what, \"Author\"))\n+\t\t\tfield = GREP_HEADER_AUTHOR;\n+\t\telse if (!strcmp(what, \"Commit\"))\n+\t\t\tfield = GREP_HEADER_COMMITTER;\n+\n+\t\tstrbuf_addf(sb, \"%s: %.*s\", what,\n+\t\t\t    (pp->fmt == CMIT_FMT_FULLER) ? 4 : 0, \"    \");\n+\t\tstrbuf_addf(&id, \"%.*s <%.*s>\", (int)namelen, namebuf,\n+\t\t\t    (int)maillen, mailbuf);\n+\n+\t\tappend_line_with_color(sb, opt, id.buf, id.len, pp->color,\n+\t\t\t\t       GREP_CONTEXT_HEAD, field);\n+\t\tstrbuf_addch(sb, '\\n');\n+\t\tstrbuf_release(&id);\n \t}\n \n \tswitch (pp->fmt) {\n@@ -1935,8 +2004,9 @@ static int pp_utf8_width(const char *start, const char *end)\n \treturn width;\n }\n \n-static void strbuf_add_tabexpand(struct strbuf *sb, int tabwidth,\n-\t\t\t\t const char *line, int linelen)\n+static void strbuf_add_tabexpand(struct strbuf *sb, struct grep_opt *opt,\n+\t\t\t\t int color, int tabwidth, const char *line,\n+\t\t\t\t int linelen)\n {\n \tconst char *tab;\n \n@@ -1953,7 +2023,9 @@ static void strbuf_add_tabexpand(struct strbuf *sb, int tabwidth,\n \t\t\tbreak;\n \n \t\t/* Output the data .. */\n-\t\tstrbuf_add(sb, line, tab - line);\n+\t\tappend_line_with_color(sb, opt, line, tab - line, color,\n+\t\t\t\t       GREP_CONTEXT_BODY,\n+\t\t\t\t       GREP_HEADER_FIELD_MAX);\n \n \t\t/* .. and the de-tabified tab */\n \t\tstrbuf_addchars(sb, ' ', tabwidth - (width % tabwidth));\n@@ -1968,7 +2040,8 @@ static void strbuf_add_tabexpand(struct strbuf *sb, int tabwidth,\n \t * worrying about width - there's nothing more to\n \t * align.\n \t */\n-\tstrbuf_add(sb, line, linelen);\n+\tappend_line_with_color(sb, opt, line, linelen, color, GREP_CONTEXT_BODY,\n+\t\t\t       GREP_HEADER_FIELD_MAX);\n }\n \n /*\n@@ -1980,11 +2053,16 @@ static void pp_handle_indent(struct pretty_print_context *pp,\n \t\t\t     struct strbuf *sb, int indent,\n \t\t\t     const char *line, int linelen)\n {\n+\tstruct grep_opt *opt = pp->rev ? &pp->rev->grep_filter : NULL;\n+\n \tstrbuf_addchars(sb, ' ', indent);\n \tif (pp->expand_tabs_in_log)\n-\t\tstrbuf_add_tabexpand(sb, pp->expand_tabs_in_log, line, linelen);\n+\t\tstrbuf_add_tabexpand(sb, opt, pp->color, pp->expand_tabs_in_log,\n+\t\t\t\t     line, linelen);\n \telse\n-\t\tstrbuf_add(sb, line, linelen);\n+\t\tappend_line_with_color(sb, opt, line, linelen, pp->color,\n+\t\t\t\t       GREP_CONTEXT_BODY,\n+\t\t\t\t       GREP_HEADER_FIELD_MAX);\n }\n \n static int is_mboxrd_from(const char *line, int len)\n@@ -2002,7 +2080,9 @@ void pp_remainder(struct pretty_print_context *pp,\n \t\t  struct strbuf *sb,\n \t\t  int indent)\n {\n+\tstruct grep_opt *opt = pp->rev ? &pp->rev->grep_filter : NULL;\n \tint first = 1;\n+\n \tfor (;;) {\n \t\tconst char *line = *msg_p;\n \t\tint linelen = get_one_line(line);\n@@ -2023,14 +2103,17 @@ void pp_remainder(struct pretty_print_context *pp,\n \t\tif (indent)\n \t\t\tpp_handle_indent(pp, sb, indent, line, linelen);\n \t\telse if (pp->expand_tabs_in_log)\n-\t\t\tstrbuf_add_tabexpand(sb, pp->expand_tabs_in_log,\n-\t\t\t\t\t     line, linelen);\n+\t\t\tstrbuf_add_tabexpand(sb, opt, pp->color,\n+\t\t\t\t\t     pp->expand_tabs_in_log, line,\n+\t\t\t\t\t     linelen);\n \t\telse {\n \t\t\tif (pp->fmt == CMIT_FMT_MBOXRD &&\n \t\t\t\t\tis_mboxrd_from(line, linelen))\n \t\t\t\tstrbuf_addch(sb, '>');\n \n-\t\t\tstrbuf_add(sb, line, linelen);\n+\t\t\tappend_line_with_color(sb, opt, line, linelen,\n+\t\t\t\t\t       pp->color, GREP_CONTEXT_BODY,\n+\t\t\t\t\t       GREP_HEADER_FIELD_MAX);\n \t\t}\n \t\tstrbuf_addch(sb, '\\n');\n \t}\ndiff --git a/t/t4202-log.sh b/t/t4202-log.sh\nindex 9dfead936b..72138fa7a0 100755\n--- a/t/t4202-log.sh\n+++ b/t/t4202-log.sh\n@@ -449,6 +449,48 @@ test_expect_success !FAIL_PREREQS 'log with various grep.patternType configurati\n \t)\n '\n \n+cat > expect << EOF\n+Author: \u001b[1;31mA U\u001b[m Thor <author@example.com>\n+Author: \u001b[1;31mA U\u001b[m Thor <author@example.com>\n+Author: \u001b[1;31mA U\u001b[m Thor <author@example.com>\n+Author: \u001b[1;31mA U\u001b[m Thor <author@example.com>\n+Author: \u001b[1;31mA U\u001b[m Thor <author@example.com>\n+Author: \u001b[1;31mA U\u001b[m Thor <author@example.com>\n+Author: \u001b[1;31mA U\u001b[m Thor <author@example.com>\n+EOF\n+\n+test_expect_success 'log --author' '\n+\tgit log --color=always --author=\"A U\" >log &&\n+\tgrep Author log >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+cat > expect << EOF\n+Commit:     C O \u001b[1;31mMitter\u001b[m <committer@example.com>\n+Commit:     C O \u001b[1;31mMitter\u001b[m <committer@example.com>\n+Commit:     C O \u001b[1;31mMitter\u001b[m <committer@example.com>\n+Commit:     C O \u001b[1;31mMitter\u001b[m <committer@example.com>\n+Commit:     C O \u001b[1;31mMitter\u001b[m <committer@example.com>\n+Commit:     C O \u001b[1;31mMitter\u001b[m <committer@example.com>\n+Commit:     C O \u001b[1;31mMitter\u001b[m <committer@example.com>\n+EOF\n+\n+test_expect_success 'log --committer' '\n+\tgit log --color=always --pretty=fuller --committer=\"Mitter\" >log &&\n+\tgrep \"Commit:\" log >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+cat > expect << EOF\n+    \u001b[1;31msec\u001b[mond\n+EOF\n+\n+test_expect_success 'log --grep color' '\n+\tgit log --color=always --grep=sec >log &&\n+\tgrep sec log >actual &&\n+\ttest_cmp expect actual\n+'\n+\n cat > expect <<EOF\n * Second\n * sixth\n-- \n2.33.0\n\n"}]}