Subject: [PATCH 4/4] grep: add --hunk-heading option

From: Mark Lodato <[email-masked]>
Date: Sun, 25 Mar 2012 22:41:45 -0400

The purpose of this option is to make the output of grep when showing
context (i.e., -A, -B, -C, or -W) easier to read.  By default, grep
prints a simple separator ("--") between each hunk and then prefixes
each line within the hunk with the same filename and optionally an
incrementing line number.  This repeated information is redundant, so
the new --hunk-heading option moves it all to the hunk separator line.
The idea is similar to the hunk context line of diff.

The new option can be combined with --heading to provide both the
filename and line number of each hunk with minimal visual clutter.

The new configuration, grep.hunkHeading, can be used to set this option
by default.

Signed-off-by: Mark Lodato <[email-masked]>
---
 Documentation/config.txt     |    3 +
 Documentation/git-grep.txt   |   10 ++-
 builtin/grep.c               |   10 ++-
 grep.c                       |   28 ++++++-
 grep.h                       |    1 +
 t/t7812-grep-hunk-heading.sh |  181 ++++++++++++++++++++++++++++++++++++++++++
 6 files changed, 227 insertions(+), 6 deletions(-)
 create mode 100755 t/t7812-grep-hunk-heading.sh

diff --git a/Documentation/config.txt b/Documentation/config.txt
index c081657..ade9503 100644
--- a/Documentation/config.txt
+++ b/Documentation/config.txt
@@ -1140,6 +1140,9 @@ grep.lineNumber::
 grep.extendedRegexp::
 	If set to true, enable '--extended-regexp' option by default.
 
+grep.hunkHeading::
+	If set to true, enable '--hunk-heading' option by default.
+
 gpg.program::
 	Use this custom program instead of "gpg" found on $PATH when
 	making or verifying a PGP signature. The program must support the
diff --git a/Documentation/git-grep.txt b/Documentation/git-grep.txt
index 343eadd..26c085b 100644
--- a/Documentation/git-grep.txt
+++ b/Documentation/git-grep.txt
@@ -20,7 +20,7 @@ SYNOPSIS
 	   [-c | --count] [--all-match] [-q | --quiet]
 	   [--max-depth <depth>]
 	   [--color[=<when>] | --no-color]
-	   [--break] [--heading] [-p | --show-function]
+	   [--break] [--heading] [--hunk-heading] [-p | --show-function]
 	   [-A <post-context>] [-B <pre-context>] [-C <context>]
 	   [-W | --function-context]
 	   [-f <file>] [-e] <pattern>
@@ -43,6 +43,9 @@ grep.lineNumber::
 grep.extendedRegexp::
 	If set to true, enable '--extended-regexp' option by default.
 
+grep.hunkHeading::
+	If set to true, enable '--hunk-heading' option by default.
+
 
 OPTIONS
 -------
@@ -173,6 +176,11 @@ OPTIONS
 	Show the filename above the matches in that file instead of
 	at the start of each shown line.
 
+--hunk-heading::
+	Append the filename (if not `--heading` and not `-h`) and line number
+	(if not `-n`) to each hunk separator, and suppress printing of the
+	filename at the start of each shown line.
+
 -p::
 --show-function::
 	Show the preceding line that contains the function name of
diff --git a/builtin/grep.c b/builtin/grep.c
index 643938d..cdafc5a 100644
--- a/builtin/grep.c
+++ b/builtin/grep.c
@@ -276,6 +276,11 @@ static int grep_config(const char *var, const char *value, void *cb)
 		return 0;
 	}
 
+	if (!strcmp(var, "grep.hunkheading")) {
+		opt->hunk_heading = git_config_bool(var, value);
+		return 0;
+	}
+
 	if (!strcmp(var, "grep.linenumber")) {
 		opt->linenum = git_config_bool(var, value);
 		return 0;
@@ -740,6 +745,8 @@ int cmd_grep(int argc, const char **argv, const char *prefix)
 			"print empty line between matches from different files"),
 		OPT_BOOLEAN(0, "heading", &opt.heading,
 			"show filename only once above matches from same file"),
