ref-filter: implement %(if), %(then), and %(else) atoms

Implement %(if), %(then) and %(else) atoms. Used as %(if)...%(then)...%(end) or %(if)...%(then)...%(else)...%(end). If the format string between %(if) and %(then) expands to an empty string, or to only whitespaces, then the whole %(if)...%(end) expands to the string following %(then). Otherwise, it expands to the string following %(else), if any. Nesting of this construct is possible. This is in preparation for porting over `git branch -l` to use ref-filter APIs for printing. Add documentation and tests regarding the same. Mentored-by: Christian Couder <christian.couder@gmail.com> Mentored-by: Matthieu Moy <matthieu.moy@grenoble-inp.fr> Signed-off-by: Karthik Nayak <karthik.188@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Karthik Nayak committed Jan 10, 2017 at 14:19 UTC c58492d4347fb6ee68fbb59b40e34ffde4dc2389
3 files changed +237 -7
Documentation/git-for-each-ref.txt
+34
@@ -149,6 +149,16 @@ align::
149 quoted, but if nested then only the topmost level performs
150 quoting.
151
152 +if::
153 + Used as %(if)...%(then)...%(end) or
154 + %(if)...%(then)...%(else)...%(end). If there is an atom with
155 + value or string literal after the %(if) then everything after
156 + the %(then) is printed, else if the %(else) atom is used, then
157 + everything after %(else) is printed. We ignore space when
158 + evaluating the string before %(then), this is useful when we
159 + use the %(HEAD) atom which prints either "*" or " " and we
160 + want to apply the 'if' condition only on the 'HEAD' ref.
161 +
162 In addition to the above, for commit and tag objects, the header
163 field names (`tree`, `parent`, `object`, `type`, and `tag`) can
164 be used to specify the value in the header field.
@@ -186,6 +196,14 @@ As a special case for the date-type fields, you may specify a format for
196 the date by adding `:` followed by date format name (see the
197 values the `--date` option to linkgit:git-rev-list[1] takes).
198
199 +Some atoms like %(align) and %(if) always require a matching %(end).
200 +We call them "opening atoms" and sometimes denote them as %($open).
201 +
202 +When a scripting language specific quoting is in effect, everything
203 +between a top-level opening atom and its matching %(end) is evaluated
204 +according to the semantics of the opening atom and only its result
205 +from the top-level is quoted.
206 +
207
208 EXAMPLES
209 --------
@@ -273,6 +291,22 @@ eval=`git for-each-ref --shell --format="$fmt" \
291 eval "$eval"
292 ------------
293
294 +
295 +An example to show the usage of %(if)...%(then)...%(else)...%(end).
296 +This prefixes the current branch with a star.
297 +
298 +------------
299 +git for-each-ref --format="%(if)%(HEAD)%(then)* %(else) %(end)%(refname:short)" refs/heads/
300 +------------
301 +
302 +
303 +An example to show the usage of %(if)...%(then)...%(end).
304 +This prints the authorname, if present.
305 +
306 +------------
307 +git for-each-ref --format="%(refname)%(if)%(authorname)%(then) Authored by: %(authorname)%(end)"
308 +------------
309 +
310 SEE ALSO
311 --------
312 linkgit:git-show-ref[1]
ref-filter.c
+127 -7
@@ -22,6 +22,12 @@ struct align {
22 unsigned int width;
23 };
24
25 +struct if_then_else {
26 + unsigned int then_atom_seen : 1,
27 + else_atom_seen : 1,
28 + condition_satisfied : 1;
29 +};
30 +
31 /*
32 * An atom is a valid field atom listed below, possibly prefixed with
33 * a "*" to denote deref_tag().
@@ -214,6 +220,9 @@ static struct {
220 { "color", FIELD_STR, color_atom_parser },
221 { "align", FIELD_STR, align_atom_parser },
222 { "end" },
223 + { "if" },
224 + { "then" },
225 + { "else" },
226 };
227
228 #define REF_FORMATTING_STATE_INIT { 0, NULL }
@@ -221,7 +230,7 @@ static struct {
230 struct ref_formatting_stack {
231 struct ref_formatting_stack *prev;
232 struct strbuf output;
224 - void (*at_end)(struct ref_formatting_stack *stack);
233 + void (*at_end)(struct ref_formatting_stack **stack);
234 void *at_end_data;
235 };
236
@@ -354,13 +363,14 @@ static void pop_stack_element(struct ref_formatting_stack **stack)
363 *stack = prev;
364 }
365
357 -static void end_align_handler(struct ref_formatting_stack *stack)
366 +static void end_align_handler(struct ref_formatting_stack **stack)
367 {
359 - struct align *align = (struct align *)stack->at_end_data;
368 + struct ref_formatting_stack *cur = *stack;
369 + struct align *align = (struct align *)cur->at_end_data;
370 struct strbuf s = STRBUF_INIT;
371
362 - strbuf_utf8_align(&s, align->position, align->width, stack->output.buf);
363 - strbuf_swap(&stack->output, &s);
372 + strbuf_utf8_align(&s, align->position, align->width, cur->output.buf);
373 + strbuf_swap(&cur->output, &s);
374 strbuf_release(&s);
375 }
376
@@ -374,6 +384,104 @@ static void align_atom_handler(struct atom_value *atomv, struct ref_formatting_s
384 new->at_end_data = &atomv->u.align;
385 }
386
387 +static void if_then_else_handler(struct ref_formatting_stack **stack)
388 +{
389 + struct ref_formatting_stack *cur = *stack;
390 + struct ref_formatting_stack *prev = cur->prev;
391 + struct if_then_else *if_then_else = (struct if_then_else *)cur->at_end_data;
392 +
393 + if (!if_then_else->then_atom_seen)
394 + die(_("format: %%(if) atom used without a %%(then) atom"));
395 +
396 + if (if_then_else->else_atom_seen) {
397 + /*
398 + * There is an %(else) atom: we need to drop one state from the
399 + * stack, either the %(else) branch if the condition is satisfied, or
400 + * the %(then) branch if it isn't.
401 + */
402 + if (if_then_else->condition_satisfied) {
403 + strbuf_reset(&cur->output);
404 + pop_stack_element(&cur);
405 + } else {
406 + strbuf_swap(&cur->output, &prev->output);
407 + strbuf_reset(&cur->output);
408 + pop_stack_element(&cur);
409 + }
410 + } else if (!if_then_else->condition_satisfied) {
411 + /*
412 + * No %(else) atom: just drop the %(then) branch if the
413 + * condition is not satisfied.
414 + */
415 + strbuf_reset(&cur->output);
416 + }
417 +
418 + *stack = cur;
419 + free(if_then_else);
420 +}
421 +
422 +static void if_atom_handler(struct atom_value *atomv, struct ref_formatting_state *state)
423 +{
424 + struct ref_formatting_stack *new;
425 + struct if_then_else *if_then_else = xcalloc(sizeof(struct if_then_else), 1);
426 +
427 + push_stack_element(&state->stack);
428 + new = state->stack;
429 + new->at_end = if_then_else_handler;
430 + new->at_end_data = if_then_else;
431 +}
432 +
433 +static int is_empty(const char *s)
434 +{
435 + while (*s != '\0') {
436 + if (!isspace(*s))
437 + return 0;
438 + s++;
439 + }
440 + return 1;
441 +}
442 +
443 +static void then_atom_handler(struct atom_value *atomv, struct ref_formatting_state *state)
444 +{
445 + struct ref_formatting_stack *cur = state->stack;
446 + struct if_then_else *if_then_else = NULL;
447 +
448 + if (cur->at_end == if_then_else_handler)
449 + if_then_else = (struct if_then_else *)cur->at_end_data;
450 + if (!if_then_else)
451 + die(_("format: %%(then) atom used without an %%(if) atom"));
452 + if (if_then_else->then_atom_seen)
453 + die(_("format: %%(then) atom used more than once"));
454 + if (if_then_else->else_atom_seen)
455 + die(_("format: %%(then) atom used after %%(else)"));
456 + if_then_else->then_atom_seen = 1;
457 + /*
458 + * If there exists non-empty string between the 'if' and
459 + * 'then' atom then the 'if' condition is satisfied.
460 + */
461 + if (cur->output.len && !is_empty(cur->output.buf))
462 + if_then_else->condition_satisfied = 1;
463 + strbuf_reset(&cur->output);
464 +}
465 +
466 +static void else_atom_handler(struct atom_value *atomv, struct ref_formatting_state *state)
467 +{
468 + struct ref_formatting_stack *prev = state->stack;
469 + struct if_then_else *if_then_else = NULL;
470 +
471 + if (prev->at_end == if_then_else_handler)
472 + if_then_else = (struct if_then_else *)prev->at_end_data;
473 + if (!if_then_else)
474 + die(_("format: %%(else) atom used without an %%(if) atom"));
475 + if (!if_then_else->then_atom_seen)
476 + die(_("format: %%(else) atom used without a %%(then) atom"));
477 + if (if_then_else->else_atom_seen)
478 + die(_("format: %%(else) atom used more than once"));
479 + if_then_else->else_atom_seen = 1;
480 + push_stack_element(&state->stack);
481 + state->stack->at_end_data = prev->at_end_data;
482 + state->stack->at_end = prev->at_end;
483 +}
484 +
485 static void end_atom_handler(struct atom_value *atomv, struct ref_formatting_state *state)
486 {
487 struct ref_formatting_stack *current = state->stack;
@@ -381,14 +489,17 @@ static void end_atom_handler(struct atom_value *atomv, struct ref_formatting_sta
489
490 if (!current->at_end)
491 die(_("format: %%(end) atom used without corresponding atom"));
384 - current->at_end(current);
492 + current->at_end(&state->stack);
493 +
494 + /* Stack may have been popped within at_end(), hence reset the current pointer */
495 + current = state->stack;
496
497 /*
498 * Perform quote formatting when the stack element is that of
499 * a supporting atom. If nested then perform quote formatting
500 * only on the topmost supporting atom.
501 */
391 - if (!state->stack->prev->prev) {
502 + if (!current->prev->prev) {
503 quote_formatting(&s, current->output.buf, state->quote_style);
504 strbuf_swap(&current->output, &s);
505 }
@@ -1049,6 +1160,15 @@ static void populate_value(struct ref_array_item *ref)
1160 } else if (!strcmp(name, "end")) {
1161 v->handler = end_atom_handler;
1162 continue;
1163 + } else if (!strcmp(name, "if")) {
1164 + v->handler = if_atom_handler;
1165 + continue;
1166 + } else if (!strcmp(name, "then")) {
1167 + v->handler = then_atom_handler;
1168 + continue;
1169 + } else if (!strcmp(name, "else")) {
1170 + v->handler = else_atom_handler;
1171 + continue;
1172 } else
1173 continue;
1174
t/t6302-for-each-ref-filter.sh
+76
@@ -327,4 +327,80 @@ test_expect_success 'reverse version sort' '
327 test_cmp expect actual
328 '
329
330 +test_expect_success 'improper usage of %(if), %(then), %(else) and %(end) atoms' '
331 + test_must_fail git for-each-ref --format="%(if)" &&
332 + test_must_fail git for-each-ref --format="%(then) %(end)" &&
333 + test_must_fail git for-each-ref --format="%(else) %(end)" &&
334 + test_must_fail git for-each-ref --format="%(if) %(else) %(end)" &&
335 + test_must_fail git for-each-ref --format="%(if) %(then) %(then) %(end)" &&
336 + test_must_fail git for-each-ref --format="%(then) %(else) %(end)" &&
337 + test_must_fail git for-each-ref --format="%(if) %(else) %(end)" &&
338 + test_must_fail git for-each-ref --format="%(if) %(then) %(else)" &&
339 + test_must_fail git for-each-ref --format="%(if) %(else) %(then) %(end)" &&
340 + test_must_fail git for-each-ref --format="%(if) %(then) %(else) %(else) %(end)" &&
341 + test_must_fail git for-each-ref --format="%(if) %(end)"
342 +'
343 +
344 +test_expect_success 'check %(if)...%(then)...%(end) atoms' '
345 + git for-each-ref --format="%(refname)%(if)%(authorname)%(then) Author: %(authorname)%(end)" >actual &&
346 + cat >expect <<-\EOF &&
347 + refs/heads/master Author: A U Thor
348 + refs/heads/side Author: A U Thor
349 + refs/odd/spot Author: A U Thor
350 + refs/tags/annotated-tag
351 + refs/tags/doubly-annotated-tag
352 + refs/tags/doubly-signed-tag
353 + refs/tags/foo1.10 Author: A U Thor
354 + refs/tags/foo1.3 Author: A U Thor
355 + refs/tags/foo1.6 Author: A U Thor
356 + refs/tags/four Author: A U Thor
357 + refs/tags/one Author: A U Thor
358 + refs/tags/signed-tag
359 + refs/tags/three Author: A U Thor
360 + refs/tags/two Author: A U Thor
361 + EOF
362 + test_cmp expect actual
363 +'
364 +
365 +test_expect_success 'check %(if)...%(then)...%(else)...%(end) atoms' '
366 + git for-each-ref --format="%(if)%(authorname)%(then)%(authorname)%(else)No author%(end): %(refname)" >actual &&
367 + cat >expect <<-\EOF &&
368 + A U Thor: refs/heads/master
369 + A U Thor: refs/heads/side
370 + A U Thor: refs/odd/spot
371 + No author: refs/tags/annotated-tag
372 + No author: refs/tags/doubly-annotated-tag
373 + No author: refs/tags/doubly-signed-tag
374 + A U Thor: refs/tags/foo1.10
375 + A U Thor: refs/tags/foo1.3
376 + A U Thor: refs/tags/foo1.6
377 + A U Thor: refs/tags/four
378 + A U Thor: refs/tags/one
379 + No author: refs/tags/signed-tag
380 + A U Thor: refs/tags/three
381 + A U Thor: refs/tags/two
382 + EOF
383 + test_cmp expect actual
384 +'
385 +test_expect_success 'ignore spaces in %(if) atom usage' '
386 + git for-each-ref --format="%(refname:short): %(if)%(HEAD)%(then)Head ref%(else)Not Head ref%(end)" >actual &&
387 + cat >expect <<-\EOF &&
388 + master: Head ref
389 + side: Not Head ref
390 + odd/spot: Not Head ref
391 + annotated-tag: Not Head ref
392 + doubly-annotated-tag: Not Head ref
393 + doubly-signed-tag: Not Head ref
394 + foo1.10: Not Head ref
395 + foo1.3: Not Head ref
396 + foo1.6: Not Head ref
397 + four: Not Head ref
398 + one: Not Head ref
399 + signed-tag: Not Head ref
400 + three: Not Head ref
401 + two: Not Head ref
402 + EOF
403 + test_cmp expect actual
404 +'
405 +
406 test_done