Merge branch 'rr/for-each-ref-decoration'
Add a few formatting directives to "git for-each-ref --format=...", to paint them in color, etc. * rr/for-each-ref-decoration: for-each-ref: avoid color leakage for-each-ref: introduce %(color:...) for color for-each-ref: introduce %(upstream:track[short]) for-each-ref: introduce %(HEAD) asterisk marker t6300 (for-each-ref): don't hardcode SHA-1 hexes t6300 (for-each-ref): clearly demarcate setup
This commit is contained in:
		| @ -91,7 +91,19 @@ objectname:: | ||||
| upstream:: | ||||
| 	The name of a local ref which can be considered ``upstream'' | ||||
| 	from the displayed ref. Respects `:short` in the same way as | ||||
| 	`refname` above. | ||||
| 	`refname` above.  Additionally respects `:track` to show | ||||
| 	"[ahead N, behind M]" and `:trackshort` to show the terse | ||||
| 	version: ">" (ahead), "<" (behind), "<>" (ahead and behind), | ||||
| 	or "=" (in sync).  Has no effect if the ref does not have | ||||
| 	tracking information associated with it. | ||||
|  | ||||
| HEAD:: | ||||
| 	'*' if HEAD matches current ref (the checked out branch), ' ' | ||||
| 	otherwise. | ||||
|  | ||||
| color:: | ||||
| 	Change output color.  Followed by `:<colorname>`, where names | ||||
| 	are described in `color.branch.*`. | ||||
|  | ||||
| In addition to the above, for commit and tag objects, the header | ||||
| field names (`tree`, `parent`, `object`, `type`, and `tag`) can | ||||
|  | ||||
| @ -9,6 +9,7 @@ | ||||
| #include "quote.h" | ||||
| #include "parse-options.h" | ||||
| #include "remote.h" | ||||
| #include "color.h" | ||||
|  | ||||
| /* Quoting styles */ | ||||
| #define QUOTE_NONE 0 | ||||
| @ -75,6 +76,8 @@ static struct { | ||||
| 	{ "upstream" }, | ||||
| 	{ "symref" }, | ||||
| 	{ "flag" }, | ||||
| 	{ "HEAD" }, | ||||
| 	{ "color" }, | ||||
| }; | ||||
|  | ||||
| /* | ||||
| @ -90,6 +93,7 @@ static struct { | ||||
| static const char **used_atom; | ||||
| static cmp_type *used_atom_type; | ||||
| static int used_atom_cnt, sort_atom_limit, need_tagged, need_symref; | ||||
| static int need_color_reset_at_eol; | ||||
|  | ||||
| /* | ||||
|  * Used to parse format string and sort specifiers | ||||
| @ -176,13 +180,21 @@ static const char *find_next(const char *cp) | ||||
| static int verify_format(const char *format) | ||||
| { | ||||
| 	const char *cp, *sp; | ||||
| 	static const char color_reset[] = "color:reset"; | ||||
|  | ||||
| 	need_color_reset_at_eol = 0; | ||||
| 	for (cp = format; *cp && (sp = find_next(cp)); ) { | ||||
| 		const char *ep = strchr(sp, ')'); | ||||
| 		int at; | ||||
|  | ||||
| 		if (!ep) | ||||
| 			return error("malformed format string %s", sp); | ||||
| 		/* sp points at "%(" and ep points at the closing ")" */ | ||||
| 		parse_atom(sp + 2, ep); | ||||
| 		at = parse_atom(sp + 2, ep); | ||||
| 		cp = ep + 1; | ||||
|  | ||||
| 		if (!memcmp(used_atom[at], "color:", 6)) | ||||
| 			need_color_reset_at_eol = !!strcmp(used_atom[at], color_reset); | ||||
| 	} | ||||
| 	return 0; | ||||
| } | ||||
| @ -649,6 +661,7 @@ static void populate_value(struct refinfo *ref) | ||||
| 		int deref = 0; | ||||
| 		const char *refname; | ||||
| 		const char *formatp; | ||||
| 		struct branch *branch = NULL; | ||||
|  | ||||
| 		if (*name == '*') { | ||||
| 			deref = 1; | ||||
| @ -660,7 +673,6 @@ static void populate_value(struct refinfo *ref) | ||||
| 		else if (!prefixcmp(name, "symref")) | ||||
| 			refname = ref->symref ? ref->symref : ""; | ||||
| 		else if (!prefixcmp(name, "upstream")) { | ||||
| 			struct branch *branch; | ||||
| 			/* only local branches may have an upstream */ | ||||
| 			if (prefixcmp(ref->refname, "refs/heads/")) | ||||
| 				continue; | ||||
| @ -670,8 +682,13 @@ static void populate_value(struct refinfo *ref) | ||||
| 			    !branch->merge[0]->dst) | ||||
| 				continue; | ||||
| 			refname = branch->merge[0]->dst; | ||||
| 		} | ||||
| 		else if (!strcmp(name, "flag")) { | ||||
| 		} else if (!prefixcmp(name, "color:")) { | ||||
| 			char color[COLOR_MAXLEN] = ""; | ||||
|  | ||||
| 			color_parse(name + 6, "--format", color); | ||||
| 			v->s = xstrdup(color); | ||||
| 			continue; | ||||
| 		} else if (!strcmp(name, "flag")) { | ||||
| 			char buf[256], *cp = buf; | ||||
| 			if (ref->flag & REF_ISSYMREF) | ||||
| 				cp = copy_advance(cp, ",symref"); | ||||
| @ -684,20 +701,62 @@ static void populate_value(struct refinfo *ref) | ||||
| 				v->s = xstrdup(buf + 1); | ||||
| 			} | ||||
| 			continue; | ||||
| 		} | ||||
| 		else if (!deref && grab_objectname(name, ref->objectname, v)) | ||||
| 		} else if (!deref && grab_objectname(name, ref->objectname, v)) { | ||||
| 			continue; | ||||
| 		else | ||||
| 		} else if (!strcmp(name, "HEAD")) { | ||||
| 			const char *head; | ||||
| 			unsigned char sha1[20]; | ||||
|  | ||||
| 			head = resolve_ref_unsafe("HEAD", sha1, 1, NULL); | ||||
| 			if (!strcmp(ref->refname, head)) | ||||
| 				v->s = "*"; | ||||
| 			else | ||||
| 				v->s = " "; | ||||
| 			continue; | ||||
| 		} else | ||||
| 			continue; | ||||
|  | ||||
| 		formatp = strchr(name, ':'); | ||||
| 		/* look for "short" refname format */ | ||||
| 		if (formatp) { | ||||
| 			int num_ours, num_theirs; | ||||
|  | ||||
| 			formatp++; | ||||
| 			if (!strcmp(formatp, "short")) | ||||
| 				refname = shorten_unambiguous_ref(refname, | ||||
| 						      warn_ambiguous_refs); | ||||
| 			else | ||||
| 			else if (!strcmp(formatp, "track") && | ||||
| 				!prefixcmp(name, "upstream")) { | ||||
| 				char buf[40]; | ||||
|  | ||||
| 				stat_tracking_info(branch, &num_ours, &num_theirs); | ||||
| 				if (!num_ours && !num_theirs) | ||||
| 					v->s = ""; | ||||
| 				else if (!num_ours) { | ||||
| 					sprintf(buf, "[behind %d]", num_theirs); | ||||
| 					v->s = xstrdup(buf); | ||||
| 				} else if (!num_theirs) { | ||||
| 					sprintf(buf, "[ahead %d]", num_ours); | ||||
| 					v->s = xstrdup(buf); | ||||
| 				} else { | ||||
| 					sprintf(buf, "[ahead %d, behind %d]", | ||||
| 						num_ours, num_theirs); | ||||
| 					v->s = xstrdup(buf); | ||||
| 				} | ||||
| 				continue; | ||||
| 			} else if (!strcmp(formatp, "trackshort") && | ||||
| 				!prefixcmp(name, "upstream")) { | ||||
| 				assert(branch); | ||||
| 				stat_tracking_info(branch, &num_ours, &num_theirs); | ||||
| 				if (!num_ours && !num_theirs) | ||||
| 					v->s = "="; | ||||
| 				else if (!num_ours) | ||||
| 					v->s = "<"; | ||||
| 				else if (!num_theirs) | ||||
| 					v->s = ">"; | ||||
| 				else | ||||
| 					v->s = "<>"; | ||||
| 				continue; | ||||
| 			} else | ||||
| 				die("unknown %.*s format %s", | ||||
| 				    (int)(formatp - name), name, formatp); | ||||
| 		} | ||||
| @ -875,11 +934,9 @@ static void sort_refs(struct ref_sort *sort, struct refinfo **refs, int num_refs | ||||
| 	qsort(refs, num_refs, sizeof(struct refinfo *), compare_refs); | ||||
| } | ||||
|  | ||||
| static void print_value(struct refinfo *ref, int atom, int quote_style) | ||||
| static void print_value(struct atom_value *v, int quote_style) | ||||
| { | ||||
| 	struct atom_value *v; | ||||
| 	struct strbuf sb = STRBUF_INIT; | ||||
| 	get_value(ref, atom, &v); | ||||
| 	switch (quote_style) { | ||||
| 	case QUOTE_NONE: | ||||
| 		fputs(v->s, stdout); | ||||
| @ -946,15 +1003,26 @@ static void show_ref(struct refinfo *info, const char *format, int quote_style) | ||||
| 	const char *cp, *sp, *ep; | ||||
|  | ||||
| 	for (cp = format; *cp && (sp = find_next(cp)); cp = ep + 1) { | ||||
| 		struct atom_value *atomv; | ||||
|  | ||||
| 		ep = strchr(sp, ')'); | ||||
| 		if (cp < sp) | ||||
| 			emit(cp, sp); | ||||
| 		print_value(info, parse_atom(sp + 2, ep), quote_style); | ||||
| 		get_value(info, parse_atom(sp + 2, ep), &atomv); | ||||
| 		print_value(atomv, quote_style); | ||||
| 	} | ||||
| 	if (*cp) { | ||||
| 		sp = cp + strlen(cp); | ||||
| 		emit(cp, sp); | ||||
| 	} | ||||
| 	if (need_color_reset_at_eol) { | ||||
| 		struct atom_value resetv; | ||||
| 		char color[COLOR_MAXLEN] = ""; | ||||
|  | ||||
| 		color_parse("reset", "--format", color); | ||||
| 		resetv.s = color; | ||||
| 		print_value(&resetv, quote_style); | ||||
| 	} | ||||
| 	putchar('\n'); | ||||
| } | ||||
|  | ||||
|  | ||||
| @ -18,16 +18,13 @@ setdate_and_increment () { | ||||
|     export GIT_COMMITTER_DATE GIT_AUTHOR_DATE | ||||
| } | ||||
|  | ||||
| test_expect_success 'Create sample commit with known timestamp' ' | ||||
| test_expect_success setup ' | ||||
| 	setdate_and_increment && | ||||
| 	echo "Using $datestamp" > one && | ||||
| 	git add one && | ||||
| 	git commit -m "Initial" && | ||||
| 	setdate_and_increment && | ||||
| 	git tag -a -m "Tagging at $datestamp" testtag | ||||
| ' | ||||
|  | ||||
| test_expect_success 'Create upstream config' ' | ||||
| 	git tag -a -m "Tagging at $datestamp" testtag && | ||||
| 	git update-ref refs/remotes/origin/master master && | ||||
| 	git remote add origin nowhere && | ||||
| 	git config branch.master.remote origin && | ||||
| @ -52,8 +49,8 @@ test_atom head refname refs/heads/master | ||||
| test_atom head upstream refs/remotes/origin/master | ||||
| test_atom head objecttype commit | ||||
| test_atom head objectsize 171 | ||||
| test_atom head objectname 67a36f10722846e891fbada1ba48ed035de75581 | ||||
| test_atom head tree 0e51c00fcb93dffc755546f27593d511e1bdb46f | ||||
| test_atom head objectname $(git rev-parse refs/heads/master) | ||||
| test_atom head tree $(git rev-parse refs/heads/master^{tree}) | ||||
| test_atom head parent '' | ||||
| test_atom head numparent 0 | ||||
| test_atom head object '' | ||||
| @ -82,16 +79,17 @@ test_atom head contents:body '' | ||||
| test_atom head contents:signature '' | ||||
| test_atom head contents 'Initial | ||||
| ' | ||||
| test_atom head HEAD '*' | ||||
|  | ||||
| test_atom tag refname refs/tags/testtag | ||||
| test_atom tag upstream '' | ||||
| test_atom tag objecttype tag | ||||
| test_atom tag objectsize 154 | ||||
| test_atom tag objectname 98b46b1d36e5b07909de1b3886224e3e81e87322 | ||||
| test_atom tag objectname $(git rev-parse refs/tags/testtag) | ||||
| test_atom tag tree '' | ||||
| test_atom tag parent '' | ||||
| test_atom tag numparent '' | ||||
| test_atom tag object '67a36f10722846e891fbada1ba48ed035de75581' | ||||
| test_atom tag object $(git rev-parse refs/tags/testtag^0) | ||||
| test_atom tag type 'commit' | ||||
| test_atom tag '*objectname' '67a36f10722846e891fbada1ba48ed035de75581' | ||||
| test_atom tag '*objecttype' 'commit' | ||||
| @ -117,6 +115,7 @@ test_atom tag contents:body '' | ||||
| test_atom tag contents:signature '' | ||||
| test_atom tag contents 'Tagging at 1151939927 | ||||
| ' | ||||
| test_atom tag HEAD ' ' | ||||
|  | ||||
| test_expect_success 'Check invalid atoms names are errors' ' | ||||
| 	test_must_fail git for-each-ref --format="%(INVALID)" refs/heads | ||||
| @ -308,8 +307,35 @@ test_expect_success 'Check short upstream format' ' | ||||
| 	test_cmp expected actual | ||||
| ' | ||||
|  | ||||
| test_expect_success 'setup for upstream:track[short]' ' | ||||
| 	test_commit two | ||||
| ' | ||||
|  | ||||
| cat >expected <<EOF | ||||
| 67a36f1 | ||||
| [ahead 1] | ||||
| EOF | ||||
|  | ||||
| test_expect_success 'Check upstream:track format' ' | ||||
| 	git for-each-ref --format="%(upstream:track)" refs/heads >actual && | ||||
| 	test_cmp expected actual | ||||
| ' | ||||
|  | ||||
| cat >expected <<EOF | ||||
| > | ||||
| EOF | ||||
|  | ||||
| test_expect_success 'Check upstream:trackshort format' ' | ||||
| 	git for-each-ref --format="%(upstream:trackshort)" refs/heads >actual && | ||||
| 	test_cmp expected actual | ||||
| ' | ||||
|  | ||||
| test_expect_success 'Check that :track[short] cannot be used with other atoms' ' | ||||
| 	test_must_fail git for-each-ref --format="%(refname:track)" 2>/dev/null && | ||||
| 	test_must_fail git for-each-ref --format="%(refname:trackshort)" 2>/dev/null | ||||
| ' | ||||
|  | ||||
| cat >expected <<EOF | ||||
| $(git rev-parse --short HEAD) | ||||
| EOF | ||||
|  | ||||
| test_expect_success 'Check short objectname format' ' | ||||
| @ -321,6 +347,23 @@ test_expect_success 'Check for invalid refname format' ' | ||||
| 	test_must_fail git for-each-ref --format="%(refname:INVALID)" | ||||
| ' | ||||
|  | ||||
| get_color () | ||||
| { | ||||
| 	git config --get-color no.such.slot "$1" | ||||
| } | ||||
|  | ||||
| cat >expected <<EOF | ||||
| $(git rev-parse --short refs/heads/master) $(get_color green)master$(get_color reset) | ||||
| $(git rev-parse --short refs/remotes/origin/master) $(get_color green)origin/master$(get_color reset) | ||||
| $(git rev-parse --short refs/tags/testtag) $(get_color green)testtag$(get_color reset) | ||||
| $(git rev-parse --short refs/tags/two) $(get_color green)two$(get_color reset) | ||||
| EOF | ||||
|  | ||||
| test_expect_success 'Check %(color:...) ' ' | ||||
| 	git for-each-ref --format="%(objectname:short) %(color:green)%(refname:short)" >actual && | ||||
| 	test_cmp expected actual | ||||
| ' | ||||
|  | ||||
| cat >expected <<\EOF | ||||
| heads/master | ||||
| tags/master | ||||
| @ -460,9 +503,9 @@ test_atom refs/tags/signed-long contents "subject line | ||||
| body contents | ||||
| $sig" | ||||
|  | ||||
| cat >expected <<\EOF | ||||
| 408fe76d02a785a006c2e9c669b7be5589ede96d <committer@example.com> refs/tags/master | ||||
| 90b5ebede4899eda64893bc2a4c8f1d6fb6dfc40 <committer@example.com> refs/tags/bogo | ||||
| cat >expected <<EOF | ||||
| $(git rev-parse refs/tags/master) <committer@example.com> refs/tags/master | ||||
| $(git rev-parse refs/tags/bogo) <committer@example.com> refs/tags/bogo | ||||
| EOF | ||||
|  | ||||
| test_expect_success 'Verify sort with multiple keys' ' | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Junio C Hamano
					Junio C Hamano