+		OPT_BOOLEAN(0, "hunk-heading", &opt.hunk_heading,
+			"show filename and line number after hunk separator"),
 		OPT_GROUP(""),
 		OPT_CALLBACK('C', "context", &opt, "n",
 			"show <n> context lines before and after matches",
@@ -920,8 +927,9 @@ int cmd_grep(int argc, const char **argv, const char *prefix)
 #ifndef NO_PTHREADS
 	if (use_threads) {
 		if (!(opt.name_only || opt.unmatch_name_only || opt.count)
+		    && !(opt.hunk_heading && !opt.heading && !opt.file_break)
 		    && (opt.pre_context || opt.post_context ||
-			opt.file_break || opt.funcbody))
+			opt.file_break || opt.funcbody || opt.hunk_heading))
 			skip_first_line = 1;
 		start_threads(&opt);
 	}
diff --git a/grep.c b/grep.c
index 14e0480..f0e00f7 100644
--- a/grep.c
+++ b/grep.c
@@ -747,21 +747,41 @@ static void show_line(struct grep_opt *opt, char *bol, char *eol,
 		if (opt->heading && !opt->file_break)
 			output_color(opt, "--", 2, opt->color_sep);
 		opt->output(opt, "\n", 1);
-		show_hunk = 0;
+		if (!opt->hunk_heading)
+			show_hunk = 0;
 	}
 	if (opt->heading && opt->last_shown == 0) {
 		output_color(opt, name, strlen(name), opt->color_filename);
 		opt->output(opt, "\n", 1);
 	}
 	if (show_hunk &&
-	    ((opt->last_shown == 0 && opt->show_hunk_mark) ||
+	    ((opt->last_shown == 0 && (opt->show_hunk_mark || opt->hunk_heading)) ||
 	     (opt->last_shown != 0 && lno > opt->last_shown + 1))) {
 		output_color(opt, "--", 2, opt->color_sep);
+		if (opt->hunk_heading &&
+		    ((!opt->heading && opt->pathname) || !opt->linenum)) {
+			opt->output(opt, " ", 1);
+			if (!opt->heading && opt->pathname) {
+				output_color(opt, name, strlen(name),
+					     opt->color_filename);
+				if (!opt->linenum)
+					output_sep(opt, ':');
+			}
+			if (!opt->linenum) {
+				char buf[32];
+				snprintf(buf, sizeof(buf), "%d", lno);
+				output_color(opt, buf, strlen(buf),
+					     opt->color_lineno);
+			}
+			opt->output(opt, " ", 1);
+			output_color(opt, "--", 2, opt->color_sep);
+		}
 		opt->output(opt, "\n", 1);
 	}
 	opt->last_shown = lno;
 
-	if (!opt->heading && opt->pathname) {
+	if (opt->pathname && !opt->heading &&
+	    !(opt->hunk_heading && show_hunk)) {
 		output_color(opt, name, strlen(name), opt->color_filename);
 		output_sep(opt, sign);
 	}
@@ -1001,7 +1021,7 @@ static int grep_source_1(struct grep_opt *opt, struct grep_source *gs, int colle
 		opt->output = std_output;
 
 	if (opt->pre_context || opt->post_context || opt->file_break ||
-	    opt->funcbody) {
+	    opt->funcbody || opt->hunk_heading) {
 		/* Show hunk marks, except for the first file. */
 		if (opt->last_shown)
 			opt->show_hunk_mark = 1;
diff --git a/grep.h b/grep.h
index 36e49d8..761db2a 100644
--- a/grep.h
+++ b/grep.h
@@ -117,6 +117,7 @@ struct grep_opt {
 	int show_hunk_mark;
 	int file_break;
 	int heading;
+	int hunk_heading;
 	void *priv;
 
 	void (*output)(struct grep_opt *opt, const void *data, size_t size);
diff --git a/t/t7812-grep-hunk-heading.sh b/t/t7812-grep-hunk-heading.sh
new file mode 100755
index 0000000..6db9ba6
--- /dev/null
+++ b/t/t7812-grep-hunk-heading.sh
@@ -0,0 +1,181 @@
+#!/bin/sh
+#
+# Copyright (c) 2012 Mark Lodato
+#
+
+test_description='git grep --hunk-heading'
+
+. ./test-lib.sh
+
+cat >one.c <<EOF
+#include <stdio.h>
+int main(int argc, const char **argv)
+{
+	printf("Hello world.\n");
+	return 0;
+}
+EOF
+
+cat >two.c <<EOF
+int hello_int(int x)
+{
+	printf("Hello, %d.\n", x);
+	return 0;
+}
+
+int hello_str(const char *s)
+{
+	printf("Hello, %s.\n", s);
+	return 0;
+}
+EOF
+
+test_expect_success 'setup' '
+	git add . &&
+	git commit -m initial
+'
+
+cat >expected <<EOF
+one.c:	printf("Hello world.\n");
+two.c:	printf("Hello, %d.\n", x);
+two.c:	printf("Hello, %s.\n", s);
+EOF
+
+test_expect_success 'grep --hunk-heading without context' '
+	git grep -e Hello --hunk-heading >actual &&
+	test_cmp expected actual
+'
+
+cat >expected <<EOF
+-- one.c:4 --
+	printf("Hello world.\n");
+	return 0;
+-- two.c:3 --
+	printf("Hello, %d.\n", x);
+	return 0;
+-- two.c:9 --
+	printf("Hello, %s.\n", s);
+	return 0;
+EOF
+
+test_expect_success 'grep --hunk-heading -A1' '
+	git grep -e Hello -A1 --hunk-heading >actual &&
+	test_cmp expected actual
+'
+
+test_expect_success 'grep --hunk-heading -B1' '
+	git grep -e return -B1 --hunk-heading >actual &&
+	test_cmp expected actual
+'
+
+cat >expected <<EOF
+<RED>--<RESET> <BLUE>one.c<RESET><RED>:<RESET><GREEN>4<RESET> <RED>--<RESET>
+	printf("Hello world.\n");
+	return 0;
+<RED>--<RESET> <BLUE>two.c<RESET><RED>:<RESET><GREEN>3<RESET> <RED>--<RESET>
+	printf("Hello, %d.\n", x);
+	return 0;
+<RED>--<RESET> <BLUE>two.c<RESET><RED>:<RESET><GREEN>9<RESET> <RED>--<RESET>
+	printf("Hello, %s.\n", s);
+	return 0;
+EOF
+
+test_expect_success 'grep --hunk-heading --color' '
+	test_config color.grep.context		normal &&
+	test_config color.grep.filename		"blue" &&
+	test_config color.grep.function		normal &&
+	test_config color.grep.linenumber	"green" &&
+	test_config color.grep.match		normal &&
+	test_config color.grep.selected		normal &&
+	test_config color.grep.separator	"red" &&
+
+	git grep -e Hello -A1 --hunk-heading --color |
+	test_decode_color >actual &&
+	test_cmp expected actual
+'
+
+cat >expected <<EOF
+-- one.c:4 --
+	printf("Hello world.\n");
+	return 0;
+
+-- two.c:3 --
+	printf("Hello, %d.\n", x);
+	return 0;
+-- two.c:9 --
+	printf("Hello, %s.\n", s);
+	return 0;
+EOF
+
+test_expect_success 'grep --hunk-heading --break' '
+	git grep -e Hello -A1 --hunk-heading --break >actual &&
+	test_cmp expected actual
+'
+
+cat >expected <<EOF
+one.c
+-- 4 --
+	printf("Hello world.\n");
+	return 0;
+--
+two.c
+-- 3 --
+	printf("Hello, %d.\n", x);
+	return 0;
+-- 9 --
+	printf("Hello, %s.\n", s);
+	return 0;
+EOF
+
+test_expect_success 'grep --hunk-heading --heading' '
+	git grep -e Hello -A1 --hunk-heading --heading >actual &&
+	test_cmp expected actual
+'
+
+mv expected expected.old
+sed -e 's/^--$//' expected.old > expected
+rm expected.old
+
+test_expect_success 'grep --hunk-heading --heading --break' '
+	git grep -e Hello -A1 --hunk-heading --heading --break >actual &&
+	test_cmp expected actual
+'
+
+cat >expected <<EOF
+-- one.c --
+4:	printf("Hello world.\n");
+5-	return 0;
+-- two.c --
+3:	printf("Hello, %d.\n", x);
+4-	return 0;
+-- two.c --
+9:	printf("Hello, %s.\n", s);
+10-	return 0;
+EOF
+
+test_expect_success 'grep --hunk-heading -n' '
+	git grep -e Hello -A1 --hunk-heading -n >actual &&
+	test_cmp expected actual
+'
+
+cat >expected <<EOF
+one.c
+--
+4:	printf("Hello world.\n");
+5-	return 0;
+--
+two.c
+--
+3:	printf("Hello, %d.\n", x);
+4-	return 0;
+--
+9:	printf("Hello, %s.\n", s);
+10-	return 0;
+EOF
+
+test_expect_success 'grep --hunk-heading --heading -n' '
+	git grep -e Hello -A1 --hunk-heading --heading -n >actual &&
+	test_cmp expected actual
+'
+
+test_done
-- 
1.7.9.2