git-grep: show pathnames relative to the current directory

By default, the command shows pathnames relative to the current
directory.  Use --full-name (the same flag to do so in ls-files)
if you want to see the full pathname relative to the project root.

This makes it very pleasant to run in Emacs compilation (or
"grep-find") buffer.

Signed-off-by: Junio C Hamano <junkio@cox.net>
diff --git a/Documentation/git-grep.txt b/Documentation/git-grep.txt
index dc76833..7545dd9 100644
--- a/Documentation/git-grep.txt
+++ b/Documentation/git-grep.txt
@@ -11,7 +11,7 @@
 [verse]
 'git-grep' [--cached]
 	   [-a | --text] [-I] [-i | --ignore-case] [-w | --word-regexp]
-	   [-v | --invert-match]
+	   [-v | --invert-match] [--full-name]
 	   [-E | --extended-regexp] [-G | --basic-regexp] [-F | --fixed-strings]
 	   [-n] [-l | --files-with-matches] [-L | --files-without-match]
 	   [-c | --count]
@@ -47,6 +47,12 @@
 -v | --invert-match::
 	Select non-matching lines.
 
+--full-name::
+	When run from a subdirectory, the command usually
+	outputs paths relative to the current directory.  This
+	option forces paths to be output relative to the project
+	top directory.
+
 -E | --extended-regexp | -G | --basic-regexp::
 	Use POSIX extended/basic regexp for patterns.  Default
 	is to use basic regexp.
diff --git a/builtin-grep.c b/builtin-grep.c
index 93b7e07..a561612 100644
--- a/builtin-grep.c
+++ b/builtin-grep.c
@@ -123,6 +123,7 @@
 	struct grep_pat *pattern_list;
 	struct grep_pat **pattern_tail;
 	struct grep_expr *pattern_expression;
+	int prefix_length;
 	regex_t regexp;
 	unsigned linenum:1;
 	unsigned invert:1;
@@ -136,6 +137,7 @@
 #define GREP_BINARY_TEXT	2
 	unsigned binary:2;
 	unsigned extended:1;
+	unsigned relative:1;
 	int regflags;
 	unsigned pre_context;
 	unsigned post_context;
@@ -632,19 +634,40 @@
 	return !!last_hit;
 }
 
-static int grep_sha1(struct grep_opt *opt, const unsigned char *sha1, const char *name)
+static int grep_sha1(struct grep_opt *opt, const unsigned char *sha1, const char *name, int tree_name_len)
 {
 	unsigned long size;
 	char *data;
 	char type[20];
+	char *to_free = NULL;
 	int hit;
+
 	data = read_sha1_file(sha1, type, &size);
 	if (!data) {
 		error("'%s': unable to read %s", name, sha1_to_hex(sha1));
 		return 0;
 	}
+	if (opt->relative && opt->prefix_length) {
+		static char name_buf[PATH_MAX];
+		char *cp;
+		int name_len = strlen(name) - opt->prefix_length + 1;
+
+		if (!tree_name_len)
+			name += opt->prefix_length;
+		else {
+			if (ARRAY_SIZE(name_buf) <= name_len)
+				cp = to_free = xmalloc(name_len);
+			else
+				cp = name_buf;
+			memcpy(cp, name, tree_name_len);
+			strcpy(cp + tree_name_len,
+			       name + tree_name_len + opt->prefix_length);
+			name = cp;
+		}
+	}
 	hit = grep_buffer(opt, name, data, size);
 	free(data);
+	free(to_free);
 	return hit;
 }
 
@@ -674,6 +697,8 @@
 		return 0;
 	}
 	close(i);
+	if (opt->relative && opt->prefix_length)
+		filename += opt->prefix_length;
 	i = grep_buffer(opt, filename, data, st.st_size);
 	free(data);
 	return i;
@@ -720,7 +745,7 @@
 	char *argptr = randarg;
 	struct grep_pat *p;
 
-	if (opt->extended)
+	if (opt->extended || (opt->relative && opt->prefix_length))
 		return -1;
 	len = nr = 0;
 	push_arg("grep");
@@ -845,7 +870,7 @@
 		if (!pathspec_matches(paths, ce->name))
 			continue;
 		if (cached)
-			hit |= grep_sha1(opt, ce->sha1, ce->name);
+			hit |= grep_sha1(opt, ce->sha1, ce->name, 0);
 		else
 			hit |= grep_file(opt, ce->name);
 	}
