Merge branch 'kn/for-each-tag'

The "ref-filter" code was taught about many parts of what "tag -l"
does and then "tag -l" is being reimplemented in terms of "ref-filter".

* kn/for-each-tag:
  tag.c: implement '--merged' and '--no-merged' options
  tag.c: implement '--format' option
  tag.c: use 'ref-filter' APIs
  tag.c: use 'ref-filter' data structures
  ref-filter: add option to match literal pattern
  ref-filter: add support to sort by version
  ref-filter: add support for %(contents:lines=X)
  ref-filter: add option to filter out tags, branches and remotes
  ref-filter: implement an `align` atom
  ref-filter: introduce match_atom_name()
  ref-filter: introduce handler function for each atom
  utf8: add function to align a string into given strbuf
  ref-filter: introduce ref_formatting_state and ref_formatting_stack
  ref-filter: move `struct atom_value` to ref-filter.c
  strtoul_ui: reject negative values
This commit is contained in:
Junio C Hamano 2015-10-05 12:30:18 -07:00
commit 8a54523f0f
13 changed files with 746 additions and 379 deletions

View File

@ -127,6 +127,17 @@ color::
Change output color. Followed by `:<colorname>`, where names
are described in `color.branch.*`.
align::
Left-, middle-, or right-align the content between
%(align:...) and %(end). The "align:" is followed by `<width>`
and `<position>` in any order separated by a comma, where the
`<position>` is either left, right or middle, default being
left and `<width>` is the total length of the content with
alignment. If the contents length is more than the width then
no alignment is performed. If used with '--quote' everything
in between %(align:...) and %(end) is quoted, but if nested
then only the topmost level performs quoting.
In addition to the above, for commit and tag objects, the header
field names (`tree`, `parent`, `object`, `type`, and `tag`) can
be used to specify the value in the header field.
@ -139,12 +150,16 @@ The complete message in a commit and tag object is `contents`.
Its first line is `contents:subject`, where subject is the concatenation
of all lines of the commit message up to the first blank line. The next
line is 'contents:body', where body is all of the lines after the first
blank line. Finally, the optional GPG signature is `contents:signature`.
blank line. The optional GPG signature is `contents:signature`. The
first `N` lines of the message is obtained using `contents:lines=N`.
For sorting purposes, fields with numeric values sort in numeric
order (`objectsize`, `authordate`, `committerdate`, `taggerdate`).
All other fields are used to sort in their byte-value order.
There is also an option to sort by versions, this can be done by using
the fieldname `version:refname` or its alias `v:refname`.
In any case, a field name that refers to a field inapplicable to
the object referred by the ref does not cause an error. It
returns an empty string instead.

View File

