{"thread":{"id":"56503","subject":"[PATCH v4] pretty: colorize pattern matches in commit messages","startedAt":"2021-09-15T11:00:08Z","lastAt":"2021-09-15T18:04:14Z","messageCount":2,"participants":["Hamza Mahfooz","Junio C Hamano"],"isPatch":true,"patchVersion":4,"patchTotal":null},"messages":[{"id":"435976","messageId":"20210915105943.650308-1-someguy@effective-light.com","threadId":"56503","inReplyTo":null,"subject":"[PATCH v4] pretty: colorize pattern matches in commit messages","fromName":"Hamza Mahfooz","fromEmail":"someguy@effective-light.com","sentAt":"2021-09-15T10:59:43Z","receivedAt":"2021-09-15T11:00:08Z","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\nv4: add strbuf_add_with_color(), use the correct color code scheme in the\n    unit tests and add more unit tests.\n---\n Documentation/git-log.txt |   8 +++\n grep.c                    |  41 +++++++++-----\n grep.h                    |   3 +\n pretty.c                  | 113 ++++++++++++++++++++++++++++++++++----\n t/t4202-log.sh            |  55 +++++++++++++++++++\n 5 files changed, 193 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..51f69f86af 100644\n--- a/pretty.c\n+++ b/pretty.c\n@@ -431,15 +431,77 @@ const char *show_ident_date(const struct ident_split *ident,\n \treturn show_date(date, tz, mode);\n }\n \n+static inline void strbuf_add_with_color(struct strbuf *sb, const char *color,\n+\t\t\t\t\t char *buf, size_t buflen)\n+{\n+\tstrbuf_add(sb, color, strlen(color));\n+\tstrbuf_add(sb, buf, buflen);\n+\tif (strlen(color))\n+\t\tstrbuf_add(sb, GIT_COLOR_RESET, strlen(GIT_COLOR_RESET));\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    (2 * strlen(GIT_COLOR_RESET)));\n+\t\tstrbuf_add_with_color(sb, line_color, buf, match.rm_so);\n+\t\tstrbuf_add_with_color(sb, match_color, buf + match.rm_so,\n+\t\t\t\t      match.rm_eo - match.rm_so);\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) + strlen(GIT_COLOR_RESET));\n+\t\tstrbuf_add_with_color(sb, line_color, buf, eol - buf);\n+\t} else {\n+\t\tstrbuf_add(sb, buf, eol - buf);\n+\t}\n+\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 +558,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 +2010,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 +2029,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 +2046,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 +2059,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 +2086,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 +2109,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..943c00e338 100755\n--- a/t/t4202-log.sh\n+++ b/t/t4202-log.sh\n@@ -449,6 +449,61 @@ test_expect_success !FAIL_PREREQS 'log with various grep.patternType configurati\n \t)\n '\n \n+cat > expect << EOF\n+Author: <BOLD;RED>A U<RESET> Thor <author@example.com>\n+EOF\n+\n+test_expect_success 'log --author' '\n+\tgit log -1 --color=always --author=\"A U\" >log &&\n+\tgrep Author log >actual.raw &&\n+\ttest_decode_color <actual.raw >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+cat > expect << EOF\n+Commit:     C O Mitter <committer@<BOLD;RED>example<RESET>.com>\n+EOF\n+\n+test_expect_success 'log --committer' '\n+\tgit log -1 --color=always --pretty=fuller --committer=\"example\" >log &&\n+\tgrep \"Commit:\" log >actual.raw &&\n+\ttest_decode_color <actual.raw >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+cat > expect << EOF\n+    <BOLD;RED>Sec<RESET>ond\n+    <BOLD;RED>sec<RESET>ond\n+EOF\n+\n+test_expect_success 'log -i --grep with color' '\n+\tgit log --color=always -i --grep=sec >log &&\n+\tgrep -i sec log >actual.raw &&\n+\ttest_decode_color <actual.raw >actual &&\n+\ttest_cmp expect actual\n+'\n+cat > expect << EOF\n+    <GREEN>th<RESET><BOLD;RED>ir<RESET><GREEN>d<RESET>\n+EOF\n+\n+test_expect_success '-c color.grep.selected log --grep' '\n+\tgit -c color.grep.selected=\"green\" log --color=always --grep=ir >log &&\n+\tgrep ir log >actual.raw &&\n+\ttest_decode_color <actual.raw >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+cat > expect << EOF\n+    <BLUE>i<RESET>n<BLUE>i<RESET>t<BLUE>i<RESET>al\n+EOF\n+\n+test_expect_success '-c color.grep.matchSelected log --grep' '\n+\tgit -c color.grep.matchSelected=\"blue\" log --color=always --grep=i >log &&\n+\tgrep al log >actual.raw &&\n+\ttest_decode_color <actual.raw >actual &&\n+\ttest_cmp expect actual\n+'\n+\n cat > expect <<EOF\n * Second\n * sixth\n-- \n2.33.0\n\n"},{"id":"436010","messageId":"xmqq1r5pyaui.fsf@gitster.g","threadId":"56503","inReplyTo":"20210915105943.650308-1-someguy@effective-light.com","subject":"Re: [PATCH v4] pretty: colorize pattern matches in commit messages","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-09-15T18:04:05Z","receivedAt":"2021-09-15T18:04:14Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Hamza Mahfooz <someguy@effective-light.com> writes:\n\n> The \"git log\" command limits its output to the commits that contain strings\n> matched by a pattern when the \"--grep=<pattern>\" option is used, but unlike\n> output from \"git grep -e <pattern>\", the matches are not highlighted,\n> making them harder to spot.\n>\n> Teach the pretty-printer code to highlight matches from the\n> \"--grep=<pattern>\", \"--author=<pattern>\" and \"--committer=<pattern>\"\n> options (to view the last one, you may have to ask for --pretty=fuller).\n>\n> Signed-off-by: Hamza Mahfooz <someguy@effective-light.com>\n> ---\n> v2: 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>\n> v3: fix a formatting issue with the added documentation.\n>\n> v4: add strbuf_add_with_color(), use the correct color code scheme in the\n>     unit tests and add more unit tests.\n\nIt looks to me that it would make it easier to read and understand\nif this patch is done as at least two-patch series, the first one\ndoing a necessary refactoring of next_match() and match_one_pattern()\nas a preliminary enhancement step that touches only grep.[ch], and\nthe second step that utilizes the now-external interface in pretty.c\nto implement the new feature, with tests and docs.\n\nFor example, it is unclear from the above description (including the\nparts that are outside the log message proper) why changes other\nthan the function name and addition of it to grepl.h are necessary\nwhen next_match() is externalized as grep_next_match(); the changes\ncan and should be explained in the proposed log message for the\nfirst \"preliminary enhancement\" step.\n\nAs a feature (read: what we'd describe in the documentation), I\nthink this is quite nicely designed in this iteration.  I would\nhowever like to see some discussion on the performance implications\nin the proposed log message for the second \"adding the feature\"\nstep.  Backing up the change with some concrete numbers (\"because\nessentially we need to run the same 'grep' twice, this slows down\n'git log --author=^H' in our history by N%, but that would be a\nsmall enough and acceptable price\", or something like that) would be\na good idea.\n\nThanks.\n\n> index 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\n> diff --git a/grep.c b/grep.c\n> index 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>  \n> diff --git a/grep.h b/grep.h\n> index 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;\n> diff --git a/pretty.c b/pretty.c\n> index 9631529c10..51f69f86af 100644\n> --- a/pretty.c\n> +++ b/pretty.c\n> @@ -431,15 +431,77 @@ const char *show_ident_date(const struct ident_split *ident,\n>  \treturn show_date(date, tz, mode);\n>  }\n>  \n> +static inline void strbuf_add_with_color(struct strbuf *sb, const char *color,\n> +\t\t\t\t\t char *buf, size_t buflen)\n> +{\n> +\tstrbuf_add(sb, color, strlen(color));\n> +\tstrbuf_add(sb, buf, buflen);\n> +\tif (strlen(color))\n> +\t\tstrbuf_add(sb, GIT_COLOR_RESET, strlen(GIT_COLOR_RESET));\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    (2 * strlen(GIT_COLOR_RESET)));\n> +\t\tstrbuf_add_with_color(sb, line_color, buf, match.rm_so);\n> +\t\tstrbuf_add_with_color(sb, match_color, buf + match.rm_so,\n> +\t\t\t\t      match.rm_eo - match.rm_so);\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) + strlen(GIT_COLOR_RESET));\n> +\t\tstrbuf_add_with_color(sb, line_color, buf, eol - buf);\n> +\t} else {\n> +\t\tstrbuf_add(sb, buf, eol - buf);\n> +\t}\n> +\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 +558,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 +2010,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 +2029,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 +2046,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 +2059,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 +2086,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 +2109,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}\n> diff --git a/t/t4202-log.sh b/t/t4202-log.sh\n> index 9dfead936b..943c00e338 100755\n> --- a/t/t4202-log.sh\n> +++ b/t/t4202-log.sh\n> @@ -449,6 +449,61 @@ test_expect_success !FAIL_PREREQS 'log with various grep.patternType configurati\n>  \t)\n>  '\n>  \n> +cat > expect << EOF\n> +Author: <BOLD;RED>A U<RESET> Thor <author@example.com>\n> +EOF\n> +\n> +test_expect_success 'log --author' '\n> +\tgit log -1 --color=always --author=\"A U\" >log &&\n> +\tgrep Author log >actual.raw &&\n> +\ttest_decode_color <actual.raw >actual &&\n> +\ttest_cmp expect actual\n> +'\n> +\n> +cat > expect << EOF\n> +Commit:     C O Mitter <committer@<BOLD;RED>example<RESET>.com>\n> +EOF\n> +\n> +test_expect_success 'log --committer' '\n> +\tgit log -1 --color=always --pretty=fuller --committer=\"example\" >log &&\n> +\tgrep \"Commit:\" log >actual.raw &&\n> +\ttest_decode_color <actual.raw >actual &&\n> +\ttest_cmp expect actual\n> +'\n> +\n> +cat > expect << EOF\n> +    <BOLD;RED>Sec<RESET>ond\n> +    <BOLD;RED>sec<RESET>ond\n> +EOF\n> +\n> +test_expect_success 'log -i --grep with color' '\n> +\tgit log --color=always -i --grep=sec >log &&\n> +\tgrep -i sec log >actual.raw &&\n> +\ttest_decode_color <actual.raw >actual &&\n> +\ttest_cmp expect actual\n> +'\n> +cat > expect << EOF\n> +    <GREEN>th<RESET><BOLD;RED>ir<RESET><GREEN>d<RESET>\n> +EOF\n> +\n> +test_expect_success '-c color.grep.selected log --grep' '\n> +\tgit -c color.grep.selected=\"green\" log --color=always --grep=ir >log &&\n> +\tgrep ir log >actual.raw &&\n> +\ttest_decode_color <actual.raw >actual &&\n> +\ttest_cmp expect actual\n> +'\n> +\n> +cat > expect << EOF\n> +    <BLUE>i<RESET>n<BLUE>i<RESET>t<BLUE>i<RESET>al\n> +EOF\n> +\n> +test_expect_success '-c color.grep.matchSelected log --grep' '\n> +\tgit -c color.grep.matchSelected=\"blue\" log --color=always --grep=i >log &&\n> +\tgrep al log >actual.raw &&\n> +\ttest_decode_color <actual.raw >actual &&\n> +\ttest_cmp expect actual\n> +'\n> +\n>  cat > expect <<EOF\n>  * Second\n>  * sixth\n"}]}