@@ -860,11 +885,12 @@
 	int hit = 0;
 	struct name_entry entry;
 	char *down;
-	char *path_buf = xmalloc(PATH_MAX + strlen(tree_name) + 100);
+	int tn_len = strlen(tree_name);
+	char *path_buf = xmalloc(PATH_MAX + tn_len + 100);
 
-	if (tree_name[0]) {
-		int offset = sprintf(path_buf, "%s:", tree_name);
-		down = path_buf + offset;
+	if (tn_len) {
+		tn_len = sprintf(path_buf, "%s:", tree_name);
+		down = path_buf + tn_len;
 		strcat(down, base);
 	}
 	else {
@@ -886,7 +912,7 @@
 		if (!pathspec_matches(paths, down))
 			;
 		else if (S_ISREG(entry.mode))
-			hit |= grep_sha1(opt, entry.sha1, path_buf);
+			hit |= grep_sha1(opt, entry.sha1, path_buf, tn_len);
 		else if (S_ISDIR(entry.mode)) {
 			char type[20];
 			struct tree_desc sub;
@@ -907,7 +933,7 @@
 		       struct object *obj, const char *name)
 {
 	if (obj->type == OBJ_BLOB)
-		return grep_sha1(opt, obj->sha1, name);
+		return grep_sha1(opt, obj->sha1, name, 0);
 	if (obj->type == OBJ_COMMIT || obj->type == OBJ_TREE) {
 		struct tree_desc tree;
 		void *data;
@@ -945,6 +971,8 @@
 	int i;
 
 	memset(&opt, 0, sizeof(opt));
+	opt.prefix_length = (prefix && *prefix) ? strlen(prefix) : 0;
+	opt.relative = 1;
 	opt.pattern_tail = &opt.pattern_list;
 	opt.regflags = REG_NEWLINE;
 
@@ -1118,6 +1146,10 @@
 			}
 			die(emsg_missing_argument, arg);
 		}
+		if (!strcmp("--full-name", arg)) {
+			opt.relative = 0;
+			continue;
+		}
 		if (!strcmp("--", arg)) {
 			/* later processing wants to have this at argv[1] */
 			argv--;
@@ -1176,8 +1208,15 @@
 			verify_filename(prefix, argv[j]);
 	}
 
-	if (i < argc)
+	if (i < argc) {
 		paths = get_pathspec(prefix, argv + i);
+		if (opt.prefix_length && opt.relative) {
+			/* Make sure we do not get outside of paths */
+			for (i = 0; paths[i]; i++)
+				if (strncmp(prefix, paths[i], opt.prefix_length))
+					die("git-grep: cannot generate relative filenames containing '..'");
+		}
+	}
 	else if (prefix) {
 		paths = xcalloc(2, sizeof(const char *));
 		paths[0] = prefix;
diff --git a/t/t7002-grep.sh b/t/t7002-grep.sh
index 00a7d76..6bfb899 100755
--- a/t/t7002-grep.sh
+++ b/t/t7002-grep.sh
@@ -3,7 +3,7 @@
 # Copyright (c) 2006 Junio C Hamano
 #
 
-test_description='git grep -w
+test_description='git grep various.
 '
 
 . ./test-lib.sh
@@ -19,7 +19,9 @@
 	echo x x xx x >x &&
 	echo y yy >y &&
 	echo zzz > z &&
-	git add file x y z &&
+	mkdir t &&
+	echo test >t/t &&
+	git add file x y z t/t &&
 	git commit -m initial
 '
 
@@ -80,6 +82,31 @@
 			diff expected actual
 		fi
 	'
+
+	test_expect_success "grep $L (t-1)" '
+		echo "${HC}t/t:1:test" >expected &&
+		git grep -n -e test $H >actual &&
+		diff expected actual
+	'
+
+	test_expect_success "grep $L (t-2)" '
+		echo "${HC}t:1:test" >expected &&
+		(
+			cd t &&
+			git grep -n -e test $H
+		) >actual &&
+		diff expected actual
+	'
+
+	test_expect_success "grep $L (t-3)" '
+		echo "${HC}t/t:1:test" >expected &&
+		(
+			cd t &&
+			git grep --full-name -n -e test $H
+		) >actual &&
+		diff expected actual
+	'
+
 done
 
 test_done