@ -13,7 +13,8 @@ SYNOPSIS
<tagname> [<commit> | <object>]
'git tag' -d <tagname>...
'git tag' [-n[<num>]] -l [--contains <commit>] [--points-at <object>]
[--column[=<options>] | --no-column] [--create-reflog] [<pattern>...]
[--column[=<options>] | --no-column] [--create-reflog] [--sort=<key>]
[--format=<format>] [--[no-]merged [<commit>]] [<pattern>...]
'git tag' -v <tagname>...
DESCRIPTION
@ -94,14 +95,16 @@ OPTIONS
using fnmatch(3)). Multiple patterns may be given; if any of
them matches, the tag is shown.
--sort=<type>::
Sort in a specific order. Supported type is "refname"
(lexicographic order), "version:refname" or "v:refname" (tag
--sort=<key>::
Sort based on the key given. Prefix `-` to sort in
descending order of the value. You may use the --sort=<key> option
multiple times, in which case the last key becomes the primary
key. Also supports "version:refname" or "v:refname" (tag
names are treated as versions). The "version:refname" sort
order can also be affected by the
"versionsort.prereleaseSuffix" configuration variable. Prepend
"-" to reverse sort order. When this option is not given, the
sort order defaults to the value configured for the 'tag.sort'
"versionsort.prereleaseSuffix" configuration variable.
The keys supported are the same as those in `git for-each-ref`.
Sort order defaults to the value configured for the 'tag.sort'
variable if it exists, or lexicographic order otherwise. See
linkgit:git-config[1].
@ -156,6 +159,16 @@ This option is only applicable when listing tags without annotation lines.
The object that the new tag will refer to, usually a commit.
Defaults to HEAD.
<format>::
A string that interpolates `%(fieldname)` from the object
pointed at by a ref being shown. The format is the same as
that of linkgit:git-for-each-ref[1]. When unspecified,
defaults to `%(refname:short)`.
--[no-]merged [<commit>]::
Only list tags whose tips are reachable, or not reachable
if '--no-merged' is used, from the specified commit ('HEAD'
if not specified).
CONFIGURATION
-------------

View File

@ -68,6 +68,7 @@ int cmd_for_each_ref(int argc, const char **argv, const char *prefix)
git_config(git_default_config, NULL);
filter.name_patterns = argv;
filter.match_as_path = 1;
filter_refs(&array, &filter, FILTER_REFS_ALL | FILTER_REFS_INCLUDE_BROKEN);
ref_array_sort(sorting, &array);

View File

@ -17,280 +17,49 @@
#include "gpg-interface.h"
#include "sha1-array.h"
#include "column.h"
#include "ref-filter.h"
static const char * const git_tag_usage[] = {
N_("git tag [-a | -s | -u <key-id>] [-f] [-m <msg> | -F <file>] <tagname> [<head>]"),
N_("git tag -d <tagname>..."),
N_("git tag -l [-n[<num>]] [--contains <commit>] [--points-at <object>]"
"\n\t\t[<pattern>...]"),
"\n\t\t[--format=<format>] [--[no-]merged [<commit>]] [<pattern>...]"),
N_("git tag -v <tagname>..."),
NULL
};
#define STRCMP_SORT 0 /* must be zero */
#define VERCMP_SORT 1
#define SORT_MASK 0x7fff
#define REVERSE_SORT 0x8000
static int tag_sort;
struct tag_filter {
const char **patterns;
int lines;
int sort;
struct string_list tags;
struct commit_list *with_commit;
};
static struct sha1_array points_at;
static unsigned int colopts;
static int match_pattern(const char **patterns, const char *ref)
{
/* no pattern means match everything */
if (!*patterns)
return 1;
for (; *patterns; patterns++)
if (!wildmatch(*patterns, ref, 0, NULL))
return 1;
return 0;
}
/*
* This is currently duplicated in ref-filter.c, and will eventually be
* removed as we port tag.c to use the ref-filter APIs.
*/
static const unsigned char *match_points_at(const char *refname,
const unsigned char *sha1)
{
const unsigned char *tagged_sha1 = NULL;
struct object *obj;
if (sha1_array_lookup(&points_at, sha1) >= 0)
return sha1;
obj = parse_object(sha1);
if (!obj)
die(_("malformed object at '%s'"), refname);
if (obj->type == OBJ_TAG)
tagged_sha1 = ((struct tag *)obj)->tagged->sha1;
if (tagged_sha1 && sha1_array_lookup(&points_at, tagged_sha1) >= 0)
return tagged_sha1;
return NULL;
}
static int in_commit_list(const struct commit_list *want, struct commit *c)
{
for (; want; want = want->next)
if (!hashcmp(want->item->object.sha1, c->object.sha1))
return 1;
return 0;
}
/*
* The entire code segment for supporting the --contains option has been
* copied over to ref-filter.{c,h}. This will be deleted evetually when
* we port tag.c to use ref-filter APIs.
*/
enum contains_result {
CONTAINS_UNKNOWN = -1,
CONTAINS_NO = 0,
CONTAINS_YES = 1
};
/*
* Test whether the candidate or one of its parents is contained in the list.
* Do not recurse to find out, though, but return -1 if inconclusive.
*/
static enum contains_result contains_test(struct commit *candidate,
const struct commit_list *want)
{
/* was it previously marked as containing a want commit? */
if (candidate->object.flags & TMP_MARK)
return 1;
/* or marked as not possibly containing a want commit? */
if (candidate->object.flags & UNINTERESTING)
return 0;
/* or are we it? */
if (in_commit_list(want, candidate)) {
candidate->object.flags |= TMP_MARK;
return 1;
}
if (parse_commit(candidate) < 0)
return 0;
return -1;
}
/*
* Mimicking the real stack, this stack lives on the heap, avoiding stack
* overflows.
*
* At each recursion step, the stack items points to the commits whose
* ancestors are to be inspected.
*/
struct stack {
int nr, alloc;
struct stack_entry {
struct commit *commit;
struct commit_list *parents;
} *stack;
};
static void push_to_stack(struct commit *candidate, struct stack *stack)
{
int index = stack->nr++;
ALLOC_GROW(stack->stack, stack->nr, stack->alloc);
stack->stack[index].commit = candidate;
stack->stack[index].parents = candidate->parents;
}
static enum contains_result contains(struct commit *candidate,
const struct commit_list *want)
{
struct stack stack = { 0, 0, NULL };
int result = contains_test(candidate, want);
if (result != CONTAINS_UNKNOWN)
return result;
push_to_stack(candidate, &stack);
while (stack.nr) {
struct stack_entry *entry = &stack.stack[stack.nr - 1];
struct commit *commit = entry->commit;
struct commit_list *parents = entry->parents;
if (!parents) {
commit->object.flags |= UNINTERESTING;
stack.nr--;
}
/*
* If we just popped the stack, parents->item has been marked,
* therefore contains_test will return a meaningful 0 or 1.
*/
else switch (contains_test(parents->item, want)) {
case CONTAINS_YES:
commit->object.flags |= TMP_MARK;
stack.nr--;
break;
case CONTAINS_NO:
entry->parents = parents->next;
break;
case CONTAINS_UNKNOWN:
push_to_stack(parents->item, &stack);
break;
}
}
free(stack.stack);
return contains_test(candidate, want);
}
static void show_tag_lines(const struct object_id *oid, int lines)
static int list_tags(struct ref_filter *filter, struct ref_sorting *sorting, const char *format)
{
struct ref_array array;
char *to_free = NULL;
int i;
unsigned long size;
enum object_type type;
char *buf, *sp, *eol;
size_t len;
buf = read_sha1_file(oid->hash, &type, &size);
if (!buf)
die_errno("unable to read object %s", oid_to_hex(oid));
if (type != OBJ_COMMIT && type != OBJ_TAG)
goto free_return;
if (!size)
die("an empty %s object %s?",
typename(type), oid_to_hex(oid));
memset(&array, 0, sizeof(array));
/* skip header */
sp = strstr(buf, "\n\n");
if (!sp)
goto free_return;
if (filter->lines == -1)
filter->lines = 0;
/* only take up to "lines" lines, and strip the signature from a tag */
if (type == OBJ_TAG)
size = parse_signature(buf, size);
for (i = 0, sp += 2; i < lines && sp < buf + size; i++) {
if (i)
printf("\n ");
eol = memchr(sp, '\n', size - (sp - buf));
len = eol ? eol - sp : size - (sp - buf);
fwrite(sp, len, 1, stdout);
if (!eol)
break;
sp = eol + 1;
}
free_return:
free(buf);
}
static int show_reference(const char *refname, const struct object_id *oid,
int flag, void *cb_data)
{
struct tag_filter *filter = cb_data;
if (match_pattern(filter->patterns, refname)) {
if (filter->with_commit) {
struct commit *commit;
commit = lookup_commit_reference_gently(oid->hash, 1);
if (!commit)
return 0;
if (!contains(commit, filter->with_commit))
return 0;
}
if (points_at.nr && !match_points_at(refname, oid->hash))
return 0;
if (!filter->lines) {
if (filter->sort)
string_list_append(&filter->tags, refname);
else
printf("%s\n", refname);
return 0;
}
printf("%-15s ", refname);
show_tag_lines(oid, filter->lines);
putchar('\n');
if (!format) {
if (filter->lines) {
to_free = xstrfmt("%s %%(contents:lines=%d)",
"%(align:15)%(refname:short)%(end)",
filter->lines);
format = to_free;
} else
format = "%(refname:short)";
}
return 0;
}
verify_ref_format(format);
filter_refs(&array, filter, FILTER_REFS_TAGS);
ref_array_sort(sorting, &array);
static int sort_by_version(const void *a_, const void *b_)
{
const struct string_list_item *a = a_;
const struct string_list_item *b = b_;
return versioncmp(a->string, b->string);
}
for (i = 0; i < array.nr; i++)
show_ref_array_item(array.items[i], format, 0);
ref_array_clear(&array);
free(to_free);
static int list_tags(const char **patterns, int lines,
struct commit_list *with_commit, int sort)
{
struct tag_filter filter;
filter.patterns = patterns;
filter.lines = lines;
filter.sort = sort;
filter.with_commit = with_commit;
memset(&filter.tags, 0, sizeof(filter.tags));
filter.tags.strdup_strings = 1;
for_each_tag_ref(show_reference, (void *)&filter);
if (sort) {
int i;
if ((sort & SORT_MASK) == VERCMP_SORT)
qsort(filter.tags.items, filter.tags.nr,
sizeof(struct string_list_item), sort_by_version);
if (sort & REVERSE_SORT)
for (i = filter.tags.nr - 1; i >= 0; i--)
printf("%s\n", filter.tags.items[i].string);
else
for (i = 0; i < filter.tags.nr; i++)
printf("%s\n", filter.tags.items[i].string);
string_list_clear(&filter.tags, 0);
}
return 0;
}
@ -357,35 +126,26 @@ static const char tag_template_nocleanup[] =
"Lines starting with '%c' will be kept; you may remove them"
" yourself if you want to.\n");
/*
* Parse a sort string, and return 0 if parsed successfully. Will return
* non-zero when the sort string does not parse into a known type. If var is
* given, the error message becomes a warning and includes information about
* the configuration value.
*/
static int parse_sort_string(const char *var, const char *arg, int *sort)
/* Parse arg given and add it the ref_sorting array */
static int parse_sorting_string(const char *arg, struct ref_sorting **sorting_tail)
{
int type = 0, flags = 0;
struct ref_sorting *s;
int len;
if (skip_prefix(arg, "-", &arg))
flags |= REVERSE_SORT;
s = xcalloc(1, sizeof(*s));
s->next = *sorting_tail;
*sorting_tail = s;
if (skip_prefix(arg, "version:", &arg) || skip_prefix(arg, "v:", &arg))
type = VERCMP_SORT;
else
type = STRCMP_SORT;
if (strcmp(arg, "refname")) {
if (!var)
return error(_("unsupported sort specification '%s'"), arg);
else {
warning(_("unsupported sort specification '%s' in variable '%s'"),
var, arg);
return -1;
}
if (*arg == '-') {
s->reverse = 1;
arg++;
}
if (skip_prefix(arg, "version:", &arg) ||
skip_prefix(arg, "v:", &arg))
s->version = 1;
*sort = (type | flags);
len = strlen(arg);
s->atom = parse_ref_filter_atom(arg, arg+len);
return 0;
}
@ -393,11 +153,12 @@ static int parse_sort_string(const char *var, const char *arg, int *sort)
static int git_tag_config(const char *var, const char *value, void *cb)
{
int status;
struct ref_sorting **sorting_tail = (struct ref_sorting **)cb;
if (!strcmp(var, "tag.sort")) {
if (!value)
return config_error_nonbool(var);
parse_sort_string(var, value, &tag_sort);
parse_sorting_string(value, sorting_tail);
return 0;
}
@ -555,13 +316,6 @@ static int strbuf_check_tag_ref(struct strbuf *sb, const char *name)
return check_refname_format(sb->buf, 0);
}
static int parse_opt_sort(const struct option *opt, const char *arg, int unset)
{
int *sort = opt->value;
return parse_sort_string(NULL, arg, sort);
}
int cmd_tag(int argc, const char **argv, const char *prefix)
{
struct strbuf buf = STRBUF_INIT;
@ -570,17 +324,19 @@ int cmd_tag(int argc, const char **argv, const char *prefix)
const char *object_ref, *tag;
struct create_tag_options opt;
char *cleanup_arg = NULL;
int annotate = 0, force = 0, lines = -1;
int create_reflog = 0;
int annotate = 0, force = 0;
int cmdmode = 0;
const char *msgfile = NULL, *keyid = NULL;
struct msg_arg msg = { 0, STRBUF_INIT };
struct commit_list *with_commit = NULL;
struct ref_transaction *transaction;
struct strbuf err = STRBUF_INIT;
struct ref_filter filter;
static struct ref_sorting *sorting = NULL, **sorting_tail = &sorting;
const char *format = NULL;
struct option options[] = {
OPT_CMDMODE('l', "list", &cmdmode, N_("list tag names"), 'l'),
{ OPTION_INTEGER, 'n', NULL, &lines, N_("n"),
{ OPTION_INTEGER, 'n', NULL, &filter.lines, N_("n"),
N_("print <n> lines of each tag message"),
PARSE_OPT_OPTARG, NULL, 1 },
OPT_CMDMODE('d', "delete", &cmdmode, N_("delete tags"), 'd'),
@ -602,22 +358,25 @@ int cmd_tag(int argc, const char **argv, const char *prefix)
OPT_GROUP(N_("Tag listing options")),
OPT_COLUMN(0, "column", &colopts, N_("show tag list in columns")),
OPT_CONTAINS(&with_commit, N_("print only tags that contain the commit")),
OPT_WITH(&with_commit, N_("print only tags that contain the commit")),
OPT_CONTAINS(&filter.with_commit, N_("print only tags that contain the commit")),
OPT_WITH(&filter.with_commit, N_("print only tags that contain the commit")),
OPT_MERGED(&filter, N_("print only tags that are merged")),
OPT_NO_MERGED(&filter, N_("print only tags that are not merged")),
OPT_CALLBACK(0 , "sort", sorting_tail, N_("key"),
N_("field name to sort on"), &parse_opt_ref_sorting),
{
OPTION_CALLBACK, 0, "sort", &tag_sort, N_("type"), N_("sort tags"),
PARSE_OPT_NONEG, parse_opt_sort
},
{
OPTION_CALLBACK, 0, "points-at", &points_at, N_("object"),
OPTION_CALLBACK, 0, "points-at", &filter.points_at, N_("object"),
N_("print only tags of the object"), 0, parse_opt_object_name
},
OPT_STRING( 0 , "format", &format, N_("format"), N_("format to use for the output")),
OPT_END()
};
git_config(git_tag_config, NULL);
git_config(git_tag_config, sorting_tail);
memset(&opt, 0, sizeof(opt));
memset(&filter, 0, sizeof(filter));
filter.lines = -1;
argc = parse_options(argc, argv, prefix, options, git_tag_usage, 0);
@ -634,11 +393,13 @@ int cmd_tag(int argc, const char **argv, const char *prefix)
usage_with_options(git_tag_usage, options);
finalize_colopts(&colopts, -1);
if (cmdmode == 'l' && lines != -1) {
if (cmdmode == 'l' && filter.lines != -1) {
if (explicitly_enable_column(colopts))
die(_("--column and -n are incompatible"));
colopts = 0;
}
if (!sorting)
sorting = ref_default_sorting();
if (cmdmode == 'l') {
int ret;
if (column_active(colopts)) {
@ -647,19 +408,20 @@ int cmd_tag(int argc, const char **argv, const char *prefix)
copts.padding = 2;
run_column_filter(colopts, &copts);
}
if (lines != -1 && tag_sort)
die(_("--sort and -n are incompatible"));
ret = list_tags(argv, lines == -1 ? 0 : lines, with_commit, tag_sort);
filter.name_patterns = argv;
ret = list_tags(&filter, sorting, format);
if (column_active(colopts))
stop_column_filter();
return ret;
}
if (lines != -1)
if (filter.lines != -1)
die(_("-n option is only allowed with -l."));
if (with_commit)
if (filter.with_commit)
die(_("--contains option is only allowed with -l."));
if (points_at.nr)
if (filter.points_at.nr)
die(_("--points-at option is only allowed with -l."));
if (filter.merge_commit)
die(_("--merged and --no-merged option are only allowed with -l"));
if (cmdmode == 'd')
return for_each_tag_name(argv, delete_tag);
if (cmdmode == 'v')

View File

@ -814,6 +814,9 @@ static inline int strtoul_ui(char const *s, int base, unsigned int *result)
char *p;
errno = 0;
/* negative values would be accepted by strtoul */
if (strchr(s, '-'))
return -1;
ul = strtoul(s, &p, base);
if (errno || *p || p == s || (unsigned int) ul != ul)
return -1;

View File

@ -10,6 +10,9 @@
#include "quote.h"
#include "ref-filter.h"
#include "revision.h"
#include "utf8.h"
#include "git-compat-util.h"
#include "version.h"
typedef enum { FIELD_STR, FIELD_ULONG, FIELD_TIME } cmp_type;
@ -44,15 +47,48 @@ static struct {
{ "subject" },
{ "body" },
{ "contents" },
{ "contents:subject" },
{ "contents:body" },
{ "contents:signature" },
{ "upstream" },
{ "push" },
{ "symref" },
{ "flag" },
{ "HEAD" },
{ "color" },
{ "align" },
{ "end" },
};
#define REF_FORMATTING_STATE_INIT { 0, NULL }
struct align {
align_type position;
unsigned int width;
};
struct contents {
unsigned int lines;
struct object_id oid;
};
struct ref_formatting_stack {
struct ref_formatting_stack *prev;
struct strbuf output;
void (*at_end)(struct ref_formatting_stack *stack);
void *at_end_data;
};
struct ref_formatting_state {
int quote_style;
struct ref_formatting_stack *stack;
};
struct atom_value {
const char *s;
union {
struct align align;
struct contents contents;
} u;
void (*handler)(struct atom_value *atomv, struct ref_formatting_state *state);
unsigned long ul; /* used for sorting when not FIELD_STR */
};
/*
@ -124,6 +160,120 @@ int parse_ref_filter_atom(const char *atom, const char *ep)
return at;
}
static void quote_formatting(struct strbuf *s, const char *str, int quote_style)
{
switch (quote_style) {
case QUOTE_NONE:
strbuf_addstr(s, str);
break;
case QUOTE_SHELL:
sq_quote_buf(s, str);
break;
case QUOTE_PERL:
perl_quote_buf(s, str);
break;
case QUOTE_PYTHON:
python_quote_buf(s, str);
break;
case QUOTE_TCL:
tcl_quote_buf(s, str);
break;
}
}
static void append_atom(struct atom_value *v, struct ref_formatting_state *state)
{
/*
* Quote formatting is only done when the stack has a single
* element. Otherwise quote formatting is done on the
* element's entire output strbuf when the %(end) atom is
* encountered.
*/
if (!state->stack->prev)
quote_formatting(&state->stack->output, v->s, state->quote_style);
else
strbuf_addstr(&state->stack->output, v->s);
}
static void push_stack_element(struct ref_formatting_stack **stack)
{
struct ref_formatting_stack *s = xcalloc(1, sizeof(struct ref_formatting_stack));
strbuf_init(&s->output, 0);
s->prev = *stack;
*stack = s;
}
static void pop_stack_element(struct ref_formatting_stack **stack)
{
struct ref_formatting_stack *current = *stack;
struct ref_formatting_stack *prev = current->prev;
if (prev)
strbuf_addbuf(&prev->output, &current->output);
strbuf_release(&current->output);
free(current);
*stack = prev;
}
static void end_align_handler(struct ref_formatting_stack *stack)
{
struct align *align = (struct align *)stack->at_end_data;
struct strbuf s = STRBUF_INIT;
strbuf_utf8_align(&s, align->position, align->width, stack->output.buf);
strbuf_swap(&stack->output, &s);
strbuf_release(&s);
}
static void align_atom_handler(struct atom_value *atomv, struct ref_formatting_state *state)
{
struct ref_formatting_stack *new;
push_stack_element(&state->stack);
new = state->stack;
new->at_end = end_align_handler;
new->at_end_data = &atomv->u.align;
}
static void end_atom_handler(struct atom_value *atomv, struct ref_formatting_state *state)
{
struct ref_formatting_stack *current = state->stack;
struct strbuf s = STRBUF_INIT;
if (!current->at_end)
die(_("format: %%(end) atom used without corresponding atom"));
current->at_end(current);
/*
* Perform quote formatting when the stack element is that of
* a supporting atom. If nested then perform quote formatting
* only on the topmost supporting atom.
*/
if (!state->stack->prev->prev) {
quote_formatting(&s, current->output.buf, state->quote_style);
strbuf_swap(&current->output, &s);
}
strbuf_release(&s);
pop_stack_element(&state->stack);
}
static int match_atom_name(const char *name, const char *atom_name, const char **val)
{
const char *body;
if (!skip_prefix(name, atom_name, &body))
return 0; /* doesn't even begin with "atom_name" */
if (!body[0]) {
*val = NULL; /* %(atom_name) and no customization */
return 1;
}
if (body[0] != ':')
return 0; /* "atom_namefoo" is not "atom_name" or "atom_name:..." */
*val = body + 1; /* "atom_name:val" */
return 1;
}
/*
* In a format string, find the next occurrence of %(atom).
*/
@ -498,6 +648,30 @@ static void find_subpos(const char *buf, unsigned long sz,
*nonsiglen = *sig - buf;
}
/*
* If 'lines' is greater than 0, append that many lines from the given
* 'buf' of length 'size' to the given strbuf.
*/
static void append_lines(struct strbuf *out, const char *buf, unsigned long size, int lines)
{
int i;
const char *sp, *eol;
size_t len;
sp = buf;
for (i = 0; i < lines && sp < buf + size; i++) {
if (i)
strbuf_addstr(out, "\n ");
eol = memchr(sp, '\n', size - (sp - buf));
len = eol ? eol - sp : size - (sp - buf);
strbuf_add(out, sp, len);
if (!eol)
break;
sp = eol + 1;
}
}
/* See grab_values */
static void grab_sub_body_contents(struct atom_value *val, int deref, struct object *obj, void *buf, unsigned long sz)
{
@ -508,6 +682,7 @@ static void grab_sub_body_contents(struct atom_value *val, int deref, struct obj
for (i = 0; i < used_atom_cnt; i++) {
const char *name = used_atom[i];
struct atom_value *v = &val[i];
const char *valp = NULL;
if (!!deref != (*name == '*'))
continue;
if (deref)
@ -517,7 +692,8 @@ static void grab_sub_body_contents(struct atom_value *val, int deref, struct obj
strcmp(name, "contents") &&
strcmp(name, "contents:subject") &&
strcmp(name, "contents:body") &&
strcmp(name, "contents:signature"))
strcmp(name, "contents:signature") &&
!starts_with(name, "contents:lines="))
continue;
if (!subpos)
find_subpos(buf, sz,
@ -537,6 +713,16 @@ static void grab_sub_body_contents(struct atom_value *val, int deref, struct obj
v->s = xmemdupz(sigpos, siglen);
else if (!strcmp(name, "contents"))
v->s = xstrdup(subpos);
else if (skip_prefix(name, "contents:lines=", &valp)) {
struct strbuf s = STRBUF_INIT;
const char *contents_end = bodylen + bodypos - siglen;
if (strtoul_ui(valp, 10, &v->u.contents.lines))
die(_("positive value expected contents:lines=%s"), valp);
/* Size is the length of the message after removing the signature */
append_lines(&s, subpos, contents_end - subpos, v->u.contents.lines);
v->s = strbuf_detach(&s, NULL);
}
}
}
@ -622,8 +808,11 @@ static void populate_value(struct ref_array_item *ref)
int deref = 0;
const char *refname;
const char *formatp;
const char *valp;
struct branch *branch = NULL;
v->handler = append_atom;
if (*name == '*') {
deref = 1;
name++;
@ -654,10 +843,12 @@ static void populate_value(struct ref_array_item *ref)
refname = branch_get_push(branch, NULL);
if (!refname)
continue;
} else if (starts_with(name, "color:")) {
} else if (match_atom_name(name, "color", &valp)) {
char color[COLOR_MAXLEN] = "";
if (color_parse(name + 6, color) < 0)
if (!valp)
die(_("expected format: %%(color:<color>)"));
if (color_parse(valp, color) < 0)
die(_("unable to parse format"));
v->s = xstrdup(color);
continue;
@ -687,6 +878,48 @@ static void populate_value(struct ref_array_item *ref)
else
v->s = " ";
continue;
} else if (match_atom_name(name, "align", &valp)) {
struct align *align = &v->u.align;
struct strbuf **s, **to_free;
int width = -1;
if (!valp)
die(_("expected format: %%(align:<width>,<position>)"));
/*
* TODO: Implement a function similar to strbuf_split_str()
* which would omit the separator from the end of each value.
*/
s = to_free = strbuf_split_str(valp, ',', 0);
align->position = ALIGN_LEFT;
while (*s) {
/* Strip trailing comma */
if (s[1])
strbuf_setlen(s[0], s[0]->len - 1);
if (!strtoul_ui(s[0]->buf, 10, (unsigned int *)&width))
;
else if (!strcmp(s[0]->buf, "left"))
align->position = ALIGN_LEFT;
else if (!strcmp(s[0]->buf, "right"))
align->position = ALIGN_RIGHT;
else if (!strcmp(s[0]->buf, "middle"))
align->position = ALIGN_MIDDLE;
else
die(_("improper format entered align:%s"), s[0]->buf);
s++;
}
if (width < 0)
die(_("positive width expected with the %%(align) atom"));
align->width = width;
strbuf_list_free(to_free);
v->handler = align_atom_handler;
continue;
} else if (!strcmp(name, "end")) {
v->handler = end_atom_handler;
continue;
} else
continue;
@ -926,11 +1159,35 @@ static int commit_contains(struct ref_filter *filter, struct commit *commit)
return is_descendant_of(commit, filter->with_commit);
}
/*
* Return 1 if the refname matches one of the patterns, otherwise 0.
* A pattern can be a literal prefix (e.g. a refname "refs/heads/master"
* matches a pattern "refs/heads/mas") or a wildcard (e.g. the same ref
* matches "refs/heads/mas*", too).
*/
static int match_pattern(const char **patterns, const char *refname)
{
/*
* When no '--format' option is given we need to skip the prefix
* for matching refs of tags and branches.
*/
(void)(skip_prefix(refname, "refs/tags/", &refname) ||
skip_prefix(refname, "refs/heads/", &refname) ||
skip_prefix(refname, "refs/remotes/", &refname) ||
skip_prefix(refname, "refs/", &refname));
for (; *patterns; patterns++) {
if (!wildmatch(*patterns, refname, 0, NULL))
return 1;
}
return 0;
}
/*
* Return 1 if the refname matches one of the patterns, otherwise 0.
* A pattern can be path prefix (e.g. a refname "refs/heads/master"
* matches a pattern "refs/heads/") or a wildcard (e.g. the same ref
* matches "refs/heads/m*",too).
* matches a pattern "refs/heads/" but not "refs/heads/m") or a
* wildcard (e.g. the same ref matches "refs/heads/m*", too).
*/
static int match_name_as_path(const char **pattern, const char *refname)
{
@ -951,6 +1208,16 @@ static int match_name_as_path(const char **pattern, const char *refname)
return 0;
}
/* Return 1 if the refname matches one of the patterns, otherwise 0. */
static int filter_pattern_match(struct ref_filter *filter, const char *refname)
{
if (!*filter->name_patterns)
return 1; /* No pattern always matches */
if (filter->match_as_path)
return match_name_as_path(filter->name_patterns, refname);
return match_pattern(filter->name_patterns, refname);
}
/*
* Given a ref (sha1, refname), check if the ref belongs to the array
* of sha1s. If the given ref is a tag, check if the given tag points
@ -998,6 +1265,34 @@ static struct ref_array_item *new_ref_array_item(const char *refname,
return ref;
}
static int filter_ref_kind(struct ref_filter *filter, const char *refname)
{
unsigned int i;
static struct {
const char *prefix;
unsigned int kind;
} ref_kind[] = {
{ "refs/heads/" , FILTER_REFS_BRANCHES },
{ "refs/remotes/" , FILTER_REFS_REMOTES },
{ "refs/tags/", FILTER_REFS_TAGS}
};
if (filter->kind == FILTER_REFS_BRANCHES ||
filter->kind == FILTER_REFS_REMOTES ||
filter->kind == FILTER_REFS_TAGS)
return filter->kind;
else if (!strcmp(refname, "HEAD"))
return FILTER_REFS_DETACHED_HEAD;
for (i = 0; i < ARRAY_SIZE(ref_kind); i++) {
if (starts_with(refname, ref_kind[i].prefix))
return ref_kind[i].kind;
}
return FILTER_REFS_OTHERS;
}
/*
* A call-back given to for_each_ref(). Filter refs and keep them for
* later object processing.
@ -1008,6 +1303,7 @@ static int ref_filter_handler(const char *refname, const struct object_id *oid,
struct ref_filter *filter = ref_cbdata->filter;
struct ref_array_item *ref;
struct commit *commit = NULL;
unsigned int kind;
if (flag & REF_BAD_NAME) {
warning("ignoring ref with broken name %s", refname);
@ -1019,7 +1315,12 @@ static int ref_filter_handler(const char *refname, const struct object_id *oid,
return 0;
}
if (*filter->name_patterns && !match_name_as_path(filter->name_patterns, refname))
/* Obtain the current ref kind from filter_ref_kind() and ignore unwanted refs. */
kind = filter_ref_kind(filter, refname);
if (!(kind & filter->kind))
return 0;
if (!filter_pattern_match(filter, refname))
return 0;
if (filter->points_at.nr && !match_points_at(&filter->points_at, oid->hash, refname))
@ -1050,6 +1351,7 @@ static int ref_filter_handler(const char *refname, const struct object_id *oid,
REALLOC_ARRAY(ref_cbdata->array->items, ref_cbdata->array->nr + 1);
ref_cbdata->array->items[ref_cbdata->array->nr++] = ref;
ref->kind = kind;
return 0;
}
@ -1126,17 +1428,37 @@ int filter_refs(struct ref_array *array, struct ref_filter *filter, unsigned int
{
struct ref_filter_cbdata ref_cbdata;
int ret = 0;
unsigned int broken = 0;
ref_cbdata.array = array;
ref_cbdata.filter = filter;
if (type & FILTER_REFS_INCLUDE_BROKEN)
broken = 1;
filter->kind = type & FILTER_REFS_KIND_MASK;
/* Simple per-ref filtering */
if (type & (FILTER_REFS_ALL | FILTER_REFS_INCLUDE_BROKEN))
ret = for_each_rawref(ref_filter_handler, &ref_cbdata);
else if (type & FILTER_REFS_ALL)
ret = for_each_ref(ref_filter_handler, &ref_cbdata);
else if (type)
if (!filter->kind)
die("filter_refs: invalid type");
else {
/*
* For common cases where we need only branches or remotes or tags,
* we only iterate through those refs. If a mix of refs is needed,
* we iterate over all refs and filter out required refs with the help
* of filter_ref_kind().
*/
if (filter->kind == FILTER_REFS_BRANCHES)
ret = for_each_fullref_in("refs/heads/", ref_filter_handler, &ref_cbdata, broken);
else if (filter->kind == FILTER_REFS_REMOTES)
ret = for_each_fullref_in("refs/remotes/", ref_filter_handler, &ref_cbdata, broken);
else if (filter->kind == FILTER_REFS_TAGS)
ret = for_each_fullref_in("refs/tags/", ref_filter_handler, &ref_cbdata, broken);
else if (filter->kind & FILTER_REFS_ALL)
ret = for_each_fullref_in("", ref_filter_handler, &ref_cbdata, broken);
if (!ret && (filter->kind & FILTER_REFS_DETACHED_HEAD))
head_ref(ref_filter_handler, &ref_cbdata);
}
/* Filters that need revision walking */
if (filter->merge_commit)
@ -1153,19 +1475,19 @@ static int cmp_ref_sorting(struct ref_sorting *s, struct ref_array_item *a, stru
get_ref_atom_value(a, s->atom, &va);
get_ref_atom_value(b, s->atom, &vb);
switch (cmp_type) {
case FIELD_STR:
if (s->version)
cmp = versioncmp(va->s, vb->s);
else if (cmp_type == FIELD_STR)
cmp = strcmp(va->s, vb->s);
break;
default:
else {
if (va->ul < vb->ul)
cmp = -1;
else if (va->ul == vb->ul)
cmp = 0;
else
cmp = 1;
break;
}
return (s->reverse) ? -cmp : cmp;
}
@ -1190,32 +1512,6 @@ void ref_array_sort(struct ref_sorting *sorting, struct ref_array *array)
qsort(array->items, array->nr, sizeof(struct ref_array_item *), compare_refs);
}
static void print_value(struct atom_value *v, int quote_style)
{
struct strbuf sb = STRBUF_INIT;
switch (quote_style) {
case QUOTE_NONE:
fputs(v->s, stdout);
break;
case QUOTE_SHELL:
sq_quote_buf(&sb, v->s);
break;
case QUOTE_PERL:
perl_quote_buf(&sb, v->s);
break;
case QUOTE_PYTHON:
python_quote_buf(&sb, v->s);
break;
case QUOTE_TCL:
tcl_quote_buf(&sb, v->s);
break;
}
if (quote_style != QUOTE_NONE) {
fputs(sb.buf, stdout);
strbuf_release(&sb);
}
}
static int hex1(char ch)
{
if ('0' <= ch && ch <= '9')
@ -1234,8 +1530,10 @@ static int hex2(const char *cp)
return -1;
}
static void emit(const char *cp, const char *ep)
static void append_literal(const char *cp, const char *ep, struct ref_formatting_state *state)
{
struct strbuf *s = &state->stack->output;
while (*cp && (!ep || cp < ep)) {
if (*cp == '%') {
if (cp[1] == '%')
@ -1243,13 +1541,13 @@ static void emit(const char *cp, const char *ep)
else {
int ch = hex2(cp + 1);
if (0 <= ch) {
putchar(ch);
strbuf_addch(s, ch);
cp += 3;
continue;
}
}
}
putchar(*cp);
strbuf_addch(s, *cp);
cp++;
}
}
@ -1257,19 +1555,24 @@ static void emit(const char *cp, const char *ep)
void show_ref_array_item(struct ref_array_item *info, const char *format, int quote_style)
{
const char *cp, *sp, *ep;
struct strbuf *final_buf;
struct ref_formatting_state state = REF_FORMATTING_STATE_INIT;
state.quote_style = quote_style;
push_stack_element(&state.stack);
for (cp = format; *cp && (sp = find_next(cp)); cp = ep + 1) {
struct atom_value *atomv;
ep = strchr(sp, ')');
if (cp < sp)
emit(cp, sp);
append_literal(cp, sp, &state);
get_ref_atom_value(info, parse_ref_filter_atom(sp + 2, ep), &atomv);
print_value(atomv, quote_style);
atomv->handler(atomv, &state);
}
if (*cp) {
sp = cp + strlen(cp);
emit(cp, sp);
append_literal(cp, sp, &state);
}
if (need_color_reset_at_eol) {
struct atom_value resetv;
@ -1278,8 +1581,13 @@ void show_ref_array_item(struct ref_array_item *info, const char *format, int qu
if (color_parse("reset", color) < 0)
die("BUG: couldn't parse 'reset' as a color");
resetv.s = color;
print_value(&resetv, quote_style);
append_atom(&resetv, &state);
}
if (state.stack->prev)
die(_("format: %%(end) atom missing"));
final_buf = &state.stack->output;
fwrite(final_buf->buf, 1, final_buf->len, stdout);
pop_stack_element(&state.stack);
putchar('\n');
}
@ -1312,6 +1620,9 @@ int parse_opt_ref_sorting(const struct option *opt, const char *arg, int unset)
s->reverse = 1;
arg++;
}
if (skip_prefix(arg, "version:", &arg) ||
skip_prefix(arg, "v:", &arg))
s->version = 1;
len = strlen(arg);
s->atom = parse_ref_filter_atom(arg, arg+len);
return 0;

View File

@ -13,23 +13,29 @@
#define QUOTE_PYTHON 4
#define QUOTE_TCL 8
#define FILTER_REFS_INCLUDE_BROKEN 0x1
#define FILTER_REFS_ALL 0x2
#define FILTER_REFS_INCLUDE_BROKEN 0x0001
#define FILTER_REFS_TAGS 0x0002
#define FILTER_REFS_BRANCHES 0x0004
#define FILTER_REFS_REMOTES 0x0008
#define FILTER_REFS_OTHERS 0x0010
#define FILTER_REFS_ALL (FILTER_REFS_TAGS | FILTER_REFS_BRANCHES | \
FILTER_REFS_REMOTES | FILTER_REFS_OTHERS)
#define FILTER_REFS_DETACHED_HEAD 0x0020
#define FILTER_REFS_KIND_MASK (FILTER_REFS_ALL | FILTER_REFS_DETACHED_HEAD)
struct atom_value {
const char *s;
unsigned long ul; /* used for sorting when not FIELD_STR */
};
struct atom_value;
struct ref_sorting {
struct ref_sorting *next;
int atom; /* index into used_atom array (internal) */
unsigned reverse : 1;
unsigned reverse : 1,
version : 1;
};
struct ref_array_item {
unsigned char objectname[20];
int flag;
unsigned int kind;
const char *symref;
struct commit *commit;
struct atom_value *value;
@ -53,7 +59,10 @@ struct ref_filter {
} merge;
struct commit *merge_commit;
unsigned int with_commit_tag_algo : 1;
unsigned int with_commit_tag_algo : 1,
match_as_path : 1;
unsigned int kind,
lines;
};
struct ref_filter_cbdata {

9
refs.c
View File

@ -2131,6 +2131,15 @@ int for_each_ref_in(const char *prefix, each_ref_fn fn, void *cb_data)
return do_for_each_ref(&ref_cache, prefix, fn, strlen(prefix), 0, cb_data);
}
int for_each_fullref_in(const char *prefix, each_ref_fn fn, void *cb_data, unsigned int broken)
{
unsigned int flag = 0;
if (broken)
flag = DO_FOR_EACH_INCLUDE_BROKEN;
return do_for_each_ref(&ref_cache, prefix, fn, 0, flag, cb_data);
}
int for_each_ref_in_submodule(const char *submodule, const char *prefix,
each_ref_fn fn, void *cb_data)
{

1
refs.h
View File

@ -173,6 +173,7 @@ typedef int each_ref_fn(const char *refname,
extern int head_ref(each_ref_fn fn, void *cb_data);
extern int for_each_ref(each_ref_fn fn, void *cb_data);
extern int for_each_ref_in(const char *prefix, each_ref_fn fn, void *cb_data);
extern int for_each_fullref_in(const char *prefix, each_ref_fn fn, void *cb_data, unsigned int broken);
extern int for_each_tag_ref(each_ref_fn fn, void *cb_data);
extern int for_each_branch_ref(each_ref_fn fn, void *cb_data);
extern int for_each_remote_ref(each_ref_fn fn, void *cb_data);

View File

@ -81,4 +81,178 @@ test_expect_success 'filtering with --contains' '
test_cmp expect actual
'
test_expect_success '%(color) must fail' '
test_must_fail git for-each-ref --format="%(color)%(refname)"
'
test_expect_success 'left alignment is default' '
cat >expect <<-\EOF &&
refname is refs/heads/master |refs/heads/master
refname is refs/heads/side |refs/heads/side
refname is refs/odd/spot |refs/odd/spot
refname is refs/tags/double-tag|refs/tags/double-tag
refname is refs/tags/four |refs/tags/four
refname is refs/tags/one |refs/tags/one
refname is refs/tags/signed-tag|refs/tags/signed-tag
refname is refs/tags/three |refs/tags/three
refname is refs/tags/two |refs/tags/two
EOF
git for-each-ref --format="%(align:30)refname is %(refname)%(end)|%(refname)" >actual &&
test_cmp expect actual
'
test_expect_success 'middle alignment' '
cat >expect <<-\EOF &&
| refname is refs/heads/master |refs/heads/master
| refname is refs/heads/side |refs/heads/side
| refname is refs/odd/spot |refs/odd/spot
|refname is refs/tags/double-tag|refs/tags/double-tag
| refname is refs/tags/four |refs/tags/four
| refname is refs/tags/one |refs/tags/one
|refname is refs/tags/signed-tag|refs/tags/signed-tag
| refname is refs/tags/three |refs/tags/three
| refname is refs/tags/two |refs/tags/two
EOF
git for-each-ref --format="|%(align:middle,30)refname is %(refname)%(end)|%(refname)" >actual &&
test_cmp expect actual
'
test_expect_success 'right alignment' '
cat >expect <<-\EOF &&
| refname is refs/heads/master|refs/heads/master
| refname is refs/heads/side|refs/heads/side
| refname is refs/odd/spot|refs/odd/spot
|refname is refs/tags/double-tag|refs/tags/double-tag
| refname is refs/tags/four|refs/tags/four
| refname is refs/tags/one|refs/tags/one
|refname is refs/tags/signed-tag|refs/tags/signed-tag
| refname is refs/tags/three|refs/tags/three
| refname is refs/tags/two|refs/tags/two
EOF
git for-each-ref --format="|%(align:30,right)refname is %(refname)%(end)|%(refname)" >actual &&
test_cmp expect actual
'
# Individual atoms inside %(align:...) and %(end) must not be quoted.
test_expect_success 'alignment with format quote' "
cat >expect <<-\EOF &&
|' '\''master| A U Thor'\'' '|
|' '\''side| A U Thor'\'' '|
|' '\''odd/spot| A U Thor'\'' '|
|' '\''double-tag| '\'' '|
|' '\''four| A U Thor'\'' '|
|' '\''one| A U Thor'\'' '|
|' '\''signed-tag| '\'' '|
|' '\''three| A U Thor'\'' '|
|' '\''two| A U Thor'\'' '|
EOF
git for-each-ref --shell --format=\"|%(align:30,middle)'%(refname:short)| %(authorname)'%(end)|\" >actual &&
test_cmp expect actual
"
test_expect_success 'nested alignment with quote formatting' "
cat >expect <<-\EOF &&
|' master '|
|' side '|
|' odd/spot '|
|' double-tag '|
|' four '|
|' one '|
|' signed-tag '|
|' three '|
|' two '|
EOF
git for-each-ref --shell --format='|%(align:30,left)%(align:15,right)%(refname:short)%(end)%(end)|' >actual &&
test_cmp expect actual
"
test_expect_success 'check `%(contents:lines=1)`' '
cat >expect <<-\EOF &&
master |three
side |four
odd/spot |three
double-tag |Annonated doubly
four |four
one |one
signed-tag |A signed tag message
three |three
two |two
EOF
git for-each-ref --format="%(refname:short) |%(contents:lines=1)" >actual &&
test_cmp expect actual
'
test_expect_success 'check `%(contents:lines=0)`' '
cat >expect <<-\EOF &&
master |
side |
odd/spot |
double-tag |
four |
one |
signed-tag |
three |
two |
EOF
git for-each-ref --format="%(refname:short) |%(contents:lines=0)" >actual &&
test_cmp expect actual
'
test_expect_success 'check `%(contents:lines=99999)`' '
cat >expect <<-\EOF &&
master |three
side |four
odd/spot |three
double-tag |Annonated doubly
four |four
one |one
signed-tag |A signed tag message
three |three
two |two
EOF
git for-each-ref --format="%(refname:short) |%(contents:lines=99999)" >actual &&
test_cmp expect actual
'
test_expect_success '`%(contents:lines=-1)` should fail' '
test_must_fail git for-each-ref --format="%(refname:short) |%(contents:lines=-1)"
'
test_expect_success 'setup for version sort' '
test_commit foo1.3 &&
test_commit foo1.6 &&
test_commit foo1.10
'
test_expect_success 'version sort' '
git for-each-ref --sort=version:refname --format="%(refname:short)" refs/tags/ | grep "foo" >actual &&
cat >expect <<-\EOF &&
foo1.3
foo1.6
foo1.10
EOF
test_cmp expect actual
'
test_expect_success 'version sort (shortened)' '
git for-each-ref --sort=v:refname --format="%(refname:short)" refs/tags/ | grep "foo" >actual &&
cat >expect <<-\EOF &&
foo1.3
foo1.6
foo1.10
EOF
test_cmp expect actual
'
test_expect_success 'reverse version sort' '
git for-each-ref --sort=-version:refname --format="%(refname:short)" refs/tags/ | grep "foo" >actual &&
cat >expect <<-\EOF &&
foo1.10
foo1.6
foo1.3
EOF
test_cmp expect actual
'
test_done

View File

@ -1462,13 +1462,7 @@ test_expect_success 'invalid sort parameter on command line' '
test_expect_success 'invalid sort parameter in configuratoin' '
git config tag.sort "v:notvalid" &&
git tag -l "foo*" >actual &&
cat >expect <<-\EOF &&
foo1.10
foo1.3
foo1.6
EOF
test_cmp expect actual
test_must_fail git tag -l "foo*"
'
test_expect_success 'version sort with prerelease reordering' '
@ -1525,4 +1519,43 @@ EOF"
test_cmp expect actual
'
test_expect_success '--format should list tags as per format given' '
cat >expect <<-\EOF &&
refname : refs/tags/foo1.10
refname : refs/tags/foo1.3
refname : refs/tags/foo1.6
refname : refs/tags/foo1.6-rc1
refname : refs/tags/foo1.6-rc2
EOF
git tag -l --format="refname : %(refname)" "foo*" >actual &&
test_cmp expect actual
'
test_expect_success 'setup --merged test tags' '
git tag mergetest-1 HEAD~2 &&
git tag mergetest-2 HEAD~1 &&
git tag mergetest-3 HEAD
'
test_expect_success '--merged cannot be used in non-list mode' '
test_must_fail git tag --merged=mergetest-2 foo
'
test_expect_success '--merged shows merged tags' '
cat >expect <<-\EOF &&
mergetest-1
mergetest-2
EOF
git tag -l --merged=mergetest-2 mergetest-* >actual &&
test_cmp expect actual
'
test_expect_success '--no-merged show unmerged tags' '
cat >expect <<-\EOF &&
mergetest-3
EOF
git tag -l --no-merged=mergetest-2 mergetest-* >actual &&
test_cmp expect actual
'
test_done

21
utf8.c
View File

@ -644,3 +644,24 @@ int skip_utf8_bom(char **text, size_t len)
*text += strlen(utf8_bom);
return 1;
}
void strbuf_utf8_align(struct strbuf *buf, align_type position, unsigned int width,
const char *s)
{
int slen = strlen(s);
int display_len = utf8_strnwidth(s, slen, 0);
int utf8_compensation = slen - display_len;
if (display_len >= width) {
strbuf_addstr(buf, s);
return;
}
if (position == ALIGN_LEFT)
strbuf_addf(buf, "%-*s", width + utf8_compensation, s);
else if (position == ALIGN_MIDDLE) {
int left = (width - display_len) / 2;
strbuf_addf(buf, "%*s%-*s", left, "", width - left + utf8_compensation, s);
} else if (position == ALIGN_RIGHT)
strbuf_addf(buf, "%*s", width + utf8_compensation, s);
}

15
utf8.h
View File

@ -55,4 +55,19 @@ int mbs_chrlen(const char **text, size_t *remainder_p, const char *encoding);
*/
int is_hfs_dotgit(const char *path);
typedef enum {
ALIGN_LEFT,
ALIGN_MIDDLE,
ALIGN_RIGHT
} align_type;
/*
* Align the string given and store it into a strbuf as per the
* 'position' and 'width'. If the given string length is larger than
* 'width' than then the input string is not truncated and no
* alignment is done.
*/
void strbuf_utf8_align(struct strbuf *buf, align_type position, unsigned int width,
const char *s);
#endif